Merge branch 'develop' into ui/agent-dropdown
This commit is contained in:
commit
9316995e62
176 changed files with 5398 additions and 801 deletions
|
@ -30,3 +30,6 @@ exclude_patterns:
|
|||
- "**/*.md"
|
||||
- "**/*.yml"
|
||||
- "app/javascript/dashboard/i18n/locale"
|
||||
- "stories/**/*"
|
||||
- "**/*.stories.js"
|
||||
- "**/stories/"
|
||||
|
|
|
@ -101,6 +101,7 @@ Rails/BulkChangeTable:
|
|||
- 'db/migrate/20170511134418_latlong.rb'
|
||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
|
|
|
@ -8,8 +8,7 @@ const custom = require('../config/webpack/environment');
|
|||
module.exports = {
|
||||
stories: [
|
||||
'../stories/**/*.stories.mdx',
|
||||
'../app/javascript/dashboard/components/ui/stories/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
'../stories/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
'../app/javascript/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
{
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -80,6 +80,8 @@ gem 'twitty'
|
|||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow'
|
||||
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
|
@ -105,6 +107,8 @@ gem 'maxminddb'
|
|||
# to create db triggers
|
||||
gem 'hairtrigger'
|
||||
|
||||
gem 'procore-sift'
|
||||
|
||||
group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
|
@ -112,7 +116,7 @@ group :development do
|
|||
gem 'web-console'
|
||||
|
||||
# used in swagger build
|
||||
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
|
||||
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: '131b11294fd6af9c428171f38516e6222a58c874'
|
||||
|
||||
# When we want to squash migrations
|
||||
gem 'squasher'
|
||||
|
|
173
Gemfile.lock
173
Gemfile.lock
|
@ -7,10 +7,10 @@ GIT
|
|||
|
||||
GIT
|
||||
remote: https://github.com/tzmfreedom/json_refs
|
||||
revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
|
||||
ref: e32deb0
|
||||
revision: 131b11294fd6af9c428171f38516e6222a58c874
|
||||
ref: 131b11294fd6af9c428171f38516e6222a58c874
|
||||
specs:
|
||||
json_refs (0.1.2)
|
||||
json_refs (0.1.6)
|
||||
hana
|
||||
|
||||
GEM
|
||||
|
@ -18,58 +18,58 @@ GEM
|
|||
specs:
|
||||
action-cable-testing (0.6.1)
|
||||
actioncable (>= 5.0)
|
||||
actioncable (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
actioncable (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
activestorage (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
actionmailbox (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
actionview (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
actionmailer (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.3.6)
|
||||
actionview (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
actionpack (6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
activestorage (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
actiontext (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
actionview (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
activejob (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
activerecord (6.0.3.6)
|
||||
activemodel (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
activemodel (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord (6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord-import (1.0.7)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
activestorage (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
marcel (~> 1.0.0)
|
||||
activesupport (6.0.3.6)
|
||||
activesupport (6.0.3.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
|
@ -79,11 +79,10 @@ GEM
|
|||
activerecord (>= 5.0, < 6.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.14.0)
|
||||
actionpack (>= 4.2)
|
||||
actionview (>= 4.2)
|
||||
activerecord (>= 4.2)
|
||||
autoprefixer-rails (>= 6.0)
|
||||
administrate (0.16.0)
|
||||
actionpack (>= 5.0)
|
||||
actionview (>= 5.0)
|
||||
activerecord (>= 5.0)
|
||||
datetime_picker_rails (~> 0.0.7)
|
||||
jquery-rails (>= 4.0)
|
||||
kaminari (>= 1.0)
|
||||
|
@ -95,8 +94,6 @@ GEM
|
|||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.1)
|
||||
attr_extras (6.2.4)
|
||||
autoprefixer-rails (9.8.6.3)
|
||||
execjs
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.360.0)
|
||||
aws-sdk-core (3.105.0)
|
||||
|
@ -205,12 +202,18 @@ GEM
|
|||
faraday (~> 1.0)
|
||||
fcm (1.0.2)
|
||||
faraday (~> 1.0.0)
|
||||
ffi (1.14.2)
|
||||
ffi (1.15.0)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.4.1)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.3.4)
|
||||
google-protobuf (~> 3.12, >= 3.12.2)
|
||||
googleapis-common-protos (>= 1.3.9, < 2.0)
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.6.3)
|
||||
gli (2.19.2)
|
||||
globalid (0.4.2)
|
||||
|
@ -223,12 +226,18 @@ GEM
|
|||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
google-cloud-core (1.5.0)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.3.3)
|
||||
google-cloud-dialogflow (1.2.0)
|
||||
google-cloud-core (~> 1.5)
|
||||
google-cloud-dialogflow-v2 (~> 0.1)
|
||||
google-cloud-dialogflow-v2 (0.6.4)
|
||||
gapic-common (~> 0.3)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.0.1)
|
||||
google-cloud-errors (1.1.0)
|
||||
google-cloud-storage (1.28.0)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
|
@ -236,7 +245,14 @@ GEM
|
|||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.13.1)
|
||||
google-protobuf (3.15.8)
|
||||
googleapis-common-protos (1.3.11)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.0.6)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.16.2)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
|
@ -245,12 +261,15 @@ GEM
|
|||
signet (~> 0.14)
|
||||
groupdate (5.1.0)
|
||||
activesupport (>= 5)
|
||||
grpc (1.37.1)
|
||||
google-protobuf (~> 3.15)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.0)
|
||||
hairtrigger (0.2.23)
|
||||
activerecord (>= 5.0, < 7)
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
hana (1.3.6)
|
||||
hana (1.3.7)
|
||||
hashdiff (1.0.1)
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
|
@ -261,7 +280,7 @@ GEM
|
|||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.9)
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
inflecto (0.0.2)
|
||||
|
@ -273,7 +292,7 @@ GEM
|
|||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.3.1)
|
||||
jwt (2.2.2)
|
||||
jwt (2.2.3)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
|
@ -298,12 +317,12 @@ GEM
|
|||
listen (3.3.3)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.9.0)
|
||||
loofah (2.9.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (1.0.0)
|
||||
marcel (1.0.1)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
|
@ -311,8 +330,8 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2020.0512)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.3)
|
||||
mini_portile2 (2.5.0)
|
||||
mini_mime (1.1.0)
|
||||
mini_portile2 (2.5.1)
|
||||
minitest (5.14.4)
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
|
@ -324,7 +343,7 @@ GEM
|
|||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.7)
|
||||
nokogiri (1.11.2)
|
||||
nokogiri (1.11.3)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
|
@ -334,12 +353,14 @@ GEM
|
|||
parser (2.7.1.4)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.2.3)
|
||||
procore-sift (0.15.0)
|
||||
rails (> 4.2.0)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.5)
|
||||
public_suffix (4.0.6)
|
||||
puma (4.3.6)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
|
@ -355,29 +376,29 @@ GEM
|
|||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.0.3.6)
|
||||
actioncable (= 6.0.3.6)
|
||||
actionmailbox (= 6.0.3.6)
|
||||
actionmailer (= 6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
actiontext (= 6.0.3.6)
|
||||
actionview (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
activemodel (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
activestorage (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
rails (6.0.3.7)
|
||||
actioncable (= 6.0.3.7)
|
||||
actionmailbox (= 6.0.3.7)
|
||||
actionmailer (= 6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actiontext (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.3.6)
|
||||
railties (= 6.0.3.7)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
railties (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
railties (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
|
@ -489,7 +510,7 @@ GEM
|
|||
sidekiq-cron (1.2.0)
|
||||
fugit (~> 1.1)
|
||||
sidekiq (>= 4.2.1)
|
||||
signet (0.14.0)
|
||||
signet (0.15.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
|
@ -612,6 +633,7 @@ DEPENDENCIES
|
|||
flag_shih_tzu
|
||||
foreman
|
||||
geocoder
|
||||
google-cloud-dialogflow
|
||||
google-cloud-storage
|
||||
groupdate
|
||||
haikunator
|
||||
|
@ -629,6 +651,7 @@ DEPENDENCIES
|
|||
mini_magick
|
||||
mock_redis!
|
||||
pg
|
||||
procore-sift
|
||||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
|
|
38
app/builders/campaigns/campaign_conversation_builder.rb
Normal file
38
app/builders/campaigns/campaign_conversation_builder.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
class Campaigns::CampaignConversationBuilder
|
||||
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes]
|
||||
|
||||
def perform
|
||||
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
||||
@campaign = @contact_inbox.inbox.campaigns.find_by!(display_id: campaign_display_id)
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact_inbox.lock!
|
||||
|
||||
# We won't send campaigns if a conversation is already present
|
||||
return if @contact_inbox.reload.conversations.present?
|
||||
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform
|
||||
end
|
||||
@conversation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_params
|
||||
ActionController::Parameters.new({
|
||||
content: @campaign.message
|
||||
})
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @campaign.account_id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
campaign_id: @campaign.id,
|
||||
additional_attributes: conversation_additional_attributes
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,4 +1,11 @@
|
|||
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
include Sift
|
||||
|
||||
sort_on :email, type: :string
|
||||
sort_on :name, type: :string
|
||||
sort_on :phone_number, type: :string
|
||||
sort_on :last_activity_at, type: :datetime
|
||||
|
||||
RESULTS_PER_PAGE = 15
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
|
@ -68,7 +75,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
@resolved_contacts ||= Current.account.contacts
|
||||
.where.not(email: [nil, ''])
|
||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||
.order('LOWER(name)')
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
|
@ -76,11 +82,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def fetch_contact_last_seen_at(contacts)
|
||||
contacts.left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count, MAX(conversations.contact_last_seen_at) as last_seen_at')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
|
|
13
app/controllers/api/v1/widget/campaigns_controller.rb
Normal file
13
app/controllers/api/v1/widget/campaigns_controller.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController
|
||||
skip_before_action :set_contact
|
||||
|
||||
def index
|
||||
@campaigns = @web_widget.inbox.campaigns.where(enabled: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
|
@ -2,7 +2,8 @@ class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
|||
include Events::Types
|
||||
|
||||
def create
|
||||
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox, event_info: event_info)
|
||||
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox,
|
||||
event_info: permitted_params[:event_info].to_h.merge(event_info))
|
||||
head :no_content
|
||||
end
|
||||
|
||||
|
@ -17,6 +18,6 @@ class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :website_token)
|
||||
params.permit(:name, :website_token, event_info: {})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,8 @@ class AsyncDispatcher < BaseDispatcher
|
|||
[
|
||||
EventListener.instance,
|
||||
WebhookListener.instance,
|
||||
InstallationWebhookListener.instance, HookListener.instance
|
||||
InstallationWebhookListener.instance, HookListener.instance,
|
||||
CampaignListener.instance
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
9
app/javascript/dashboard/api/campaigns.js
Normal file
9
app/javascript/dashboard/api/campaigns.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ApiClient from './ApiClient';
|
||||
|
||||
class CampaignsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('campaigns', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new CampaignsAPI();
|
|
@ -6,8 +6,8 @@ class ContactAPI extends ApiClient {
|
|||
super('contacts', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(page) {
|
||||
return axios.get(`${this.url}?page=${page}`);
|
||||
get(page, sortAttr = 'name') {
|
||||
return axios.get(`${this.url}?page=${page}&sort=${sortAttr}`);
|
||||
}
|
||||
|
||||
getConversations(contactId) {
|
||||
|
@ -18,8 +18,10 @@ class ContactAPI extends ApiClient {
|
|||
return axios.get(`${this.url}/${contactId}/contactable_inboxes`);
|
||||
}
|
||||
|
||||
search(search = '', page = 1) {
|
||||
return axios.get(`${this.url}/search?q=${search}&page=${page}`);
|
||||
search(search = '', page = 1, sortAttr = 'name') {
|
||||
return axios.get(
|
||||
`${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
border-radius: $space-smaller;
|
||||
font-size: $font-size-mini;
|
||||
|
|
|
@ -358,7 +358,7 @@ $form-label-font-weight: $font-weight-medium;
|
|||
$form-label-line-height: 1.8;
|
||||
$select-background: $white;
|
||||
$select-triangle-color: $dark-gray;
|
||||
$select-radius: $global-radius;
|
||||
$select-radius: var(--border-radius-normal);
|
||||
$input-color: $header-color;
|
||||
$input-placeholder-color: $light-gray;
|
||||
$input-font-family: inherit;
|
||||
|
@ -374,19 +374,19 @@ $input-shadow-focus: 0;
|
|||
$input-cursor-disabled: not-allowed;
|
||||
$input-transition: border-color 0.25s ease-in-out;
|
||||
$input-number-spinners: true;
|
||||
$input-radius: $global-radius;
|
||||
$form-button-radius: $global-radius;
|
||||
$input-radius: var(--border-radius-normal);
|
||||
$form-button-radius: var(--border-radius-normal);
|
||||
|
||||
// 20. Label
|
||||
// ---------
|
||||
|
||||
$label-background: lighten($primary-color, 40%);
|
||||
$label-color: $primary-color;
|
||||
$label-background: $primary-color;
|
||||
$label-color: $white;
|
||||
$label-color-alt: $black;
|
||||
$label-palette: $foundation-palette;
|
||||
$label-font-size: $font-size-micro;
|
||||
$label-font-size: $font-size-mini;
|
||||
$label-padding: $space-smaller $space-small;
|
||||
$label-radius: $space-micro;
|
||||
$label-radius: var(--border-radius-small);
|
||||
|
||||
// 21. Media Object
|
||||
// ----------------
|
||||
|
|
|
@ -51,25 +51,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
border-radius: var(--space-smaller) !important;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.input-group-label:first-child {
|
||||
border-bottom-left-radius: var(--space-smaller);
|
||||
border-top-left-radius: var(--space-smaller);
|
||||
}
|
||||
|
||||
.input-group-field {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.justify-space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
|
|
@ -236,3 +236,11 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin three-column-grid($column-one-width: 25.6rem,
|
||||
$column-three-width: 25.6rem) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax($column-one-width, 6fr) 10fr minmax($column-three-width, 6fr);
|
||||
}
|
||||
|
|
|
@ -24,3 +24,4 @@
|
|||
|
||||
@import 'foundation-custom';
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/forms';
|
||||
|
|
|
@ -39,20 +39,25 @@ $resolve-button-width: 13.2rem;
|
|||
.user {
|
||||
@include flex;
|
||||
@include flex-align($x: center, $y: middle);
|
||||
margin-right: var(--space-normal);
|
||||
min-width: 0;
|
||||
|
||||
.user--name {
|
||||
@include margin(0);
|
||||
display: inline-block;
|
||||
font-size: $font-size-medium;
|
||||
line-height: 1.3;
|
||||
text-transform: capitalize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user--profile__meta {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
margin-left: $space-slab;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user--profile__button {
|
||||
|
|
|
@ -132,7 +132,6 @@
|
|||
|
||||
.bubble {
|
||||
@include bubble-with-types;
|
||||
max-width: 50rem;
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
|
||||
|
@ -236,7 +235,9 @@
|
|||
|
||||
.wrap {
|
||||
@include margin($zero $space-normal);
|
||||
max-width: 69%;
|
||||
|
||||
--bubble-max-width: 49.6rem;
|
||||
max-width: Min(var(--bubble-max-width), 85%);
|
||||
|
||||
.sender--name {
|
||||
font-size: $font-size-mini;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
.error {
|
||||
|
||||
#{$all-text-inputs},
|
||||
select,
|
||||
.multiselect > .multiselect__tags {
|
||||
.multiselect>.multiselect__tags {
|
||||
@include thin-border(var(--r-400));
|
||||
}
|
||||
|
||||
|
@ -33,3 +34,10 @@ input {
|
|||
.help-text {
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
|
||||
.input-group.small {
|
||||
input {
|
||||
font-size: var(--font-size-small);
|
||||
height: var(--space-large);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import Button from './ui/WootButton';
|
|||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
import Label from './widgets/Label.vue';
|
||||
import Label from './ui/Label';
|
||||
import LoadingState from './widgets/LoadingState';
|
||||
import Modal from './Modal';
|
||||
import ModalHeader from './ModalHeader';
|
||||
|
@ -26,6 +28,8 @@ const WootUIKit = {
|
|||
Code,
|
||||
ColorPicker,
|
||||
DeleteModal,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
Input,
|
||||
LoadingState,
|
||||
Label,
|
||||
|
|
|
@ -20,22 +20,28 @@
|
|||
:key="status.value"
|
||||
class="status-items"
|
||||
>
|
||||
<button
|
||||
class="button clear status-change--dropdown-button"
|
||||
:disabled="status.disabled"
|
||||
<woot-button
|
||||
variant="clear"
|
||||
class-names="status-change--dropdown-button"
|
||||
:is-disabled="status.disabled"
|
||||
@click="changeAvailabilityStatus(status.value)"
|
||||
>
|
||||
<availability-status-badge :status="status.value" />
|
||||
{{ status.label }}
|
||||
</button>
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<button class="status-change--change-button" @click="openStatusMenu">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class-names="status-change--change-button link"
|
||||
@click="openStatusMenu"
|
||||
>
|
||||
{{ $t('SIDEBAR_ITEMS.CHANGE_AVAILABILITY_STATUS') }}
|
||||
</button>
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -156,15 +162,5 @@ export default {
|
|||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
& &--change-button {
|
||||
color: var(--b-600);
|
||||
font-size: var(--font-size-small);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--w-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -144,6 +144,7 @@ export default {
|
|||
cssClass: 'menu-title align-justify',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
newLinkRouteName: 'settings_inbox_new',
|
||||
children: this.inboxes.map(inbox => ({
|
||||
id: inbox.id,
|
||||
label: inbox.name,
|
||||
|
@ -158,10 +159,13 @@ export default {
|
|||
icon: 'ion-pound',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: true,
|
||||
newLink: true,
|
||||
key: 'label',
|
||||
cssClass: 'menu-title align-justify',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||
toStateName: 'labels_list',
|
||||
showModalForNewItem: true,
|
||||
modalName: 'AddLabel',
|
||||
children: this.accountLabels.map(label => ({
|
||||
id: label.id,
|
||||
label: label.title,
|
||||
|
@ -178,10 +182,12 @@ export default {
|
|||
icon: 'ion-ios-people',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: true,
|
||||
newLink: true,
|
||||
key: 'team',
|
||||
cssClass: 'menu-title align-justify teams-sidebar-menu',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/teams`),
|
||||
toStateName: 'teams_list',
|
||||
newLinkRouteName: 'settings_teams_new',
|
||||
children: this.teams.map(team => ({
|
||||
id: team.id,
|
||||
label: team.name,
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<span
|
||||
v-if="showItem(menuItem)"
|
||||
class="child-icon ion-android-add-circle"
|
||||
@click.prevent="newLinkClick"
|
||||
@click.prevent="newLinkClick(menuItem)"
|
||||
/>
|
||||
</a>
|
||||
<ul v-if="menuItem.hasSubMenu" class="nested vertical menu">
|
||||
|
@ -52,6 +52,11 @@
|
|||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
<add-label-modal
|
||||
v-if="showAddLabel"
|
||||
:show.sync="showAddLabel"
|
||||
:on-close="hideAddLabelPopup"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
|
@ -61,8 +66,17 @@ import { mapGetters } from 'vuex';
|
|||
import router from '../../routes';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||
export default {
|
||||
components: {
|
||||
AddLabelModal,
|
||||
},
|
||||
mixins: [adminMixin],
|
||||
data() {
|
||||
return {
|
||||
showAddLabel: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
|
@ -108,12 +122,24 @@ export default {
|
|||
if (!child.truncateLabel) return false;
|
||||
return child.label;
|
||||
},
|
||||
newLinkClick() {
|
||||
router.push({ name: 'settings_inbox_new', params: { page: 'new' } });
|
||||
newLinkClick(item) {
|
||||
if (item.newLinkRouteName) {
|
||||
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
|
||||
} else if (item.showModalForNewItem) {
|
||||
if (item.modalName === 'AddLabel') {
|
||||
this.showAddLabelPopup();
|
||||
}
|
||||
}
|
||||
},
|
||||
showItem(item) {
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabel = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabel = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -3,11 +3,13 @@ import { createLocalVue, mount } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
import WootButton from 'dashboard/components/ui/WootButton';
|
||||
import i18n from 'dashboard/i18n';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.component('woot-button', WootButton);
|
||||
|
||||
const i18nConfig = new VueI18n({
|
||||
locale: 'en',
|
||||
|
|
|
@ -4,7 +4,7 @@ import SidemenuIcon from '../SidemenuIcon';
|
|||
describe('SidemenuIcon', () => {
|
||||
test('matches snapshot', () => {
|
||||
const wrapper = mount(SidemenuIcon);
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
expect(wrapper.vm).toBeTruthy();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
148
app/javascript/dashboard/components/ui/Label.vue
Normal file
148
app/javascript/dashboard/components/ui/Label.vue
Normal file
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<div :class="labelClass" :style="labelStyle" :title="description">
|
||||
<i v-if="icon" class="label--icon" :class="icon" @click="onClick" />
|
||||
<span v-if="!href">{{ title }}</span>
|
||||
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
|
||||
<i v-if="showClose" class="close--icon ion-close" @click="onClick" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getContrastingTextColor } from 'shared/helpers/ColorHelper';
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
colorScheme: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.bgColor);
|
||||
},
|
||||
labelClass() {
|
||||
return `label ${this.colorScheme} ${this.small ? 'small' : ''}`;
|
||||
},
|
||||
labelStyle() {
|
||||
if (this.bgColor) {
|
||||
return { background: this.bgColor, color: this.textColor };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
anchorStyle() {
|
||||
if (this.bgColor) {
|
||||
return { color: this.textColor };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('click', this.title);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.label {
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-right: var(--space-smaller);
|
||||
margin-bottom: var(--space-smaller);
|
||||
|
||||
&.small {
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
.label--icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.label--icon,
|
||||
.close--icon {
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
&.small .label--icon,
|
||||
&.small .close--icon {
|
||||
font-size: var(--font-size-nano);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: var(--font-size-mini);
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* Color Schemes */
|
||||
&.primary {
|
||||
background: var(--w-100);
|
||||
color: var(--w-900);
|
||||
border: 1px solid var(--w-200);
|
||||
a {
|
||||
color: var(--w-900);
|
||||
}
|
||||
}
|
||||
&.secondary {
|
||||
background: var(--s-100);
|
||||
color: var(--s-900);
|
||||
border: 1px solid var(--s-200);
|
||||
a {
|
||||
color: var(--s-900);
|
||||
}
|
||||
}
|
||||
&.success {
|
||||
background: var(--g-100);
|
||||
color: var(--g-900);
|
||||
border: 1px solid var(--g-200);
|
||||
a {
|
||||
color: var(--g-900);
|
||||
}
|
||||
}
|
||||
&.alert {
|
||||
background: var(--r-100);
|
||||
color: var(--r-900);
|
||||
border: 1px solid var(--r-200);
|
||||
a {
|
||||
color: var(--r-900);
|
||||
}
|
||||
}
|
||||
&.warning {
|
||||
background: var(--y-100);
|
||||
color: var(--y-900);
|
||||
border: 1px solid var(--y-300);
|
||||
a {
|
||||
color: var(--y-900);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Components/Label',
|
||||
argTypes: {
|
||||
title: {
|
||||
defaultValue: 'sales',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
colorScheme: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['primary', 'secondary', 'success', 'alert', 'warning'],
|
||||
},
|
||||
},
|
||||
description: {
|
||||
defaultValue: 'label',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
href: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
bgColor: {
|
||||
defaultValue: '#a83262',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
small: {
|
||||
defaultValue: false,
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
showClose: {
|
||||
defaultValue: false,
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
defaultValue: 'ion-close',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
template: '<woot-label v-bind="$props" @click="onClick"></woot-label>',
|
||||
});
|
||||
|
||||
export const DefaultLabel = Template.bind({});
|
||||
DefaultLabel.args = {
|
||||
onClick: action('clicked'),
|
||||
};
|
|
@ -1,87 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
:class="labelClass"
|
||||
:style="{ background: bgColor, color: textColor }"
|
||||
:title="description"
|
||||
>
|
||||
<span v-if="!href">{{ title }}</span>
|
||||
<a v-else :href="href" :style="{ color: textColor }">{{ title }}</a>
|
||||
<i v-if="showIcon" class="label--icon" :class="icon" @click="onClick" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getContrastingTextColor } from 'shared/helpers/ColorHelper';
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#1f93ff',
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'ion-close',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.bgColor);
|
||||
},
|
||||
labelClass() {
|
||||
return `label ${this.small ? 'small' : ''}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('click', this.title);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
font-size: $font-size-small;
|
||||
line-height: 1;
|
||||
margin: $space-micro;
|
||||
|
||||
&.small {
|
||||
font-size: $font-size-mini;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label--icon {
|
||||
cursor: pointer;
|
||||
font-size: $font-size-micro;
|
||||
line-height: 1.5;
|
||||
margin-left: $space-smaller;
|
||||
}
|
||||
</style>
|
|
@ -12,39 +12,41 @@
|
|||
v-if="totalCount"
|
||||
class="primary button-group pagination-button-group"
|
||||
>
|
||||
<button
|
||||
class="button small goto-first"
|
||||
:class="firstPageButtonClass"
|
||||
<woot-button
|
||||
size="small"
|
||||
class-names="goto-first"
|
||||
:is-disabled="hasFirstPage"
|
||||
@click="onFirstPage"
|
||||
>
|
||||
<i class="ion-chevron-left" />
|
||||
<i class="ion-chevron-left" />
|
||||
</button>
|
||||
<button
|
||||
class="button small"
|
||||
:class="prevPageButtonClass"
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
:is-disabled="hasPrevPage"
|
||||
@click="onPrevPage"
|
||||
>
|
||||
<i class="ion-chevron-left" />
|
||||
</button>
|
||||
<button class="button" @click.prevent>
|
||||
</woot-button>
|
||||
<woot-button @click.prevent>
|
||||
{{ currentPage }}
|
||||
</button>
|
||||
<button
|
||||
class="button small"
|
||||
:class="nextPageButtonClass"
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
:is-disabled="hasNextPage"
|
||||
@click="onNextPage"
|
||||
>
|
||||
<i class="ion-chevron-right" />
|
||||
</button>
|
||||
<button
|
||||
class="button small goto-last"
|
||||
:class="lastPageButtonClass"
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
class-names="goto-last"
|
||||
:is-disabled="hasLastPage"
|
||||
@click="onLastPage"
|
||||
>
|
||||
<i class="ion-chevron-right" />
|
||||
<i class="ion-chevron-right" />
|
||||
</button>
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -91,35 +93,19 @@ export default {
|
|||
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||
return isDisabled;
|
||||
},
|
||||
lastPageButtonClass() {
|
||||
const className = this.hasLastPage ? 'disabled' : '';
|
||||
return className;
|
||||
},
|
||||
hasFirstPage() {
|
||||
const isDisabled = this.currentPage === 1;
|
||||
return isDisabled;
|
||||
},
|
||||
firstPageButtonClass() {
|
||||
const className = this.hasFirstPage ? 'disabled' : '';
|
||||
return className;
|
||||
},
|
||||
hasNextPage() {
|
||||
const isDisabled =
|
||||
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||
return isDisabled;
|
||||
},
|
||||
nextPageButtonClass() {
|
||||
const className = this.hasNextPage ? 'disabled' : '';
|
||||
return className;
|
||||
},
|
||||
hasPrevPage() {
|
||||
const isDisabled = this.currentPage === 1;
|
||||
return isDisabled;
|
||||
},
|
||||
prevPageButtonClass() {
|
||||
const className = this.hasPrevPage ? 'disabled' : '';
|
||||
return className;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNextPage() {
|
||||
|
|
|
@ -80,6 +80,7 @@ export default {
|
|||
.conversation-details-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
border-left: 1px solid var(--color-border);
|
||||
background: var(--color-background-light);
|
||||
|
|
|
@ -186,10 +186,12 @@ export default {
|
|||
if (this.isPrivate) {
|
||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||
}
|
||||
|
||||
if (this.isAFacebookInbox) {
|
||||
return MESSAGE_MAX_LENGTH.FACEBOOK;
|
||||
}
|
||||
if (this.isATwilioWhatsappChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
||||
}
|
||||
if (this.isATwilioSMSChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,79 @@
|
|||
{
|
||||
"CAMPAIGN": {
|
||||
"HEADER": "Campaigns",
|
||||
"HEADER_BTN_TXT": "Create Campaign",
|
||||
"SIDEBAR_TXT": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations. Click on <b>Add Campaign</b> to create a new campaign. You can also edit or delete an existing campaign by clicking on the Edit or Delete button.",
|
||||
"HEADER_BTN_TXT": "Create a campaign",
|
||||
"ADD": {
|
||||
"TITLE": "Create a campaign",
|
||||
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"CREATE_BUTTON_TEXT": "Create",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
"ERROR": "Message is required"
|
||||
},
|
||||
"SENT_BY": {
|
||||
"LABEL": "Sent by",
|
||||
"PLACEHOLDER": "Please select the the content of campaign",
|
||||
"ERROR": "Sender is required"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "URL",
|
||||
"PLACEHOLDER": "Please enter the URL",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"TIME_ON_PAGE": {
|
||||
"LABEL": "Time on page(Seconds)",
|
||||
"PLACEHOLDER": "Please enter the time",
|
||||
"ERROR": "Time on page is required"
|
||||
},
|
||||
"ENABLED": "Enable campaign",
|
||||
"SUBMIT": "Add Campaign"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Campaign created successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit campaign",
|
||||
"UPDATE_BUTTON_TEXT": "Update",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Campaign updated successfully",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no campaigns attached to this inbox"
|
||||
"LOADING_MESSAGE": "Loading campaigns...",
|
||||
"404": "There are no campaigns created for this inbox.",
|
||||
"TABLE_HEADER": {
|
||||
"TITLE": "Title",
|
||||
"MESSAGE": "Message",
|
||||
"STATUS": "Status",
|
||||
"SENDER": "Sender",
|
||||
"URL": "URL",
|
||||
"TIME_ON_PAGE": "Time(Seconds)",
|
||||
"CREATED_AT": "Created at"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"ADD": "Add",
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete"
|
||||
},
|
||||
"STATUS": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"SENDER": {
|
||||
"BOT": "Bot"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,11 +28,17 @@
|
|||
"INACTIVE_LABELS": "Labels available in the account",
|
||||
"REMOVE": "Click on X icon to remove the label",
|
||||
"ADD": "Click on + icon to add the label",
|
||||
"ADD_BUTTON": "Add Labels",
|
||||
"UPDATE_BUTTON": "Update labels",
|
||||
"UPDATE_ERROR": "Couldn't update labels, try again."
|
||||
},
|
||||
"NO_LABELS_TO_ADD": "There are no more labels defined in the account.",
|
||||
"NO_AVAILABLE_LABELS": "There are no labels added to this conversation."
|
||||
"NO_AVAILABLE_LABELS": "There are no labels added to this conversation.",
|
||||
"LABEL_SELECT": {
|
||||
"TITLE": "Add Labels",
|
||||
"PLACEHOLDER": "Search labels",
|
||||
"NO_RESULT": "No labels found"
|
||||
}
|
||||
},
|
||||
"MUTE_CONTACT": "Mute Conversation",
|
||||
"UNMUTE_CONTACT": "Unmute Conversation",
|
||||
|
@ -151,5 +157,18 @@
|
|||
},
|
||||
"VIEW_DETAILS": "View details"
|
||||
}
|
||||
},
|
||||
"NOTES": {
|
||||
"HEADER": {
|
||||
"TITLE": "Notes"
|
||||
},
|
||||
"ADD": {
|
||||
"BUTTON": "Add",
|
||||
"PLACEHOLDER": "Add a note",
|
||||
"TITLE": "Shift + Enter to create a note"
|
||||
},
|
||||
"FOOTER": {
|
||||
"BUTTON": "View all notes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div class="contact-attribute">
|
||||
<div class="title-wrap">
|
||||
<h4 class="text-block-title title">
|
||||
<div class="title--icon">
|
||||
<emoji-or-icon :icon="icon" :emoji="emoji" />
|
||||
</div>
|
||||
{{ label }}
|
||||
</h4>
|
||||
</div>
|
||||
<div v-show="isEditing">
|
||||
<div class="input-group small">
|
||||
<input
|
||||
ref="inputfield"
|
||||
v-model="editedValue"
|
||||
type="text"
|
||||
class="input-group-field"
|
||||
autofocus="true"
|
||||
@keyup.enter="onUpdate"
|
||||
/>
|
||||
<div class="input-group-button">
|
||||
<woot-button size="small" icon="ion-checkmark" @click="onUpdate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="!isEditing"
|
||||
class="value--view"
|
||||
:class="{ 'is-editable': showEdit }"
|
||||
>
|
||||
<p class="value">
|
||||
{{ value || '---' }}
|
||||
</p>
|
||||
<woot-button
|
||||
v-if="showEdit"
|
||||
variant="clear link"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="ion-compose"
|
||||
class-names="edit-button"
|
||||
@click="onEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
props: {
|
||||
label: { type: String, required: true },
|
||||
icon: { type: String, default: '' },
|
||||
emoji: { type: String, default: '' },
|
||||
value: { type: [String, Number], default: '' },
|
||||
showEdit: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
editedValue: this.value,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.inputfield.focus();
|
||||
},
|
||||
onEdit() {
|
||||
this.isEditing = true;
|
||||
this.$nextTick(() => {
|
||||
this.focusInput();
|
||||
});
|
||||
},
|
||||
onUpdate() {
|
||||
this.isEditing = false;
|
||||
this.$emit('update', this.editedValue);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contact-attribute {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
.title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-mini);
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
.title--icon {
|
||||
width: var(--space-two);
|
||||
}
|
||||
.edit-button {
|
||||
display: none;
|
||||
}
|
||||
.value--view {
|
||||
display: flex;
|
||||
|
||||
&.is-editable:hover {
|
||||
.value {
|
||||
background: var(--color-background);
|
||||
}
|
||||
.edit-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.value {
|
||||
display: inline-block;
|
||||
min-width: var(--space-mega);
|
||||
border-radius: var(--border-radius-small);
|
||||
word-break: break-all;
|
||||
margin: 0 var(--space-smaller) 0 var(--space-normal);
|
||||
padding: var(--space-micro) var(--space-smaller);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="contact-fields">
|
||||
<h3 class="block-title title">Contact fields</h3>
|
||||
<attribute
|
||||
:label="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
icon="ion-email"
|
||||
emoji=""
|
||||
:value="contact.email"
|
||||
:show-edit="true"
|
||||
@update="onEmailUpdate"
|
||||
/>
|
||||
<attribute
|
||||
:label="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
icon="ion-ios-telephone"
|
||||
emoji=""
|
||||
:value="contact.phone_number"
|
||||
:show-edit="true"
|
||||
@update="onPhoneUpdate"
|
||||
/>
|
||||
<attribute
|
||||
v-if="additionalAttributes.location"
|
||||
:label="$t('CONTACT_PANEL.LOCATION')"
|
||||
icon="ion-map"
|
||||
emoji="🌍"
|
||||
:value="additionalAttributes.location"
|
||||
:show-edit="true"
|
||||
@update="onLocationUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Attribute from './ContactAttribute';
|
||||
|
||||
export default {
|
||||
components: { Attribute },
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
company() {
|
||||
const { company = {} } = this.contact;
|
||||
return company;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onEmailUpdate(value) {
|
||||
this.$emit('update', { email: value });
|
||||
},
|
||||
onPhoneUpdate(value) {
|
||||
this.$emit('update', { phone: value });
|
||||
},
|
||||
onLocationUpdate(value) {
|
||||
this.$emit('update', { location: value });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contact-fields {
|
||||
margin-top: var(--space-medium);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="contact--intro">
|
||||
<thumbnail
|
||||
:src="contact.thumbnail"
|
||||
size="64px"
|
||||
:username="contact.name"
|
||||
:status="contact.availability_status"
|
||||
/>
|
||||
|
||||
<div class="contact--details">
|
||||
<h2 class="block-title contact--name">
|
||||
{{ contact.name }}
|
||||
</h2>
|
||||
<h3 class="sub-block-title contact--work">
|
||||
{{ contact.title }}
|
||||
<i v-if="company.name" class="icon ion-minus-round" />
|
||||
<span class="company-name">{{ company.name }}</span>
|
||||
</h3>
|
||||
<p v-if="additionalAttributes.description" class="contact--bio">
|
||||
{{ additionalAttributes.description }}
|
||||
</p>
|
||||
<social-icons :social-profiles="socialProfiles" />
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<woot-button
|
||||
class="new-message"
|
||||
size="small expanded"
|
||||
icon="ion-paper-airplane"
|
||||
@click="onNewMessageClick"
|
||||
>
|
||||
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small expanded"
|
||||
icon="ion-compose"
|
||||
@click="onEditClick"
|
||||
>
|
||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import SocialIcons from 'dashboard/routes/dashboard/conversation/contact/SocialIcons';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
SocialIcons,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
socialProfiles() {
|
||||
const {
|
||||
social_profiles: socialProfiles,
|
||||
screen_name: twitterScreenName,
|
||||
} = this.additionalAttributes;
|
||||
|
||||
return { twitter: twitterScreenName, ...(socialProfiles || {}) };
|
||||
},
|
||||
company() {
|
||||
const { company = {} } = this.contact;
|
||||
return company;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onEditClick() {
|
||||
this.$emit('edit');
|
||||
},
|
||||
onNewMessageClick() {
|
||||
this.$emit('message');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contact--details {
|
||||
margin-top: var(--space-small);
|
||||
}
|
||||
|
||||
.contact--work {
|
||||
color: var(--color-body);
|
||||
|
||||
.icon {
|
||||
font-size: var(--font-size-nano);
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.contact--name {
|
||||
text-transform: capitalize;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.contact--bio {
|
||||
margin: var(--space-smaller) 0 0;
|
||||
}
|
||||
|
||||
.button.new-message {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: var(--space-small);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<div class="panel">
|
||||
<contact-intro
|
||||
:contact="contact"
|
||||
@message="toggleConversationModal"
|
||||
@edit="toggleEditModal"
|
||||
/>
|
||||
<contact-fields :contact="contact" :edit="null" />
|
||||
<edit-contact
|
||||
v-if="showEditModal"
|
||||
:show="showEditModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleEditModal"
|
||||
/>
|
||||
<new-conversation
|
||||
:show="showConversationModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleConversationModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import EditContact from 'dashboard/routes/dashboard/conversation/contact/EditContact';
|
||||
import NewConversation from 'dashboard/routes/dashboard/conversation/contact/NewConversation';
|
||||
import ContactIntro from './ContactIntro';
|
||||
import ContactFields from './ContactFields';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditContact,
|
||||
NewConversation,
|
||||
ContactIntro,
|
||||
ContactFields,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showEditModal: false,
|
||||
showConversationModal: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleEditModal() {
|
||||
this.showEditModal = !this.showEditModal;
|
||||
},
|
||||
toggleConversationModal() {
|
||||
this.showConversationModal = !this.showConversationModal;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.panel {
|
||||
padding: var(--space-normal) var(--space-normal);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="wrap">
|
||||
<div class="left">
|
||||
<contact-panel v-if="!uiFlags.isFetchingItem" :contact="contact" />
|
||||
</div>
|
||||
<div class="center"></div>
|
||||
<div class="right"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactPanel from './ContactPanel';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactPanel,
|
||||
},
|
||||
props: {
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
}),
|
||||
showEmptySearchResult() {
|
||||
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
|
||||
return hasEmptyResults;
|
||||
},
|
||||
contact() {
|
||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getContactDetails();
|
||||
},
|
||||
methods: {
|
||||
getContactDetails() {
|
||||
if (this.contactId) {
|
||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.wrap {
|
||||
@include three-column-grid(27.2rem);
|
||||
background: var(--color-background);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.center {
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,43 @@
|
|||
import ContactAttribute from '../components/ContactAttribute';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Components/Contact/ContactAttribute',
|
||||
component: ContactAttribute,
|
||||
argTypes: {
|
||||
label: {
|
||||
defaultValue: 'Email',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
value: {
|
||||
defaultValue: 'dwight@schrute.farms',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
defaultValue: 'ion-email',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
showEdit: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { ContactAttribute },
|
||||
template: '<contact-attribute v-bind="$props" @update="onEdit" />',
|
||||
});
|
||||
|
||||
export const DefaultAttribute = Template.bind({});
|
||||
DefaultAttribute.args = {
|
||||
onEdit: action('edit'),
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import ContactFields from '../components/ContactFields';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Components/Contact/ContactFields',
|
||||
component: ContactFields,
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { ContactFields },
|
||||
template:
|
||||
'<contact-fields v-bind="$props" :contact="contact" @update="onUpdate" />',
|
||||
});
|
||||
|
||||
export const DefaultContactFields = Template.bind({});
|
||||
DefaultContactFields.args = {
|
||||
contact: {
|
||||
id: 979442,
|
||||
name: 'Eden Hazard',
|
||||
title: 'Playmaker',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/men/19.jpg',
|
||||
company: {
|
||||
id: 10,
|
||||
name: 'Chelsea',
|
||||
},
|
||||
email: 'hazard@chelsea.com',
|
||||
availability_status: 'offline',
|
||||
phone_number: '',
|
||||
custom_attributes: {},
|
||||
additional_attributes: {
|
||||
description:
|
||||
'Known for his dribbling, he is considered to be one of the best players in the world.',
|
||||
social_profiles: {
|
||||
twitter: 'hazardeden10',
|
||||
facebook: 'hazardeden10',
|
||||
linkedin: 'hazardeden10',
|
||||
},
|
||||
},
|
||||
},
|
||||
onUpdate: action('update'),
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import ContactIntro from '../components/ContactIntro';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Components/Contact/ContactIntro',
|
||||
component: ContactIntro,
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { ContactIntro },
|
||||
template:
|
||||
'<contact-intro v-bind="$props" :user="user" @edit="onEdit" @message="onNewMessage" />',
|
||||
});
|
||||
|
||||
export const DefaultContactIntro = Template.bind({});
|
||||
DefaultContactIntro.args = {
|
||||
contact: {
|
||||
id: 979442,
|
||||
name: 'Eden Hazard',
|
||||
title: 'Playmaker',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/men/19.jpg',
|
||||
company: {
|
||||
id: 10,
|
||||
name: 'Chelsea',
|
||||
},
|
||||
email: 'hazard@chelsea.com',
|
||||
availability_status: 'offline',
|
||||
phone_number: '',
|
||||
custom_attributes: {},
|
||||
additional_attributes: {
|
||||
description:
|
||||
'Known for his dribbling, he is considered to be one of the best players in the world.',
|
||||
social_profiles: {
|
||||
twitter: 'hazardeden10',
|
||||
facebook: 'hazardeden10',
|
||||
linkedin: 'hazardeden10',
|
||||
},
|
||||
},
|
||||
},
|
||||
onEdit: action('edit'),
|
||||
onNewMessage: action('new message 💬'),
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import AddNote from './AddNote.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Notes/Add',
|
||||
component: AddNote,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AddNote },
|
||||
template: '<add-note v-bind="$props" @add="onAdd"></add-note>',
|
||||
});
|
||||
|
||||
export const Add = Template.bind({});
|
||||
Add.args = {
|
||||
onAdd: action('Added'),
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div class="wrap">
|
||||
<div class="input-wrap">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
|
||||
class="input--note"
|
||||
@keydown.enter.shift.exact="onAdd"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
color-scheme="warning"
|
||||
:title="$t('NOTES.ADD.TITLE')"
|
||||
:is-disabled="buttonDisabled"
|
||||
class="button-wrap"
|
||||
@click="onAdd"
|
||||
>
|
||||
{{ $t('NOTES.ADD.BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
export default {
|
||||
components: {
|
||||
WootButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputText: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttonDisabled() {
|
||||
return this.inputText === '';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAdd() {
|
||||
if (this.inputText !== '') {
|
||||
this.$emit('add', this.inputText);
|
||||
}
|
||||
this.inputText = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-small);
|
||||
margin-bottom: var(--space-smaller);
|
||||
width: 100%;
|
||||
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
|
||||
.input--note {
|
||||
font-size: var(--font-size-mini);
|
||||
border-color: transparent;
|
||||
padding: var(--space-small) var(--space-small) 0 var(--space-small);
|
||||
resize: none;
|
||||
box-sizing: border-box;
|
||||
min-height: var(--space-larger);
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
|
||||
.button-wrap {
|
||||
float: right;
|
||||
margin-bottom: var(--space-small);
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import ContactNote from './ContactNote.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Notes/Note',
|
||||
component: ContactNote,
|
||||
argTypes: {
|
||||
id: {
|
||||
control: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
note: {
|
||||
defaultValue:
|
||||
'A copy and paste musical notes symbols & music symbols collection for easy access.',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
userName: {
|
||||
defaultValue: 'John Doe',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
timeStamp: {
|
||||
defaultValue: '1618046084',
|
||||
control: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
thumbnail: {
|
||||
defaultValue: 'https://randomuser.me/api/portraits/men/62.jpg',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { ContactNote },
|
||||
template:
|
||||
'<contact-note v-bind="$props" @edit="onEdit" @delete="onDelete"></contact-note>',
|
||||
});
|
||||
|
||||
export const Note = Template.bind({});
|
||||
Note.args = {
|
||||
onEdit: action('Edit'),
|
||||
onDelete: action('Delete'),
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
<div class="footer">
|
||||
<div class="meta">
|
||||
<div :title="userName">
|
||||
<Thumbnail :src="thumbnail" :username="userName" size="16" />
|
||||
<Thumbnail :src="thumbnail" :username="userName" size="16px" />
|
||||
</div>
|
||||
<div class="date-wrap">
|
||||
<span>{{ readableTime }}</span>
|
||||
|
@ -50,6 +50,10 @@ export default {
|
|||
mixins: [timeMixin],
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
note: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
@ -76,10 +80,10 @@ export default {
|
|||
|
||||
methods: {
|
||||
onEdit() {
|
||||
this.$emit('edit');
|
||||
this.$emit('edit', this.id);
|
||||
},
|
||||
onDelete() {
|
||||
this.$emit('delete');
|
||||
this.$emit('delete', this.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -90,6 +94,7 @@ export default {
|
|||
padding: var(--space-small);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-small);
|
||||
margin-bottom: var(--space-smaller);
|
||||
|
||||
.text {
|
||||
padding-bottom: var(--space-small);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
:columns="columns"
|
||||
:table-data="tableData"
|
||||
:border-around="false"
|
||||
:sort-option="sortOption"
|
||||
/>
|
||||
|
||||
<empty-state
|
||||
|
@ -57,37 +58,82 @@ export default {
|
|||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
sortParam: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
sortOrder: {
|
||||
type: String,
|
||||
default: 'asc',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columns: [
|
||||
sortConfig: {},
|
||||
sortOption: {
|
||||
sortAlways: true,
|
||||
sortChange: params => this.$emit('on-sort-change', params),
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tableData() {
|
||||
if (this.isLoading) {
|
||||
return [];
|
||||
}
|
||||
return this.contacts.map(item => {
|
||||
// Note: The attributes used here is in snake case
|
||||
// as it simplier the sort attribute calculation
|
||||
const additional = item.additional_attributes || {};
|
||||
const { last_activity_at: lastActivityAt } = item;
|
||||
return {
|
||||
...item,
|
||||
phone_number: item.phone_number || '---',
|
||||
company: additional.company_name || '---',
|
||||
location: additional.location || '---',
|
||||
profiles: additional.social_profiles || {},
|
||||
city: additional.city || '---',
|
||||
country: additional.country || '---',
|
||||
conversations_count: item.conversations_count || '---',
|
||||
last_activity_at: lastActivityAt
|
||||
? this.dynamicTime(lastActivityAt)
|
||||
: '---',
|
||||
};
|
||||
});
|
||||
},
|
||||
columns() {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
key: 'name',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
|
||||
fixed: 'left',
|
||||
align: 'left',
|
||||
sortBy: this.sortConfig.name || '',
|
||||
width: 300,
|
||||
renderBodyCell: ({ row }) => (
|
||||
<button
|
||||
class="row--user-block cursor-pointer"
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="expanded"
|
||||
onClick={() => this.onClickContact(row.id)}
|
||||
>
|
||||
<Thumbnail
|
||||
src={row.thumbnail}
|
||||
size="36px"
|
||||
username={row.name}
|
||||
status={row.availability_status}
|
||||
/>
|
||||
<div>
|
||||
<h6 class="sub-block-title user-name text-truncate">
|
||||
{row.name}
|
||||
</h6>
|
||||
<button class="button clear small">
|
||||
{this.$t('CONTACTS_PAGE.LIST.VIEW_DETAILS')}
|
||||
</button>
|
||||
<div class="row--user-block">
|
||||
<Thumbnail
|
||||
src={row.thumbnail}
|
||||
size="36px"
|
||||
username={row.name}
|
||||
status={row.availability_status}
|
||||
/>
|
||||
<div class="user-block">
|
||||
<h6 class="sub-block-title user-name text-truncate">
|
||||
{row.name}
|
||||
</h6>
|
||||
<span class="button clear small">
|
||||
{this.$t('CONTACTS_PAGE.LIST.VIEW_DETAILS')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</woot-button>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -95,6 +141,7 @@ export default {
|
|||
key: 'email',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
|
||||
align: 'left',
|
||||
sortBy: this.sortConfig.email || '',
|
||||
width: 240,
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.email)
|
||||
|
@ -113,8 +160,9 @@ export default {
|
|||
},
|
||||
},
|
||||
{
|
||||
field: 'phone',
|
||||
key: 'phone',
|
||||
field: 'phone_number',
|
||||
key: 'phone_number',
|
||||
sortBy: this.sortConfig.phone_number || '',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
|
||||
align: 'left',
|
||||
},
|
||||
|
@ -167,8 +215,9 @@ export default {
|
|||
},
|
||||
},
|
||||
{
|
||||
field: 'lastSeen',
|
||||
key: 'lastSeen',
|
||||
field: 'last_activity_at',
|
||||
key: 'last_activity_at',
|
||||
sortBy: this.sortConfig.last_activity_at || '',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
|
||||
align: 'left',
|
||||
},
|
||||
|
@ -179,29 +228,23 @@ export default {
|
|||
width: 150,
|
||||
align: 'left',
|
||||
},
|
||||
],
|
||||
};
|
||||
];
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tableData() {
|
||||
if (this.isLoading) {
|
||||
return [];
|
||||
}
|
||||
return this.contacts.map(item => {
|
||||
const additional = item.additional_attributes || {};
|
||||
const { last_seen_at: lastSeenAt } = item;
|
||||
return {
|
||||
...item,
|
||||
phone: item.phone_number || '---',
|
||||
company: additional.company_name || '---',
|
||||
location: additional.location || '---',
|
||||
profiles: additional.social_profiles || {},
|
||||
city: additional.city || '---',
|
||||
country: additional.country || '---',
|
||||
conversationsCount: item.conversations_count || '---',
|
||||
lastSeen: lastSeenAt ? this.dynamicTime(lastSeenAt) : '---',
|
||||
};
|
||||
});
|
||||
watch: {
|
||||
sortOrder() {
|
||||
this.setSortConfig();
|
||||
},
|
||||
sortParam() {
|
||||
this.setSortConfig();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setSortConfig();
|
||||
},
|
||||
methods: {
|
||||
setSortConfig() {
|
||||
this.sortConfig = { [this.sortParam]: this.sortOrder };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -225,6 +268,10 @@ export default {
|
|||
display: flex;
|
||||
text-align: left;
|
||||
|
||||
.user-block {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-thumbnail-box {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
@ -251,6 +298,9 @@ export default {
|
|||
.ve-table-header-th {
|
||||
font-size: var(--font-size-mini) !important;
|
||||
}
|
||||
.ve-table-sort {
|
||||
top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.contacts--loader {
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-contact="openContactInfoPanel"
|
||||
:active-contact-id="selectedContactId"
|
||||
:sort-config="sortConfig"
|
||||
@on-sort-change="onSortChange"
|
||||
/>
|
||||
<table-footer
|
||||
:on-page-change="onPageChange"
|
||||
|
@ -39,6 +41,8 @@ import ContactInfoPanel from './ContactInfoPanel';
|
|||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactsHeader,
|
||||
|
@ -52,6 +56,7 @@ export default {
|
|||
searchQuery: '',
|
||||
showCreateModal: false,
|
||||
selectedContactId: '',
|
||||
sortConfig: { name: 'asc' },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -81,43 +86,63 @@ export default {
|
|||
},
|
||||
pageParameter() {
|
||||
const selectedPageNumber = Number(this.$route.query?.page);
|
||||
return !Number.isNaN(selectedPageNumber) && selectedPageNumber >= 1
|
||||
return !Number.isNaN(selectedPageNumber) &&
|
||||
selectedPageNumber >= DEFAULT_PAGE
|
||||
? selectedPageNumber
|
||||
: 1;
|
||||
: DEFAULT_PAGE;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('contacts/get', { page: this.pageParameter });
|
||||
this.fetchContacts(this.pageParameter);
|
||||
},
|
||||
methods: {
|
||||
updatePageParam(page) {
|
||||
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
||||
},
|
||||
getSortAttribute() {
|
||||
let sortAttr = Object.keys(this.sortConfig).reduce((acc, sortKey) => {
|
||||
const sortOrder = this.sortConfig[sortKey];
|
||||
if (sortOrder) {
|
||||
const sortOrderSign = sortOrder === 'asc' ? '' : '-';
|
||||
return `${sortOrderSign}${sortKey}`;
|
||||
}
|
||||
return acc;
|
||||
}, '');
|
||||
if (!sortAttr) {
|
||||
this.sortConfig = { name: 'asc' };
|
||||
sortAttr = 'name';
|
||||
}
|
||||
return sortAttr;
|
||||
},
|
||||
fetchContacts(page) {
|
||||
this.updatePageParam(page);
|
||||
const requestParams = { page, sortAttr: this.getSortAttribute() };
|
||||
if (!this.searchQuery) {
|
||||
this.$store.dispatch('contacts/get', requestParams);
|
||||
} else {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: this.searchQuery,
|
||||
...requestParams,
|
||||
});
|
||||
}
|
||||
},
|
||||
onInputSearch(event) {
|
||||
const newQuery = event.target.value;
|
||||
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
||||
if (refetchAllContacts) {
|
||||
this.$store.dispatch('contacts/get', { page: 1 });
|
||||
}
|
||||
this.searchQuery = newQuery;
|
||||
if (refetchAllContacts) {
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
}
|
||||
},
|
||||
onSearchSubmit() {
|
||||
this.selectedContactId = '';
|
||||
if (this.searchQuery) {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: this.searchQuery,
|
||||
page: 1,
|
||||
});
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
}
|
||||
},
|
||||
onPageChange(page) {
|
||||
this.selectedContactId = '';
|
||||
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
||||
if (this.searchQuery) {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: this.searchQuery,
|
||||
page,
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch('contacts/get', { page });
|
||||
}
|
||||
this.fetchContacts(page);
|
||||
},
|
||||
openContactInfoPanel(contactId) {
|
||||
this.selectedContactId = contactId;
|
||||
|
@ -130,6 +155,10 @@ export default {
|
|||
onToggleCreate() {
|
||||
this.showCreateModal = !this.showCreateModal;
|
||||
},
|
||||
onSortChange(params) {
|
||||
this.sortConfig = params;
|
||||
this.fetchContacts(this.meta.currentPage);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -138,6 +167,7 @@ export default {
|
|||
.contacts-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -17,18 +17,22 @@
|
|||
@keyup.enter="onSearchSubmit"
|
||||
@input="onInputSearch"
|
||||
/>
|
||||
<woot-submit-button
|
||||
:button-text="$t('CONTACTS_PAGE.SEARCH_BUTTON')"
|
||||
:loading="false"
|
||||
:button-class="searchButtonClass"
|
||||
<woot-button
|
||||
:is-loading="false"
|
||||
:class-names="searchButtonClass"
|
||||
@click="onSearchSubmit"
|
||||
/>
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
<button class="button success icon" @click="onToggleCreate">
|
||||
<i class="icon ion-android-add-circle" />
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
icon="ion-android-add-circle"
|
||||
@click="onToggleCreate"
|
||||
>
|
||||
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
||||
</button>
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import noteList from './NoteList';
|
||||
|
||||
export default {
|
||||
title: 'Components/Notes/List',
|
||||
component: noteList,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { noteList },
|
||||
template:
|
||||
'<note-list v-bind="$props" @addNote="onAddNote" @editNote="onEditNote" @deleteNote="onDeleteNote" @show="onClick"></note-list>',
|
||||
});
|
||||
|
||||
export const List = Template.bind({});
|
||||
List.args = {
|
||||
onClick: action('show'),
|
||||
onAddNote: action('added'),
|
||||
onEditNote: action('edit'),
|
||||
onDeleteNote: action('deleted'),
|
||||
notes: [
|
||||
{
|
||||
id: '12345',
|
||||
content:
|
||||
'It is a long established fact that a reader will be distracted.',
|
||||
user: {
|
||||
name: 'John Doe',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/men/69.jpg',
|
||||
},
|
||||
created_at: 1618046084,
|
||||
},
|
||||
{
|
||||
id: '12346',
|
||||
content:
|
||||
'It is simply dummy text of the printing and typesetting industry.',
|
||||
user: {
|
||||
name: 'Pearl Cruz',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/women/29.jpg',
|
||||
},
|
||||
created_at: 1616046076,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="notelist-wrap">
|
||||
<h3 class="block-title">
|
||||
{{ $t('NOTES.HEADER.TITLE') }}
|
||||
</h3>
|
||||
<add-note @add="onAddNote" />
|
||||
<contact-note
|
||||
v-for="note in notes"
|
||||
:id="note.id"
|
||||
:key="note.id"
|
||||
:note="note.content"
|
||||
:user-name="note.user.name"
|
||||
:time-stamp="note.created_at"
|
||||
:thumbnail="note.user.thumbnail"
|
||||
@edit="onEditNote"
|
||||
@delete="onDeleteNote"
|
||||
/>
|
||||
<div class="button-wrap">
|
||||
<woot-button variant="clear link" class="button" @click="onclick">
|
||||
{{ $t('NOTES.FOOTER.BUTTON') }}
|
||||
<i class="ion-arrow-right-c" />
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContactNote from './ContactNote';
|
||||
import AddNote from './AddNote';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactNote,
|
||||
AddNote,
|
||||
},
|
||||
|
||||
props: {
|
||||
notes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onclick() {
|
||||
this.$emit('show');
|
||||
},
|
||||
onAddNote(value) {
|
||||
this.$emit('addNote', value);
|
||||
},
|
||||
onEditNote(value) {
|
||||
this.$emit('editNote', value);
|
||||
},
|
||||
onDeleteNote(value) {
|
||||
this.$emit('deleteNote', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-wrap {
|
||||
margin-top: var(--space-one);
|
||||
}
|
||||
</style>
|
|
@ -1,12 +1,29 @@
|
|||
<template>
|
||||
<div class="contact-manage-view row"></div>
|
||||
<div class="contact-manage-view">
|
||||
<contacts-header
|
||||
:search-query="searchQuery"
|
||||
:on-search-submit="onSearchSubmit"
|
||||
:on-input-search="onInputSearch"
|
||||
:on-toggle-create="onToggleCreate"
|
||||
/>
|
||||
<manage-layout :contact-id="contactId" />
|
||||
|
||||
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactsHeader from '../components/Header';
|
||||
import ManageLayout from 'dashboard/modules/contact/components/ManageLayout';
|
||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
components: {
|
||||
ContactsHeader,
|
||||
CreateContact,
|
||||
ManageLayout,
|
||||
},
|
||||
props: {
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
|
@ -14,7 +31,10 @@ export default {
|
|||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
return {
|
||||
searchQuery: '',
|
||||
showCreateModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
@ -26,7 +46,28 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {},
|
||||
methods: {
|
||||
onInputSearch(event) {
|
||||
const newQuery = event.target.value;
|
||||
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
||||
if (refetchAllContacts) {
|
||||
this.$store.dispatch('contacts/get', { page: 1 });
|
||||
}
|
||||
this.searchQuery = newQuery;
|
||||
},
|
||||
onSearchSubmit() {
|
||||
this.selectedContactId = '';
|
||||
if (this.searchQuery) {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: this.searchQuery,
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
onToggleCreate() {
|
||||
this.showCreateModal = !this.showCreateModal;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
:title="label.title"
|
||||
:description="label.description"
|
||||
:bg-color="label.color"
|
||||
:show-icon="true"
|
||||
:show-close="true"
|
||||
@click="onRemove"
|
||||
/>
|
||||
</div>
|
||||
|
@ -44,7 +44,6 @@
|
|||
:title="label.title"
|
||||
:description="label.description"
|
||||
:bg-color="label.color"
|
||||
:show-icon="true"
|
||||
icon="ion-plus"
|
||||
@click="onAdd"
|
||||
/>
|
||||
|
|
|
@ -258,7 +258,7 @@
|
|||
<weekly-availability :inbox="inbox" />
|
||||
</div>
|
||||
<div v-if="selectedTabKey === 'campaign'">
|
||||
<campaign />
|
||||
<campaign :selected-agents="selectedAgents" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -335,10 +335,6 @@ export default {
|
|||
if (this.isAWebWidgetInbox) {
|
||||
return [
|
||||
...visibleToAllChannelTabs,
|
||||
{
|
||||
key: 'campaign',
|
||||
name: this.$t('INBOX_MGMT.TABS.CAMPAIGN'),
|
||||
},
|
||||
{
|
||||
key: 'preChatForm',
|
||||
name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'),
|
||||
|
|
|
@ -56,7 +56,8 @@ import { required } from 'vuelidate/lib/validators';
|
|||
import router from '../../../../index';
|
||||
import PageHeader from '../../SettingsSubPageHeader';
|
||||
|
||||
const shouldBeWebhookUrl = (value = '') => value.startsWith('http');
|
||||
const shouldBeWebhookUrl = (value = '') =>
|
||||
value ? value.startsWith('http') : true;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -76,7 +77,7 @@ export default {
|
|||
},
|
||||
validations: {
|
||||
channelName: { required },
|
||||
webhookUrl: { required, shouldBeWebhookUrl },
|
||||
webhookUrl: { shouldBeWebhookUrl },
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CAMPAIGN.ADD.TITLE')"
|
||||
:header-content="$t('CAMPAIGN.ADD.DESC')"
|
||||
/>
|
||||
<form class="row" @submit.prevent="addCampaign">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.title.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.TITLE.LABEL') }}
|
||||
<input
|
||||
v-model.trim="title"
|
||||
type="text"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TITLE.PLACEHOLDER')"
|
||||
@input="$v.title.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.message.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="message"
|
||||
rows="5"
|
||||
type="text"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.MESSAGE.PLACEHOLDER')"
|
||||
@input="$v.message.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.selectedSender.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }}
|
||||
<select v-model="selectedSender">
|
||||
<option
|
||||
v-for="sender in sendersAndBotList"
|
||||
:key="sender.name"
|
||||
:value="sender.id"
|
||||
>
|
||||
{{ sender.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="$v.selectedSender.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.endPoint.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.END_POINT.LABEL') }}
|
||||
<input
|
||||
v-model.trim="endPoint"
|
||||
type="text"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.END_POINT.PLACEHOLDER')"
|
||||
@input="$v.endPoint.$touch"
|
||||
/>
|
||||
<span v-if="$v.endPoint.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.timeOnPage.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.LABEL') }}
|
||||
<input
|
||||
v-model.trim="timeOnPage"
|
||||
type="number"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.PLACEHOLDER')"
|
||||
@input="$v.timeOnPage.$touch"
|
||||
/>
|
||||
<span v-if="$v.timeOnPage.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label>
|
||||
<input
|
||||
v-model="enabled"
|
||||
type="checkbox"
|
||||
value="enabled"
|
||||
name="enabled"
|
||||
/>
|
||||
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:disabled="buttonDisabled"
|
||||
:loading="uiFlags.isCreating"
|
||||
:button-text="$t('CAMPAIGN.ADD.CREATE_BUTTON_TEXT')"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CAMPAIGN.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
||||
import Modal from 'dashboard/components/Modal';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
senderList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
message: '',
|
||||
selectedSender: 0,
|
||||
endPoint: '',
|
||||
timeOnPage: 10,
|
||||
show: true,
|
||||
enabled: true,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
title: {
|
||||
required,
|
||||
},
|
||||
message: {
|
||||
required,
|
||||
},
|
||||
selectedSender: {
|
||||
required,
|
||||
},
|
||||
endPoint: {
|
||||
required,
|
||||
minLength: minLength(7),
|
||||
url,
|
||||
},
|
||||
timeOnPage: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'campaigns/getUIFlags',
|
||||
}),
|
||||
buttonDisabled() {
|
||||
return (
|
||||
this.$v.message.$invalid ||
|
||||
this.$v.title.$invalid ||
|
||||
this.$v.selectedSender.$invalid ||
|
||||
this.$v.endPoint.$invalid ||
|
||||
this.$v.timeOnPage.$invalid ||
|
||||
this.uiFlags.isCreating
|
||||
);
|
||||
},
|
||||
sendersAndBotList() {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Bot',
|
||||
},
|
||||
...this.senderList,
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async addCampaign() {
|
||||
try {
|
||||
await this.$store.dispatch('campaigns/create', {
|
||||
title: this.title,
|
||||
message: this.message,
|
||||
inbox_id: this.$route.params.inboxId,
|
||||
sender_id: this.selectedSender || null,
|
||||
enabled: this.enabled,
|
||||
trigger_rules: {
|
||||
url: this.endPoint,
|
||||
time_on_page: this.timeOnPage,
|
||||
},
|
||||
});
|
||||
this.showAlert(this.$t('CAMPAIGN.ADD.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('CAMPAIGN.ADD.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content-box .page-top-bar::v-deep {
|
||||
padding: var(--space-large) var(--space-large) var(--space-zero);
|
||||
}
|
||||
</style>
|
|
@ -1,37 +1,92 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<div class="row">
|
||||
<a class="button icon success nice button--fixed-right-top">
|
||||
<i class="icon ion-android-add-circle"></i>
|
||||
<div class="row button-wrapper">
|
||||
<woot-button icon="ion-android-add-circle" @click="openAddPopup">
|
||||
{{ $t('CAMPAIGN.HEADER_BTN_TXT') }}
|
||||
</a>
|
||||
</woot-button>
|
||||
</div>
|
||||
<campaigns-table
|
||||
:campaigns="records"
|
||||
:show-empty-result="showEmptyResult"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-edit-click="openEditPopup"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div class="small-8 columns">
|
||||
<p class="no-items-error-message">
|
||||
{{ $t('CAMPAIGN.LIST.404') }}
|
||||
<a>
|
||||
{{ $t('CAMPAIGN.HEADER_BTN_TXT') }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="small-4 columns">
|
||||
<span>
|
||||
<p>
|
||||
<b> {{ $t('CAMPAIGN.HEADER') }}</b>
|
||||
</p>
|
||||
<p>
|
||||
Proactive messages allows customer send outbound messages to their
|
||||
contacts which would trigger more conversations. Campaigns are tied
|
||||
to inbox. Click on
|
||||
<b>Add Campaign</b>
|
||||
to create a new campaign. You can also edit or delete an existing
|
||||
campaigns Response by clicking on the Edit or Delete button.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<add-campaign :on-close="hideAddPopup" :sender-list="selectedAgents" />
|
||||
</woot-modal>
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<edit-campaign
|
||||
:on-close="hideEditPopup"
|
||||
:selected-campaign="selectedCampaign"
|
||||
:sender-list="selectedAgents"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddCampaign from './AddCampaign';
|
||||
import CampaignsTable from './CampaignsTable';
|
||||
import EditCampaign from './EditCampaign';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddCampaign,
|
||||
CampaignsTable,
|
||||
EditCampaign,
|
||||
},
|
||||
props: {
|
||||
selectedAgents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
campaigns: [],
|
||||
showAddPopup: false,
|
||||
showEditPopup: false,
|
||||
selectedCampaign: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
records: 'campaigns/getCampaigns',
|
||||
uiFlags: 'campaigns/getUIFlags',
|
||||
}),
|
||||
showEmptyResult() {
|
||||
const hasEmptyResults =
|
||||
!this.uiFlags.isFetching && this.records.length === 0;
|
||||
return hasEmptyResults;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('campaigns/get');
|
||||
},
|
||||
methods: {
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
},
|
||||
openEditPopup(response) {
|
||||
const { row: campaign } = response;
|
||||
this.selectedCampaign = campaign;
|
||||
this.showEditPopup = true;
|
||||
},
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: var(--space-one);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="row--user-block">
|
||||
<Thumbnail
|
||||
:src="sender.thumbnail"
|
||||
size="20px"
|
||||
:username="sender.name"
|
||||
:status="sender.availability_status"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-block-title text-truncate">
|
||||
{{ sender.name }}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
sender: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.row--user-block {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
|
||||
.user-name {
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.user-thumbnail-box {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,213 @@
|
|||
<template>
|
||||
<section class="campaigns-table-wrap">
|
||||
<ve-table
|
||||
:columns="columns"
|
||||
scroll-width="155rem"
|
||||
:table-data="tableData"
|
||||
:border-around="true"
|
||||
/>
|
||||
|
||||
<empty-state v-if="showEmptyResult" :title="$t('CAMPAIGN.LIST.404')" />
|
||||
<div v-if="isLoading" class="campaign--loader">
|
||||
<spinner />
|
||||
<span>{{ $t('CAMPAIGN.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { VeTable } from 'vue-easytable';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Label from 'dashboard/components/ui/Label';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
import CampaignSender from './CampaignSender';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
Spinner,
|
||||
VeTable,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
campaigns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showEmptyResult: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onEditClick: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
field: 'title',
|
||||
key: 'title',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.TITLE'),
|
||||
fixed: 'left',
|
||||
align: 'left',
|
||||
renderBodyCell: ({ row }) => (
|
||||
<div class="row--title-block">
|
||||
<h6 class="sub-block-title title text-truncate">{row.title}</h6>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
field: 'message',
|
||||
key: 'message',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.MESSAGE'),
|
||||
align: 'left',
|
||||
width: 350,
|
||||
renderBodyCell: ({ row }) => {
|
||||
return (
|
||||
<div class="text-truncate">
|
||||
<span title={row.message}>{row.message}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
key: 'enabled',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.STATUS'),
|
||||
align: 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
const labelText = row.enabled
|
||||
? this.$t('CAMPAIGN.LIST.STATUS.ENABLED')
|
||||
: this.$t('CAMPAIGN.LIST.STATUS.DISABLED');
|
||||
const colorScheme = row.enabled ? 'success' : 'secondary';
|
||||
return <Label title={labelText} colorScheme={colorScheme} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sender',
|
||||
key: 'sender',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.SENDER'),
|
||||
align: 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.sender) return <CampaignSender sender={row.sender} />;
|
||||
return this.$t('CAMPAIGN.LIST.SENDER.BOT');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
key: 'url',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.URL'),
|
||||
align: 'left',
|
||||
renderBodyCell: ({ row }) => (
|
||||
<div class="text-truncate">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href={row.url}
|
||||
title={row.url}
|
||||
>
|
||||
{row.url}
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'timeOnPage',
|
||||
key: 'timeOnPage',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.TIME_ON_PAGE'),
|
||||
align: 'left',
|
||||
},
|
||||
|
||||
{
|
||||
field: 'buttons',
|
||||
key: 'buttons',
|
||||
title: '',
|
||||
align: 'left',
|
||||
renderBodyCell: (row) => (
|
||||
<div class="button-wrapper">
|
||||
<WootButton
|
||||
variant="clear"
|
||||
icon="ion-edit"
|
||||
color-scheme="secondary"
|
||||
classNames="hollow grey-btn"
|
||||
onClick={() => this.onEditClick(row)}
|
||||
>
|
||||
{this.$t('CAMPAIGN.LIST.BUTTONS.EDIT')}
|
||||
</WootButton>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tableData() {
|
||||
if (this.isLoading) {
|
||||
return [];
|
||||
}
|
||||
return this.campaigns.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
url: item.trigger_rules.url,
|
||||
timeOnPage: item.trigger_rules.time_on_page,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.campaigns-table-wrap::v-deep {
|
||||
.ve-table {
|
||||
padding-bottom: var(--space-large);
|
||||
thead.ve-table-header .ve-table-header-tr .ve-table-header-th {
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--space-small) var(--space-two);
|
||||
}
|
||||
tbody.ve-table-body .ve-table-body-tr .ve-table-body-td {
|
||||
padding: var(--space-slab) var(--space-two);
|
||||
}
|
||||
}
|
||||
|
||||
.row--title-block {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-small);
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
padding: var(--space-smaller) var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.campaign--loader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-default);
|
||||
justify-content: center;
|
||||
padding: var(--space-big);
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
justify-content: space-evenly;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 20rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,246 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header :header-title="pageTitle" />
|
||||
<form class="row" @submit.prevent="editCampaign">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.title.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.TITLE.LABEL') }}
|
||||
<input
|
||||
v-model.trim="title"
|
||||
type="text"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TITLE.PLACEHOLDER')"
|
||||
@input="$v.title.$touch"
|
||||
/>
|
||||
<span v-if="$v.title.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.TITLE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.message.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="message"
|
||||
rows="5"
|
||||
type="text"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.MESSAGE.PLACEHOLDER')"
|
||||
@input="$v.message.$touch"
|
||||
/>
|
||||
<span v-if="$v.message.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.selectedSender.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }}
|
||||
<select v-model="selectedSender">
|
||||
<option
|
||||
v-for="sender in sendersAndBotList"
|
||||
:key="sender.name"
|
||||
:value="sender.id"
|
||||
>
|
||||
{{ sender.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="$v.selectedSender.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.endPoint.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.END_POINT.LABEL') }}
|
||||
<input
|
||||
v-model.trim="endPoint"
|
||||
type="text"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.END_POINT.PLACEHOLDER')"
|
||||
@input="$v.endPoint.$touch"
|
||||
/>
|
||||
<span v-if="$v.endPoint.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.timeOnPage.$error }">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.LABEL') }}
|
||||
<input
|
||||
v-model.trim="timeOnPage"
|
||||
type="number"
|
||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.PLACEHOLDER')"
|
||||
@input="$v.timeOnPage.$touch"
|
||||
/>
|
||||
<span v-if="$v.timeOnPage.$error" class="message">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<label>
|
||||
<input
|
||||
v-model="enabled"
|
||||
type="checkbox"
|
||||
value="enabled"
|
||||
name="enabled"
|
||||
/>
|
||||
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<woot-button :disabled="buttonDisabled" :loading="uiFlags.isCreating">
|
||||
{{ $t('CAMPAIGN.EDIT.UPDATE_BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="button clear"
|
||||
:disabled="buttonDisabled"
|
||||
:loading="uiFlags.isCreating"
|
||||
@click.prevent="onClose"
|
||||
>
|
||||
{{ $t('CAMPAIGN.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
||||
import Modal from 'dashboard/components/Modal';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
selectedCampaign: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
senderList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
message: '',
|
||||
selectedSender: '',
|
||||
endPoint: '',
|
||||
timeOnPage: 10,
|
||||
show: true,
|
||||
enabled: true,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
title: {
|
||||
required,
|
||||
},
|
||||
message: {
|
||||
required,
|
||||
},
|
||||
selectedSender: {
|
||||
required,
|
||||
},
|
||||
endPoint: {
|
||||
required,
|
||||
minLength: minLength(7),
|
||||
url,
|
||||
},
|
||||
timeOnPage: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'campaigns/getUIFlags',
|
||||
}),
|
||||
buttonDisabled() {
|
||||
return (
|
||||
this.$v.message.$invalid ||
|
||||
this.$v.title.$invalid ||
|
||||
this.$v.selectedSender.$invalid ||
|
||||
this.$v.endPoint.$invalid ||
|
||||
this.$v.timeOnPage.$invalid ||
|
||||
this.uiFlags.isCreating
|
||||
);
|
||||
},
|
||||
pageTitle() {
|
||||
return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${
|
||||
this.selectedCampaign.title
|
||||
}`;
|
||||
},
|
||||
sendersAndBotList() {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Bot',
|
||||
},
|
||||
...this.senderList,
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFormValues();
|
||||
},
|
||||
methods: {
|
||||
setFormValues() {
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
enabled,
|
||||
trigger_rules: { url: endPoint, time_on_page: timeOnPage },
|
||||
sender,
|
||||
} = this.selectedCampaign;
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.endPoint = endPoint;
|
||||
this.timeOnPage = timeOnPage;
|
||||
this.selectedSender = (sender && sender.id) || 0;
|
||||
this.enabled = enabled;
|
||||
},
|
||||
|
||||
async editCampaign() {
|
||||
try {
|
||||
await this.$store.dispatch('campaigns/update', {
|
||||
id: this.selectedCampaign.id,
|
||||
title: this.title,
|
||||
message: this.message,
|
||||
inbox_id: this.$route.params.inboxId,
|
||||
sender_id: this.selectedSender || null,
|
||||
enabled: this.enabled,
|
||||
trigger_rules: {
|
||||
url: this.endPoint,
|
||||
time_on_page: this.timeOnPage,
|
||||
},
|
||||
});
|
||||
this.showAlert(this.$t('CAMPAIGN.EDIT.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('CAMPAIGN.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content-box .page-top-bar::v-deep {
|
||||
padding: var(--space-large) var(--space-large) var(--space-zero);
|
||||
}
|
||||
</style>
|
|
@ -37,16 +37,14 @@
|
|||
</label>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:disabled="$v.title.$invalid || uiFlags.isCreating"
|
||||
:button-text="$t('LABEL_MGMT.FORM.CREATE')"
|
||||
:loading="uiFlags.isCreating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('LABEL_MGMT.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
<woot-submit-button
|
||||
:disabled="$v.title.$invalid || uiFlags.isCreating"
|
||||
:button-text="$t('LABEL_MGMT.FORM.CREATE')"
|
||||
:loading="uiFlags.isCreating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('LABEL_MGMT.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -26,6 +26,7 @@ import userNotificationSettings from './modules/userNotificationSettings';
|
|||
import webhooks from './modules/webhooks';
|
||||
import teams from './modules/teams';
|
||||
import teamMembers from './modules/teamMembers';
|
||||
import campaigns from './modules/campaigns';
|
||||
|
||||
Vue.use(Vuex);
|
||||
export default new Vuex.Store({
|
||||
|
@ -55,5 +56,6 @@ export default new Vuex.Store({
|
|||
webhooks,
|
||||
teams,
|
||||
teamMembers,
|
||||
campaigns,
|
||||
},
|
||||
});
|
||||
|
|
77
app/javascript/dashboard/store/modules/campaigns.js
Normal file
77
app/javascript/dashboard/store/modules/campaigns.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import types from '../mutation-types';
|
||||
import CampaignsAPI from '../../api/campaigns';
|
||||
|
||||
export const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
isCreating: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getUIFlags(_state) {
|
||||
return _state.uiFlags;
|
||||
},
|
||||
getCampaigns(_state) {
|
||||
return _state.records;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
get: async function getCampaigns({ commit }) {
|
||||
commit(types.SET_CAMPAIGN_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const response = await CampaignsAPI.get();
|
||||
commit(types.SET_CAMPAIGNS, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
} finally {
|
||||
commit(types.SET_CAMPAIGN_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
create: async function createCampaign({ commit }, campaignObj) {
|
||||
commit(types.SET_CAMPAIGN_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await CampaignsAPI.create(campaignObj);
|
||||
commit(types.ADD_CAMPAIGN, response.data);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
commit(types.SET_CAMPAIGN_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
},
|
||||
update: async ({ commit }, { id, ...updateObj }) => {
|
||||
commit(types.SET_CAMPAIGN_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await CampaignsAPI.update(id, updateObj);
|
||||
commit(types.EDIT_CAMPAIGN, response.data);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
commit(types.SET_CAMPAIGN_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[types.SET_CAMPAIGN_UI_FLAG](_state, data) {
|
||||
_state.uiFlags = {
|
||||
..._state.uiFlags,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
[types.ADD_CAMPAIGN]: MutationHelpers.create,
|
||||
[types.SET_CAMPAIGNS]: MutationHelpers.set,
|
||||
[types.EDIT_CAMPAIGN]: MutationHelpers.update,
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
actions,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
};
|
|
@ -6,12 +6,12 @@ import types from '../../mutation-types';
|
|||
import ContactAPI from '../../../api/contacts';
|
||||
|
||||
export const actions = {
|
||||
search: async ({ commit }, { search, page }) => {
|
||||
search: async ({ commit }, { search, page, sortAttr }) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await ContactAPI.search(search, page);
|
||||
} = await ContactAPI.search(search, page, sortAttr);
|
||||
commit(types.CLEAR_CONTACTS);
|
||||
commit(types.SET_CONTACTS, payload);
|
||||
commit(types.SET_CONTACT_META, meta);
|
||||
|
@ -21,12 +21,12 @@ export const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
get: async ({ commit }, { page = 1 } = {}) => {
|
||||
get: async ({ commit }, { page = 1, sortAttr } = {}) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await ContactAPI.get(page);
|
||||
} = await ContactAPI.get(page, sortAttr);
|
||||
commit(types.CLEAR_CONTACTS);
|
||||
commit(types.SET_CONTACTS, payload);
|
||||
commit(types.SET_CONTACT_META, meta);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export const getters = {
|
||||
getContacts($state) {
|
||||
return Object.values($state.records);
|
||||
return $state.sortOrder.map(contactId => $state.records[contactId]);
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
|
|
|
@ -14,6 +14,7 @@ const state = {
|
|||
isFetchingInboxes: false,
|
||||
isUpdating: false,
|
||||
},
|
||||
sortOrder: [],
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -11,6 +11,7 @@ export const mutations = {
|
|||
|
||||
[types.CLEAR_CONTACTS]: $state => {
|
||||
Vue.set($state, 'records', {});
|
||||
Vue.set($state, 'sortOrder', []);
|
||||
},
|
||||
|
||||
[types.SET_CONTACT_META]: ($state, data) => {
|
||||
|
@ -20,12 +21,14 @@ export const mutations = {
|
|||
},
|
||||
|
||||
[types.SET_CONTACTS]: ($state, data) => {
|
||||
data.forEach(contact => {
|
||||
const sortOrder = data.map(contact => {
|
||||
Vue.set($state.records, contact.id, {
|
||||
...($state.records[contact.id] || {}),
|
||||
...contact,
|
||||
});
|
||||
return contact.id;
|
||||
});
|
||||
$state.sortOrder = sortOrder;
|
||||
},
|
||||
|
||||
[types.SET_CONTACT_ITEM]: ($state, data) => {
|
||||
|
@ -33,6 +36,10 @@ export const mutations = {
|
|||
...($state.records[data.id] || {}),
|
||||
...data,
|
||||
});
|
||||
|
||||
if (!$state.sortOrder.includes(data.id)) {
|
||||
$state.sortOrder.push(data.id);
|
||||
}
|
||||
},
|
||||
|
||||
[types.EDIT_CONTACT]: ($state, data) => {
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import axios from 'axios';
|
||||
import { actions } from '../../campaigns';
|
||||
import * as types from '../../../mutation-types';
|
||||
import campaignList from './fixtures';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#get', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: campaignList });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_CAMPAIGNS, campaignList],
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('#create', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: campaignList[0] });
|
||||
await actions.create({ commit }, campaignList[0]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: true }],
|
||||
[types.default.ADD_CAMPAIGN, campaignList[0]],
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.create({ commit })).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: true }],
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: campaignList[0] });
|
||||
await actions.update({ commit }, campaignList[0]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.EDIT_CAMPAIGN, campaignList[0]],
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.update({ commit }, campaignList[0])).rejects.toThrow(
|
||||
Error
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_CAMPAIGN_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,348 @@
|
|||
export default [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Welcome',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
greeting_enabled: true,
|
||||
greeting_message: '',
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message:
|
||||
'We are unavailable at the moment. Leave a message we will respond once we are back.',
|
||||
working_hours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
},
|
||||
],
|
||||
timezone: 'Asia/Kolkata',
|
||||
avatar_url: '',
|
||||
page_id: null,
|
||||
widget_color: '#1F93FF',
|
||||
website_url: 'chatwoot.com',
|
||||
welcome_title: 'Hi there ! 🙌🏼',
|
||||
welcome_tagline:
|
||||
'We make it simple to connect with us. Ask us anything, or share your feedback.',
|
||||
enable_auto_assignment: true,
|
||||
website_token: '',
|
||||
forward_to_email: null,
|
||||
phone_number: null,
|
||||
selected_feature_flags: ['attachments', 'emoji_picker'],
|
||||
reply_time: 'in_a_few_hours',
|
||||
hmac_token: '',
|
||||
pre_chat_form_enabled: true,
|
||||
pre_chat_form_options: {
|
||||
require_email: true,
|
||||
pre_chat_message: 'Share your queries or comments here.',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
name: 'Sojan',
|
||||
role: 'administrator',
|
||||
thumbnail:
|
||||
'http://0.0.0.0:3000/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--bfa5e8a4563aef73980771fc9b8007d380e586e5/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lNTWpVd2VESTFNQVk2QmtWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--c13bd5229b2a2a692444606e22f76ad61c634661/73185.jpeg',
|
||||
},
|
||||
message: 'Hey, What brings you today',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://github.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T04:53:36.354Z',
|
||||
updated_at: '2021-05-03T04:53:36.354Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Onboarding Campaign',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
greeting_enabled: true,
|
||||
greeting_message: '',
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message:
|
||||
'We are unavailable at the moment. Leave a message we will respond once we are back.',
|
||||
working_hours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
},
|
||||
],
|
||||
timezone: 'Asia/Kolkata',
|
||||
avatar_url: '',
|
||||
page_id: null,
|
||||
widget_color: '#1F93FF',
|
||||
website_url: 'chatwoot.com',
|
||||
welcome_title: 'Hi there ! 🙌🏼',
|
||||
welcome_tagline:
|
||||
'We make it simple to connect with us. Ask us anything, or share your feedback.',
|
||||
enable_auto_assignment: true,
|
||||
web_widget_script: '',
|
||||
website_token: '',
|
||||
forward_to_email: null,
|
||||
phone_number: null,
|
||||
selected_feature_flags: ['attachments', 'emoji_picker'],
|
||||
reply_time: 'in_a_few_hours',
|
||||
hmac_token: '',
|
||||
pre_chat_form_enabled: true,
|
||||
pre_chat_form_options: {
|
||||
require_email: true,
|
||||
pre_chat_message: 'Share your queries or comments here.',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
name: 'Sojan',
|
||||
role: 'administrator',
|
||||
thumbnail:
|
||||
'http://0.0.0.0:3000/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--bfa5e8a4563aef73980771fc9b8007d380e586e5/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lNTWpVd2VESTFNQVk2QmtWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--c13bd5229b2a2a692444606e22f76ad61c634661/73185.jpeg',
|
||||
},
|
||||
message: 'Begin your onboarding campaign with a welcome message',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://chatwoot.com',
|
||||
time_on_page: '20',
|
||||
},
|
||||
created_at: '2021-05-03T08:15:35.828Z',
|
||||
updated_at: '2021-05-03T08:15:35.828Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Thanks',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
greeting_enabled: true,
|
||||
greeting_message: '',
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message:
|
||||
'We are unavailable at the moment. Leave a message we will respond once we are back.',
|
||||
working_hours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
closed_all_day: false,
|
||||
open_hour: 11,
|
||||
open_minutes: 0,
|
||||
close_hour: 23,
|
||||
close_minutes: 30,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
},
|
||||
],
|
||||
timezone: 'Asia/Kolkata',
|
||||
avatar_url: '',
|
||||
page_id: null,
|
||||
widget_color: '#1F93FF',
|
||||
website_url: 'chatwoot.com',
|
||||
welcome_title: 'Hi there ! 🙌🏼',
|
||||
welcome_tagline:
|
||||
'We make it simple to connect with us. Ask us anything, or share your feedback.',
|
||||
enable_auto_assignment: true,
|
||||
web_widget_script: '',
|
||||
website_token: '',
|
||||
forward_to_email: null,
|
||||
phone_number: null,
|
||||
selected_feature_flags: ['attachments', 'emoji_picker'],
|
||||
reply_time: 'in_a_few_hours',
|
||||
hmac_token: '',
|
||||
pre_chat_form_enabled: true,
|
||||
pre_chat_form_options: {
|
||||
require_email: true,
|
||||
pre_chat_message: 'Share your queries or comments here.',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'nithin@chatwoot.com',
|
||||
available_name: 'Nithin',
|
||||
id: 13,
|
||||
name: 'Nithin',
|
||||
role: 'administrator',
|
||||
thumbnail: '',
|
||||
},
|
||||
message: 'Thanks for coming to the show. How may I help you?',
|
||||
enabled: false,
|
||||
trigger_rules: {
|
||||
url: 'https://noshow.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T10:22:51.025Z',
|
||||
updated_at: '2021-05-03T10:22:51.025Z',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,22 @@
|
|||
import { getters } from '../../campaigns';
|
||||
import campaigns from './fixtures';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getCampaigns', () => {
|
||||
const state = { records: campaigns };
|
||||
expect(getters.getCampaigns(state)).toEqual(campaigns);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isFetching: true,
|
||||
isCreating: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
isFetching: true,
|
||||
isCreating: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import types from '../../../mutation-types';
|
||||
import { mutations } from '../../campaigns';
|
||||
import campaigns from './fixtures';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_CAMPAIGNS', () => {
|
||||
it('set campaigns records', () => {
|
||||
const state = { records: [] };
|
||||
mutations[types.SET_CAMPAIGNS](state, campaigns);
|
||||
expect(state.records).toEqual(campaigns);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_CAMPAIGN', () => {
|
||||
it('push newly created campaigns to the store', () => {
|
||||
const state = { records: [campaigns[0]] };
|
||||
mutations[types.ADD_CAMPAIGN](state, campaigns[1]);
|
||||
expect(state.records).toEqual([campaigns[0], campaigns[1]]);
|
||||
});
|
||||
});
|
||||
describe('#EDIT_CAMPAIGN', () => {
|
||||
it('update campaign record', () => {
|
||||
const state = { records: [campaigns[0]] };
|
||||
mutations[types.EDIT_CAMPAIGN](state, {
|
||||
id: 1,
|
||||
title: 'Welcome',
|
||||
account_id: 1,
|
||||
message: 'Hey, What brings you today',
|
||||
});
|
||||
expect(state.records[0].message).toEqual('Hey, What brings you today');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,9 +6,13 @@ const { getters } = Contacts;
|
|||
describe('#getters', () => {
|
||||
it('getContacts', () => {
|
||||
const state = {
|
||||
records: { 1: contactList[0] },
|
||||
records: { 1: contactList[0], 3: contactList[2] },
|
||||
sortOrder: [3, 1],
|
||||
};
|
||||
expect(getters.getContacts(state)).toEqual([contactList[0]]);
|
||||
expect(getters.getContacts(state)).toEqual([
|
||||
contactList[2],
|
||||
contactList[0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('getContact', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@ describe('#mutations', () => {
|
|||
it('set contact records', () => {
|
||||
const state = { records: {} };
|
||||
mutations[types.SET_CONTACTS](state, [
|
||||
{ id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
]);
|
||||
expect(state.records).toEqual({
|
||||
|
@ -15,7 +16,13 @@ describe('#mutations', () => {
|
|||
name: 'contact1',
|
||||
email: 'contact1@chatwoot.com',
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
name: 'contact2',
|
||||
email: 'contact2@chatwoot.com',
|
||||
},
|
||||
});
|
||||
expect(state.sortOrder).toEqual([2, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -25,6 +32,7 @@ describe('#mutations', () => {
|
|||
records: {
|
||||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
},
|
||||
sortOrder: [1],
|
||||
};
|
||||
mutations[types.SET_CONTACT_ITEM](state, {
|
||||
id: 2,
|
||||
|
@ -35,6 +43,7 @@ describe('#mutations', () => {
|
|||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
2: { id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||
});
|
||||
expect(state.sortOrder).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -145,4 +145,10 @@ export default {
|
|||
// Conversation Search
|
||||
SEARCH_CONVERSATIONS_SET: 'SEARCH_CONVERSATIONS_SET',
|
||||
SEARCH_CONVERSATIONS_SET_UI_FLAG: 'SEARCH_CONVERSATIONS_SET_UI_FLAG',
|
||||
|
||||
// Campaigns
|
||||
SET_CAMPAIGN_UI_FLAG: 'SET_CAMPAIGN_UI_FLAG',
|
||||
SET_CAMPAIGNS: 'SET_CAMPAIGNS',
|
||||
ADD_CAMPAIGN: 'ADD_CAMPAIGN',
|
||||
EDIT_CAMPAIGN: 'EDIT_CAMPAIGN',
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
|||
computed: {
|
||||
...mapGetters({ uiSettings: 'getUISettings' }),
|
||||
isIconTypeEmoji() {
|
||||
const { icon_type: iconType } = this.uiSettings;
|
||||
const { icon_type: iconType } = this.uiSettings || {};
|
||||
return iconType === 'emoji';
|
||||
},
|
||||
showEmoji() {
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('DateSeparator', () => {
|
|||
$t: () => {},
|
||||
},
|
||||
});
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
expect(wrapper.vm).toBeTruthy();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import Spinner from '../Spinner';
|
|||
describe('Spinner', () => {
|
||||
test('matches snapshot', () => {
|
||||
const wrapper = mount(Spinner);
|
||||
expect(wrapper.isVueInstance()).toBeTruthy();
|
||||
expect(wrapper.vm).toBeTruthy();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import AddLabel from './AddLabel';
|
||||
|
||||
export default {
|
||||
title: 'Components/Label/Add Button',
|
||||
component: AddLabel,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AddLabel },
|
||||
template: '<add-label v-bind="$props" @add="onClick"></add-label>',
|
||||
});
|
||||
|
||||
export const AddButton = Template.bind({});
|
||||
AddButton.args = {
|
||||
onClick: action('opened'),
|
||||
};
|
25
app/javascript/shared/components/ui/dropdown/AddLabel.vue
Normal file
25
app/javascript/shared/components/ui/dropdown/AddLabel.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
icon="ion-plus-round"
|
||||
color-scheme="secondary"
|
||||
@click="toggleLabels"
|
||||
>
|
||||
{{ $t('CONTACT_PANEL.LABELS.MODAL.ADD_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
toggleLabels() {
|
||||
this.$emit('add');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -27,19 +27,21 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-menu__item ::v-deep {
|
||||
a,
|
||||
.button {
|
||||
font-weight: var(--font-size-normal);
|
||||
font-size: var(--font-size-small);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
padding: var(--space-small) var(--space-one);
|
||||
.dropdown-menu__item {
|
||||
list-style: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--border-radius-normal);
|
||||
::v-deep {
|
||||
a,
|
||||
.button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
padding: var(--space-small) var(--space-one);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background);
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import LabelDropdown from './LabelDropdown';
|
||||
|
||||
export default {
|
||||
title: 'Components/Label/Dropdown',
|
||||
component: LabelDropdown,
|
||||
argTypes: {
|
||||
conversationId: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
accountLabels: {
|
||||
defaultValue: [
|
||||
{
|
||||
color: '#555',
|
||||
description: '',
|
||||
id: 1,
|
||||
title: 'sales',
|
||||
},
|
||||
{
|
||||
color: '#c242f5',
|
||||
description: '',
|
||||
id: 1,
|
||||
title: 'business',
|
||||
},
|
||||
{
|
||||
color: '#4287f5',
|
||||
description: '',
|
||||
id: 1,
|
||||
title: 'testing',
|
||||
},
|
||||
],
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
selectedLabels: {
|
||||
defaultValue: 'sales, testing',
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { LabelDropdown },
|
||||
template:
|
||||
'<label-dropdown v-bind="$props" @add="onAdd" @remove="onRemove"></label-dropdown>',
|
||||
});
|
||||
|
||||
export const Dropdown = Template.bind({});
|
||||
Dropdown.args = {
|
||||
onAdd: action('added'),
|
||||
onRemove: action('removed'),
|
||||
};
|
156
app/javascript/shared/components/ui/label/LabelDropdown.vue
Normal file
156
app/javascript/shared/components/ui/label/LabelDropdown.vue
Normal file
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="dropdown-search-wrap">
|
||||
<h4 class="text-block-title">
|
||||
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.TITLE') }}
|
||||
</h4>
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
ref="searchbar"
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="search-input"
|
||||
autofocus="true"
|
||||
:placeholder="$t('CONTACT_PANEL.LABELS.LABEL_SELECT.PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
<div class="list-wrap">
|
||||
<div class="list">
|
||||
<woot-dropdown-menu>
|
||||
<label-dropdown-item
|
||||
v-for="label in filteredActiveLabels"
|
||||
:key="label.title"
|
||||
:title="label.title"
|
||||
:color="label.color"
|
||||
:selected="selectedLabels.includes(label.title)"
|
||||
@click="onAddRemove(label)"
|
||||
/>
|
||||
</woot-dropdown-menu>
|
||||
<div v-if="noResult" class="no-result">
|
||||
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.NO_RESULT') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LabelDropdownItem from './LabelDropdownItem';
|
||||
export default {
|
||||
components: {
|
||||
LabelDropdownItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
accountLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredActiveLabels() {
|
||||
return this.accountLabels.filter(label => {
|
||||
return label.title.toLowerCase().includes(this.search.toLowerCase());
|
||||
});
|
||||
},
|
||||
|
||||
noResult() {
|
||||
return this.filteredActiveLabels.length === 0 && this.search !== '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.searchbar.focus();
|
||||
},
|
||||
|
||||
updateLabels(label) {
|
||||
this.$emit('update', label);
|
||||
},
|
||||
|
||||
onAdd(label) {
|
||||
this.$emit('add', label);
|
||||
},
|
||||
|
||||
onRemove(label) {
|
||||
this.$emit('remove', label);
|
||||
},
|
||||
|
||||
onAddRemove(label) {
|
||||
if (this.selectedLabels.includes(label.title)) {
|
||||
this.onRemove(label.title);
|
||||
} else {
|
||||
this.onAdd(label);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-search-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 20rem;
|
||||
|
||||
.search-wrap {
|
||||
margin-bottom: var(--space-small);
|
||||
flex: 0 0 auto;
|
||||
max-height: var(--space-large);
|
||||
|
||||
.search-input {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
height: var(--space-large);
|
||||
font-size: var(--font-size-small);
|
||||
padding: var(--space-small);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border: 1px solid var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-result {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--s-700);
|
||||
padding: var(--space-smaller) var(--space-one);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import LabelDropdownItem from './LabelDropdownItem';
|
||||
|
||||
export default {
|
||||
title: 'Components/Label/Item',
|
||||
component: LabelDropdownItem,
|
||||
argTypes: {
|
||||
title: {
|
||||
defaultValue: 'sales',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
color: {
|
||||
defaultValue: '#555',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
defaultValue: true,
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { LabelDropdownItem },
|
||||
template:
|
||||
'<label-dropdown-item v-bind="$props" @click="onClick"></label-dropdown-item>',
|
||||
});
|
||||
|
||||
export const item = Template.bind({});
|
||||
item.args = {
|
||||
onClick: action('Selected'),
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<woot-dropdown-item>
|
||||
<div class="item-wrap">
|
||||
<woot-button variant="clear" @click="onClick">
|
||||
<div class="button-wrap">
|
||||
<div class="name-label-wrap">
|
||||
<div
|
||||
v-if="color"
|
||||
class="label-color--display"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<i v-if="selected" class="icon ion-checkmark-round" />
|
||||
</div>
|
||||
</woot-button>
|
||||
</div>
|
||||
</woot-dropdown-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('click', this.title);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-wrap {
|
||||
display: flex;
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--w-700);
|
||||
}
|
||||
|
||||
.name-label-wrap {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.label-color--display {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||
|
||||
.label-color--display {
|
||||
border-radius: var(--border-radius-normal);
|
||||
height: var(--space-slab);
|
||||
margin-right: var(--space-smaller);
|
||||
margin-top: var(--space-micro);
|
||||
min-width: var(--space-slab);
|
||||
width: var(--space-slab);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -6,5 +6,6 @@ export const MESSAGE_MAX_LENGTH = {
|
|||
GENERAL: 10000,
|
||||
FACEBOOK: 640,
|
||||
TWILIO_SMS: 160,
|
||||
TWILIO_WHATSAPP: 1600,
|
||||
TWEET: 280,
|
||||
};
|
||||
|
|
|
@ -73,6 +73,7 @@ export default {
|
|||
methods: {
|
||||
...mapActions('appConfig', ['setWidgetColor']),
|
||||
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
|
||||
...mapActions('campaign', ['fetchCampaigns']),
|
||||
...mapActions('agent', ['fetchAvailableAgents']),
|
||||
scrollConversationToBottom() {
|
||||
const container = this.$el.querySelector('.conversation-wrap');
|
||||
|
@ -149,6 +150,7 @@ export default {
|
|||
this.fetchOldConversations().then(() => this.setUnreadView());
|
||||
this.setPopoutDisplay(message.showPopoutButton);
|
||||
this.fetchAvailableAgents(websiteToken);
|
||||
this.fetchCampaigns(websiteToken);
|
||||
this.setHideMessageBubble(message.hideMessageBubble);
|
||||
this.$store.dispatch('contacts/get');
|
||||
} else if (message.event === 'widget-visible') {
|
||||
|
|
23
app/javascript/widget/api/campaign.js
Normal file
23
app/javascript/widget/api/campaign.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import endPoints from 'widget/api/endPoints';
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
const getCampaigns = async websiteToken => {
|
||||
const urlData = endPoints.getCampaigns(websiteToken);
|
||||
const result = await API.get(urlData.url, { params: urlData.params });
|
||||
return result;
|
||||
};
|
||||
|
||||
const triggerCampaign = async ({ campaignId }) => {
|
||||
const { websiteToken } = window.chatwootWebChannel;
|
||||
const urlData = endPoints.triggerCampaign(websiteToken, campaignId);
|
||||
|
||||
await API.post(
|
||||
urlData.url,
|
||||
{ ...urlData.data },
|
||||
{
|
||||
params: urlData.params,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export { getCampaigns, triggerCampaign };
|
|
@ -64,6 +64,24 @@ const getAvailableAgents = token => ({
|
|||
website_token: token,
|
||||
},
|
||||
});
|
||||
const getCampaigns = token => ({
|
||||
url: '/api/v1/widget/campaigns',
|
||||
params: {
|
||||
website_token: token,
|
||||
},
|
||||
});
|
||||
const triggerCampaign = (token, campaignId) => ({
|
||||
url: '/api/v1/widget/events',
|
||||
data: {
|
||||
name: 'campaign.triggered',
|
||||
event_info: {
|
||||
campaign_id: campaignId,
|
||||
},
|
||||
},
|
||||
params: {
|
||||
website_token: token,
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
createConversation,
|
||||
|
@ -72,4 +90,6 @@ export default {
|
|||
getConversation,
|
||||
updateMessage,
|
||||
getAvailableAgents,
|
||||
getCampaigns,
|
||||
triggerCampaign,
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:href="brandRedirectURL"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
class="branding--link"
|
||||
class="branding--link w-full justify-center"
|
||||
>
|
||||
<img :alt="globalConfig.brandName" :src="globalConfig.logoThumbnail" />
|
||||
<span>
|
||||
|
|
14
app/javascript/widget/helpers/campaignTimer.js
Normal file
14
app/javascript/widget/helpers/campaignTimer.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { triggerCampaign } from 'widget/api/campaign';
|
||||
const startTimer = async ({ allCampaigns }) => {
|
||||
allCampaigns.forEach(campaign => {
|
||||
const {
|
||||
trigger_rules: { time_on_page: timeOnPage },
|
||||
id: campaignId,
|
||||
} = campaign;
|
||||
setTimeout(async () => {
|
||||
await triggerCampaign({ campaignId });
|
||||
}, timeOnPage * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
export { startTimer };
|
|
@ -9,9 +9,9 @@ import conversationLabels from 'widget/store/modules/conversationLabels';
|
|||
import events from 'widget/store/modules/events';
|
||||
import globalConfig from 'shared/store/globalConfig';
|
||||
import message from 'widget/store/modules/message';
|
||||
import campaign from 'widget/store/modules/campaign';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
agent,
|
||||
|
@ -23,5 +23,6 @@ export default new Vuex.Store({
|
|||
events,
|
||||
globalConfig,
|
||||
message,
|
||||
campaign,
|
||||
},
|
||||
});
|
||||
|
|
51
app/javascript/widget/store/modules/campaign.js
Normal file
51
app/javascript/widget/store/modules/campaign.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import Vue from 'vue';
|
||||
import { getCampaigns } from 'widget/api/campaign';
|
||||
import { startTimer } from 'widget/helpers/campaignTimer';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isError: false,
|
||||
hasFetched: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getHasFetched: $state => $state.uiFlags.hasFetched,
|
||||
fetchCampaigns: $state => $state.records,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
fetchCampaigns: async ({ commit }, websiteToken) => {
|
||||
try {
|
||||
const { data } = await getCampaigns(websiteToken);
|
||||
startTimer({ allCampaigns: data });
|
||||
commit('setCampaigns', data);
|
||||
commit('setError', false);
|
||||
commit('setHasFetched', true);
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
commit('setHasFetched', true);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
setCampaigns($state, data) {
|
||||
Vue.set($state, 'records', data);
|
||||
},
|
||||
setError($state, value) {
|
||||
Vue.set($state.uiFlags, 'isError', value);
|
||||
},
|
||||
setHasFetched($state, value) {
|
||||
Vue.set($state.uiFlags, 'hasFetched', value);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { API } from 'widget/helpers/axios';
|
||||
import { actions } from '../../campaign';
|
||||
import { campaigns } from './data';
|
||||
|
||||
const commit = jest.fn();
|
||||
jest.mock('widget/helpers/axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#fetchCampaigns', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
API.get.mockResolvedValue({ data: campaigns });
|
||||
await actions.fetchCampaigns({ commit }, 'XDsafmADasd');
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setCampaigns', campaigns],
|
||||
['setError', false],
|
||||
['setHasFetched', true],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
API.get.mockRejectedValue({ message: 'Authentication required' });
|
||||
await actions.fetchCampaigns({ commit }, 'XDsafmADasd');
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setError', true],
|
||||
['setHasFetched', true],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
86
app/javascript/widget/store/modules/specs/campaign/data.js
Normal file
86
app/javascript/widget/store/modules/specs/campaign/data.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
export const campaigns = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Welcome',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
name: 'Sojan',
|
||||
},
|
||||
message: 'Hey, What brings you today',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://github.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T04:53:36.354Z',
|
||||
updated_at: '2021-05-03T04:53:36.354Z',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: 'Onboarding Campaign',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'GitX',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
},
|
||||
message: 'Begin your onboarding campaign with a welcome message',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://chatwoot.com',
|
||||
time_on_page: '20',
|
||||
},
|
||||
created_at: '2021-05-03T08:15:35.828Z',
|
||||
updated_at: '2021-05-03T08:15:35.828Z',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
title: 'Thanks',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'nithin@chatwoot.com',
|
||||
available_name: 'Nithin',
|
||||
},
|
||||
message: 'Thanks for coming to the show. How may I help you?',
|
||||
enabled: false,
|
||||
trigger_rules: {
|
||||
url: 'https://noshow.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T10:22:51.025Z',
|
||||
updated_at: '2021-05-03T10:22:51.025Z',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,96 @@
|
|||
import { getters } from '../../campaign';
|
||||
import { campaigns } from './data';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('fetchCampaigns', () => {
|
||||
const state = {
|
||||
records: campaigns,
|
||||
};
|
||||
expect(getters.fetchCampaigns(state)).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Welcome',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
name: 'Sojan',
|
||||
},
|
||||
message: 'Hey, What brings you today',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://github.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T04:53:36.354Z',
|
||||
updated_at: '2021-05-03T04:53:36.354Z',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: 'Onboarding Campaign',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'GitX',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'sojan@chatwoot.com',
|
||||
available_name: 'Sojan',
|
||||
id: 10,
|
||||
},
|
||||
message: 'Begin your onboarding campaign with a welcome message',
|
||||
enabled: true,
|
||||
trigger_rules: {
|
||||
url: 'https://chatwoot.com',
|
||||
time_on_page: '20',
|
||||
},
|
||||
created_at: '2021-05-03T08:15:35.828Z',
|
||||
updated_at: '2021-05-03T08:15:35.828Z',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
title: 'Thanks',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
inbox: {
|
||||
id: 37,
|
||||
channel_id: 1,
|
||||
name: 'Chatwoot',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
},
|
||||
sender: {
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
confirmed: true,
|
||||
email: 'nithin@chatwoot.com',
|
||||
available_name: 'Nithin',
|
||||
},
|
||||
message: 'Thanks for coming to the show. How may I help you?',
|
||||
enabled: false,
|
||||
trigger_rules: {
|
||||
url: 'https://noshow.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
created_at: '2021-05-03T10:22:51.025Z',
|
||||
updated_at: '2021-05-03T10:22:51.025Z',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import { mutations } from '../../campaign';
|
||||
import { campaigns } from './data';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#setCampagins', () => {
|
||||
it('set campaign records', () => {
|
||||
const state = { records: [] };
|
||||
mutations.setCampaigns(state, campaigns);
|
||||
expect(state.records).toEqual(campaigns);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setError', () => {
|
||||
it('set error flag', () => {
|
||||
const state = { records: [], uiFlags: {} };
|
||||
mutations.setError(state, true);
|
||||
expect(state.uiFlags.isError).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setHasFetched', () => {
|
||||
it('set fetched flag', () => {
|
||||
const state = { records: [], uiFlags: {} };
|
||||
mutations.setHasFetched(state, true);
|
||||
expect(state.uiFlags.hasFetched).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +1,29 @@
|
|||
class HookJob < ApplicationJob
|
||||
queue_as :integrations
|
||||
|
||||
def perform(hook, message)
|
||||
return unless hook.slack?
|
||||
|
||||
Integrations::Slack::SendOnSlackService.new(message: message, hook: hook).perform
|
||||
def perform(hook, event_name, event_data = {})
|
||||
case hook.app_id
|
||||
when 'slack'
|
||||
process_slack_integration(hook, event_name, event_data)
|
||||
when 'dialogflow'
|
||||
process_dialogflow_integration(hook, event_name, event_data)
|
||||
end
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_slack_integration(hook, event_name, event_data)
|
||||
return unless ['message.created'].include?(event_name)
|
||||
|
||||
message = event_data[:message]
|
||||
Integrations::Slack::SendOnSlackService.new(message: message, hook: hook).perform
|
||||
end
|
||||
|
||||
def process_dialogflow_integration(hook, event_name, event_data)
|
||||
return unless ['message.created', 'message.updated'].include?(event_name)
|
||||
|
||||
Integrations::Dialogflow::ProcessorService.new(event_name: event_name, hook: hook, event_data: event_data).perform
|
||||
end
|
||||
end
|
||||
|
|
14
app/listeners/campaign_listener.rb
Normal file
14
app/listeners/campaign_listener.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class CampaignListener < BaseListener
|
||||
def campaign_triggered(event)
|
||||
contact_inbox = event.data[:contact_inbox]
|
||||
campaign_display_id = event.data[:event_info][:campaign_id]
|
||||
|
||||
return if campaign_display_id.blank?
|
||||
|
||||
::Campaigns::CampaignConversationBuilder.new(
|
||||
contact_inbox_id: contact_inbox.id,
|
||||
campaign_display_id: campaign_display_id,
|
||||
conversation_additional_attributes: event.data[:event_info].except(:campaign_id)
|
||||
).perform
|
||||
end
|
||||
end
|
|
@ -4,7 +4,16 @@ class HookListener < BaseListener
|
|||
return unless message.reportable?
|
||||
|
||||
message.account.hooks.each do |hook|
|
||||
HookJob.perform_later(hook, message)
|
||||
HookJob.perform_later(hook, event.name, message: message)
|
||||
end
|
||||
end
|
||||
|
||||
def message_updated(event)
|
||||
message = extract_message_and_account(event)[0]
|
||||
return unless message.reportable?
|
||||
|
||||
message.account.hooks.each do |hook|
|
||||
HookJob.perform_later(hook, event.name, message: message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -59,7 +59,9 @@ class WebhookListener < BaseListener
|
|||
WebhookJob.perform_later(webhook.url, payload)
|
||||
end
|
||||
|
||||
# Deliver for API Inbox
|
||||
WebhookJob.perform_later(inbox.channel.webhook_url, payload) if inbox.channel_type == 'Channel::Api'
|
||||
return unless inbox.channel_type == 'Channel::Api'
|
||||
return if inbox.channel.webhook_url.blank?
|
||||
|
||||
WebhookJob.perform_later(inbox.channel.webhook_url, payload)
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue