Merge branch 'release/1.2.0'

This commit is contained in:
Sojan 2020-03-01 19:33:41 +05:30
commit c772c0af03
332 changed files with 11142 additions and 3705 deletions

View file

@ -12,8 +12,8 @@ defaults: &defaults
# Specify service dependencies here if necessary # Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images # CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/ # documented at https://circleci.com/docs/2.0/circleci-images/
- image: circleci/postgres:9.4 - image: circleci/postgres:alpine
- image: circleci/redis:5.0.7-alpine - image: circleci/redis:alpine
environment: environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f - CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f

View file

@ -1,4 +1,12 @@
SECRET_KEY_BASE= SECRET_KEY_BASE=
# Force all access to the app over SSL, default is set to false
FORCE_SSL=
# This lets you control new sign ups on your chatwoot installation
# true : default option, allows sign ups
# false : disables all the end points related to sign ups
# api_only: disables the UI for signup, but you can create sign ups via the account apis
ENABLE_ACCOUNT_SIGNUP=
#redis config #redis config
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379
@ -20,8 +28,10 @@ FB_APP_SECRET=
FB_APP_ID= FB_APP_ID=
#twitter app #twitter app
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY= TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET= TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#mail #mail
MAILER_SENDER_EMAIL=accounts@chatwoot.com MAILER_SENDER_EMAIL=accounts@chatwoot.com

View file

@ -24,10 +24,12 @@ module.exports = {
'multiline': { 'multiline': {
'max': 1, 'max': 1,
'allowFirstLine': false 'allowFirstLine': false
} },
}], }],
'vue/html-self-closing': 'off', 'vue/html-self-closing': 'off',
"vue/no-v-html": 'off' "vue/no-v-html": 'off',
'import/extensions': ['never']
}, },
settings: { settings: {
'import/resolver': { 'import/resolver': {

4
.gitignore vendored
View file

@ -37,6 +37,10 @@ public/packs*
*.swo *.swo
*.un~ *.un~
.jest-cache .jest-cache
#VS Code files
.vscode
# ignore jetbrains IDE files # ignore jetbrains IDE files
.idea .idea

View file

@ -20,14 +20,12 @@ Style/GlobalVars:
Exclude: Exclude:
- 'config/initializers/redis.rb' - 'config/initializers/redis.rb'
- 'lib/redis/alfred.rb' - 'lib/redis/alfred.rb'
- 'app/controllers/api/v1/webhooks_controller.rb'
- 'app/services/twitter/send_reply_service.rb'
- 'spec/services/twitter/send_reply_service_spec.rb'
Metrics/BlockLength: Metrics/BlockLength:
Exclude: Exclude:
- spec/**/* - spec/**/*
- '**/routes.rb' - '**/routes.rb'
- 'config/environments/*' - 'config/environments/*'
- db/schema.rb
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb' - 'app/controllers/api/v1/widget/messages_controller.rb'
@ -41,6 +39,8 @@ Style/ClassAndModuleChildren:
RSpec/NestedGroups: RSpec/NestedGroups:
Enabled: true Enabled: true
Max: 4 Max: 4
RSpec/MessageSpies:
Enabled: false
AllCops: AllCops:
Exclude: Exclude:
- db/* - db/*

View file

@ -1,7 +1,281 @@
# Default application configuration that all configurations inherit from.
scss_files: '**/*.scss'
plugin_directories: ['.scss-linters']
# List of gem names to load custom linters from (make sure they are already
# installed)
plugin_gems: []
# Default severity of all linters.
severity: warning
linters: linters:
BangFormat:
enabled: true
space_before_bang: true
space_after_bang: false
BemDepth:
enabled: false
max_elements: 1
BorderZero:
enabled: true
convention: zero # or `none`
ChainedClasses:
enabled: false
ColorKeyword:
enabled: true
ColorVariable:
enabled: true
Comment:
enabled: true
style: silent
DebugStatement:
enabled: true
DeclarationOrder:
enabled: true
DisableLinterReason:
enabled: false
DuplicateProperty:
enabled: true
ElsePlacement:
enabled: true
style: same_line # or 'new_line'
EmptyLineBetweenBlocks:
enabled: true
ignore_single_line_blocks: true
EmptyRule:
enabled: true
ExtendDirective:
enabled: false
FinalNewline:
enabled: true
present: true
HexLength:
enabled: true
style: short # or 'long'
HexNotation:
enabled: true
style: lowercase # or 'uppercase'
HexValidation:
enabled: true
IdSelector:
enabled: true
ImportantRule:
enabled: true
ImportPath:
enabled: true
leading_underscore: false
filename_extension: false
Indentation:
enabled: true
allow_non_nested_indentation: false
character: space # or 'tab'
width: 2
LeadingZero: LeadingZero:
enabled: false enabled: false
MergeableSelector:
enabled: true
force_nesting: true
NameFormat:
enabled: true
allow_leading_underscore: true
convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern
NestingDepth:
enabled: true
max_depth: 6
ignore_parent_selectors: false
PlaceholderInExtend:
enabled: true
PrivateNamingConvention:
enabled: false
prefix: _
PropertyCount:
enabled: false
include_nested: false
max_properties: 10
PropertySortOrder:
enabled: true
ignore_unspecified: false
min_properties: 2
separate_groups: false
PropertySpelling:
enabled: true
extra_properties: []
disabled_properties: []
PropertyUnits:
enabled: true
global: [
'ch',
'em',
'ex',
'rem', # Font-relative lengths
'cm',
'in',
'mm',
'pc',
'pt',
'px',
'q', # Absolute lengths
'vh',
'vw',
'vmin',
'vmax', # Viewport-percentage lengths
'fr', # Grid fractional lengths
'deg',
'grad',
'rad',
'turn', # Angle
'ms',
's', # Duration
'Hz',
'kHz', # Frequency
'dpi',
'dpcm',
'dppx', # Resolution
'%',
] # Other
properties: {}
PseudoElement:
enabled: true
QualifyingElement:
enabled: true
allow_element_with_attribute: false
allow_element_with_class: false
allow_element_with_id: false
SelectorDepth:
enabled: true
max_depth: 5
SelectorFormat:
enabled: false
Shorthand:
enabled: true
allowed_shorthands: [1, 2, 3, 4]
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
SingleLinePerSelector:
enabled: true
SpaceAfterComma:
enabled: true
style: one_space # or 'no_space', or 'at_least_one_space'
SpaceAfterComment:
enabled: false
style: one_space # or 'no_space', or 'at_least_one_space'
allow_empty_comments: true
SpaceAfterPropertyColon:
enabled: true
style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned'
SpaceAfterPropertyName:
enabled: true
SpaceAfterVariableColon:
enabled: false
style: one_space # or 'no_space', 'at_least_one_space' or 'one_space_or_newline'
SpaceAfterVariableName:
enabled: true
SpaceAroundOperator:
enabled: true
style: one_space # or 'at_least_one_space', or 'no_space'
SpaceBeforeBrace:
enabled: true
style: space # or 'new_line'
allow_single_line_padding: false
SpaceBetweenParens:
enabled: true
spaces: 0
StringQuotes:
enabled: true
style: single_quotes # or double_quotes
TrailingSemicolon:
enabled: true
TrailingWhitespace:
enabled: true
TrailingZero:
enabled: false
TransitionAll:
enabled: false
UnnecessaryMantissa:
enabled: false
UnnecessaryParentReference:
enabled: true
UrlFormat:
enabled: true
UrlQuotes:
enabled: true
VariableForProperty:
enabled: false
properties: []
VendorPrefix:
enabled: true
identifier_list: base
additional_identifiers: []
excluded_identifiers: []
ZeroUnit:
enabled: true
Compass::*:
enabled: false
exclude: exclude:
- 'app/javascript/widget/assets/scss/_reset.scss' - 'app/javascript/widget/assets/scss/_reset.scss'
- 'app/javascript/widget/assets/scss/sdk.css' - 'app/javascript/widget/assets/scss/sdk.css'

12
Gemfile
View file

@ -16,6 +16,7 @@ gem 'hashie'
gem 'jbuilder' gem 'jbuilder'
gem 'kaminari' gem 'kaminari'
gem 'responders' gem 'responders'
gem 'rest-client'
gem 'time_diff' gem 'time_diff'
gem 'tzinfo-data' gem 'tzinfo-data'
gem 'valid_email2' gem 'valid_email2'
@ -60,7 +61,6 @@ gem 'chargebee'
##--- gems for channels ---## ##--- gems for channels ---##
gem 'facebook-messenger' gem 'facebook-messenger'
gem 'telegram-bot-ruby' gem 'telegram-bot-ruby'
gem 'twitter'
# twitty will handle subscription of twitter account events # twitty will handle subscription of twitter account events
gem 'twitty', git: 'https://github.com/chatwoot/twitty' gem 'twitty', git: 'https://github.com/chatwoot/twitty'
@ -72,19 +72,27 @@ gem 'haikunator'
##--- gems for debugging and error reporting ---## ##--- gems for debugging and error reporting ---##
# static analysis # static analysis
gem 'brakeman' gem 'brakeman'
gem 'scout_apm'
gem 'sentry-raven' gem 'sentry-raven'
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq' gem 'sidekiq'
##-- used for single column multiple binary flags in notification settings/feature flagging --##
gem 'flag_shih_tzu'
group :development do group :development do
gem 'annotate' gem 'annotate'
gem 'bullet' gem 'bullet'
gem 'letter_opener' gem 'letter_opener'
gem 'web-console' gem 'web-console'
# used in swagger build
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
end end
group :development, :test do group :development, :test do
# locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved
gem 'action-cable-testing' gem 'action-cable-testing'
gem 'bundle-audit', require: false gem 'bundle-audit', require: false
gem 'byebug', platform: :mri gem 'byebug', platform: :mri
@ -98,9 +106,9 @@ group :development, :test do
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false gem 'rubocop-rspec', require: false
gem 'scss_lint', require: false
gem 'seed_dump' gem 'seed_dump'
gem 'shoulda-matchers' gem 'shoulda-matchers'
# locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved
gem 'simplecov', '0.17.1', require: false gem 'simplecov', '0.17.1', require: false
gem 'spring' gem 'spring'
gem 'spring-watcher-listen' gem 'spring-watcher-listen'

View file

@ -1,10 +1,18 @@
GIT GIT
remote: https://github.com/chatwoot/twitty remote: https://github.com/chatwoot/twitty
revision: 58b4958d7f4a58eec8fe9543caedb232308253f6 revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
specs: specs:
twitty (0.1.0) twitty (0.1.0)
oauth oauth
GIT
remote: https://github.com/tzmfreedom/json_refs
revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
ref: e32deb0
specs:
json_refs (0.1.2)
hana
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
@ -109,7 +117,6 @@ GEM
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.7.2) brakeman (4.7.2)
browser (3.0.3) browser (3.0.3)
buftok (0.2.0)
builder (3.2.4) builder (3.2.4)
bullet (6.1.0) bullet (6.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -170,6 +177,7 @@ GEM
faraday_middleware (0.14.0) faraday_middleware (0.14.0)
faraday (>= 0.7.4, < 1.0) faraday (>= 0.7.4, < 1.0)
ffi (1.12.2) ffi (1.12.2)
flag_shih_tzu (0.3.23)
foreman (0.87.0) foreman (0.87.0)
globalid (0.4.2) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -202,17 +210,11 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (~> 0.12) signet (~> 0.12)
haikunator (1.1.0) haikunator (1.1.0)
hana (1.3.5)
hashie (4.1.0) hashie (4.1.0)
http (3.3.0)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
http-accept (1.7.0) http-accept (1.7.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.2.0)
http_parser.rb (0.6.0)
httparty (0.17.3) httparty (0.17.3)
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
@ -259,8 +261,6 @@ GEM
marcel (0.3.3) marcel (0.3.3)
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
memoist (0.16.2) memoist (0.16.2)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.2) method_source (0.9.2)
mime-types (3.3.1) mime-types (3.3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
@ -275,11 +275,10 @@ GEM
multi_json (1.14.1) multi_json (1.14.1)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.1.1) multipart-post (2.1.1)
naught (1.1.0)
netrc (0.11.0) netrc (0.11.0)
nightfury (1.0.1) nightfury (1.0.1)
nio4r (2.5.2) nio4r (2.5.2)
nokogiri (1.10.7) nokogiri (1.10.8)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
oauth (0.5.4) oauth (0.5.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
@ -294,7 +293,7 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.3) public_suffix (4.0.3)
puma (4.3.1) puma (4.3.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -393,6 +392,15 @@ GEM
rubocop-rspec (1.37.1) rubocop-rspec (1.37.1)
rubocop (>= 0.68.1) rubocop (>= 0.68.1)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
scout_apm (2.6.6)
parser
scss_lint (0.59.0)
sass (~> 3.5, >= 3.5.5)
seed_dump (3.3.1) seed_dump (3.3.1)
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -410,7 +418,6 @@ GEM
faraday (~> 0.9) faraday (~> 0.9)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simple_oauth (0.3.1)
simplecov (0.17.1) simplecov (0.17.1)
docile (~> 1.1) docile (~> 1.1)
json (>= 1.8, < 3) json (>= 1.8, < 3)
@ -436,17 +443,6 @@ GEM
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
i18n i18n
twitter (6.2.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
equalizer (~> 0.0.11)
http (~> 3.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
memoizable (~> 0.4.0)
multipart-post (~> 2.0)
naught (~> 1.0)
simple_oauth (~> 0.3.0)
tzinfo (1.2.6) tzinfo (1.2.6)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2019.3) tzinfo-data (1.2019.3)
@ -507,11 +503,13 @@ DEPENDENCIES
facebook-messenger facebook-messenger
factory_bot_rails factory_bot_rails
faker faker
flag_shih_tzu
foreman foreman
google-cloud-storage google-cloud-storage
haikunator haikunator
hashie hashie
jbuilder jbuilder
json_refs!
jwt jwt
kaminari kaminari
koala koala
@ -530,11 +528,14 @@ DEPENDENCIES
redis-namespace redis-namespace
redis-rack-cache redis-rack-cache
responders responders
rest-client
rspec-rails (~> 4.0.0.beta2) rspec-rails (~> 4.0.0.beta2)
rubocop rubocop
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
scout_apm
scss_lint
seed_dump seed_dump
sentry-raven sentry-raven
shoulda-matchers shoulda-matchers
@ -544,7 +545,6 @@ DEPENDENCIES
spring-watcher-listen spring-watcher-listen
telegram-bot-ruby telegram-bot-ruby
time_diff time_diff
twitter
twitty! twitty!
tzinfo-data tzinfo-data
uglifier uglifier

View file

@ -52,16 +52,10 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under
## Docker ## Docker
You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot) Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`.
```bash
docker pull chatwoot/chatwoot
```
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker. Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker.
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using docker composer.
## Contributors ✨ ## Contributors ✨
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors): Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):

1
__mocks__/fileMock.js Normal file
View file

@ -0,0 +1 @@
module.exports = '';

View file

@ -2,7 +2,7 @@ require 'open-uri'
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` # This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
# Assumptions # Assumptions
# 1. Incase of an outgoing message which is echo, fb_id will NOT be nil, # 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
# based on this we are showing "not sent from chatwoot" message in frontend # based on this we are showing "not sent from chatwoot" message in frontend
# Hence there is no need to set user_id in message for outgoing echo messages. # Hence there is no need to set user_id in message for outgoing echo messages.
@ -121,7 +121,7 @@ class Messages::MessageBuilder
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
message_type: @message_type, message_type: @message_type,
content: response.content, content: response.content,
fb_id: response.identifier source_id: response.identifier
} }
end end

View file

@ -23,7 +23,7 @@ class Messages::Outgoing::NormalBuilder
content: @content, content: @content,
private: @private, private: @private,
user_id: @user.id, user_id: @user.id,
fb_id: @fb_id source_id: @fb_id
} }
end end
end end

View file

@ -1,16 +1,14 @@
class Api::BaseController < ApplicationController class Api::BaseController < ApplicationController
respond_to :json respond_to :json
before_action :authenticate_user! before_action :authenticate_user!
unless Rails.env.development?
rescue_from StandardError do |exception|
Raven.capture_exception(exception)
render json: { error: '500 error', message: exception.message }.to_json, status: 500
end
end
private private
def set_conversation def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id]) @conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end end
def check_billing_enabled
raise ActionController::RoutingError, 'Not Found' unless ENV['BILLING_ENABLED']
end
end end

View file

@ -0,0 +1,36 @@
class Api::V1::Account::WebhooksController < Api::BaseController
before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy]
def index
@webhooks = current_account.webhooks
end
def create
@webhook = current_account.webhooks.new(webhook_params)
@webhook.save!
end
def update
@webhook.update!(webhook_params)
end
def destroy
@webhook.destroy
head :ok
end
private
def webhook_params
params.require(:webhook).permit(:inbox_id, :url)
end
def fetch_webhook
@webhook = current_account.webhooks.find(params[:id])
end
def check_authorization
authorize(Webhook)
end
end

View file

@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController
skip_before_action :verify_authenticity_token, only: [:create] skip_before_action :verify_authenticity_token, only: [:create]
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception, skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
only: [:create], raise: false only: [:create], raise: false
before_action :check_signup_enabled
rescue_from CustomExceptions::Account::InvalidEmail, rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::UserExists, CustomExceptions::Account::UserExists,
@ -30,4 +31,8 @@ class Api::V1::AccountsController < Api::BaseController
def account_params def account_params
params.permit(:account_name, :email) params.permit(:account_name, :email)
end end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
end
end end

View file

@ -1,8 +1,7 @@
require 'rest-client' require 'rest-client'
require 'telegram/bot' require 'telegram/bot'
class Api::V1::CallbacksController < ApplicationController class Api::V1::CallbacksController < Api::BaseController
skip_before_action :verify_authenticity_token, only: [:register_facebook_page] before_action :inbox, only: [:reauthorize_page]
skip_before_action :authenticate_user!, only: [:register_facebook_page], raise: false
def register_facebook_page def register_facebook_page
user_access_token = params[:user_access_token] user_access_token = params[:user_access_token]
@ -25,15 +24,13 @@ class Api::V1::CallbacksController < ApplicationController
# get params[:inbox_id], current_account, params[:omniauth_token] # get params[:inbox_id], current_account, params[:omniauth_token]
def reauthorize_page def reauthorize_page
if @inbox&.first&.facebook? if @inbox&.facebook?
fb_page_id = @inbox.channel.page_id fb_page_id = @inbox.channel.page_id
page_details = fb_object.get_connections('me', 'accounts') page_details = fb_object.get_connections('me', 'accounts')
(page_details || []).each do |page_detail| if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] })
if fb_page_id == page_detail['id'] # found the page which has to be reauthorised
update_fb_page(fb_page_id, page_detail['access_token']) update_fb_page(fb_page_id, page_detail['access_token'])
head :ok return head :ok
end
end end
end end
@ -46,18 +43,13 @@ class Api::V1::CallbacksController < ApplicationController
@inbox = current_account.inboxes.find_by(id: params[:inbox_id]) @inbox = current_account.inboxes.find_by(id: params[:inbox_id])
end end
def update_fb_page def update_fb_page(fb_page_id, access_token)
if fb_page(fb_page_id) get_fb_page(fb_page_id)&.update!(
fb_page.update_attributes!(
user_access_token: @user_access_token, page_access_token: access_token user_access_token: @user_access_token, page_access_token: access_token
) )
head :ok
else
head :unprocessable_entity
end
end end
def fb_page(fb_page_id) def get_fb_page(fb_page_id)
current_account.facebook_pages.find_by(page_id: fb_page_id) current_account.facebook_pages.find_by(page_id: fb_page_id)
end end
@ -68,7 +60,7 @@ class Api::V1::CallbacksController < ApplicationController
def long_lived_token(omniauth_token) def long_lived_token(omniauth_token)
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET']) koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
long_lived_token = koala.exchange_access_token_info(omniauth_token)['access_token'] koala.exchange_access_token_info(omniauth_token)['access_token']
end end
def mark_already_existing_facebook_pages(data) def mark_already_existing_facebook_pages(data)
@ -81,7 +73,11 @@ class Api::V1::CallbacksController < ApplicationController
end end
def set_avatar(facebook_channel, page_id) def set_avatar(facebook_channel, page_id)
avatar_resource = LocalResource.new(get_avatar_url(page_id)) uri = get_avatar_url(page_id)
return unless uri
avatar_resource = LocalResource.new(uri)
facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
end end
@ -98,7 +94,6 @@ class Api::V1::CallbacksController < ApplicationController
raise raise
end end
pic_url = response.base_uri.to_s pic_url = response.base_uri.to_s
Rails.logger.info(pic_url)
rescue StandardError => e rescue StandardError => e
pic_url = nil pic_url = nil
end end

View file

@ -1,11 +1,5 @@
class Api::V1::ConversationsController < Api::BaseController class Api::V1::ConversationsController < Api::BaseController
before_action :set_conversation, except: [:index, :get_messages] before_action :set_conversation, except: [:index]
# TODO: move this to public controller
skip_before_action :authenticate_user!, only: [:get_messages]
skip_before_action :set_current_user, only: [:get_messages]
skip_before_action :check_subscription, only: [:get_messages]
skip_around_action :handle_with_exception, only: [:get_messages]
def index def index
result = conversation_finder.perform result = conversation_finder.perform
@ -27,11 +21,6 @@ class Api::V1::ConversationsController < Api::BaseController
head :ok head :ok
end end
def get_messages
@conversation = Conversation.find(params[:id])
@messages = messages_finder.perform
end
private private
def parsed_last_seen_at def parsed_last_seen_at

View file

@ -4,18 +4,12 @@ class Api::V1::InboxMembersController < Api::BaseController
def create def create
# update also done via same action # update also done via same action
if @inbox
begin
update_agents_list update_agents_list
head :ok head :ok
rescue StandardError => e rescue StandardError => e
Rails.logger.debug "Rescued: #{e.inspect}" Rails.logger.debug "Rescued: #{e.inspect}"
render_could_not_create_error('Could not add agents to inbox') render_could_not_create_error('Could not add agents to inbox')
end end
else
render_not_found_error('Agents or inbox not found')
end
end
def show def show
@agents = current_account.users.where(id: @inbox.members.pluck(:user_id)) @agents = current_account.users.where(id: @inbox.members.pluck(:user_id))

View file

@ -1,6 +1,6 @@
class Api::V1::InboxesController < Api::BaseController class Api::V1::InboxesController < Api::BaseController
before_action :check_authorization before_action :check_authorization
before_action :fetch_inbox, only: [:destroy] before_action :fetch_inbox, only: [:destroy, :update]
def index def index
@inboxes = policy_scope(current_account.inboxes) @inboxes = policy_scope(current_account.inboxes)
@ -11,6 +11,10 @@ class Api::V1::InboxesController < Api::BaseController
head :ok head :ok
end end
def update
@inbox.update(inbox_update_params)
end
private private
def fetch_inbox def fetch_inbox
@ -20,4 +24,8 @@ class Api::V1::InboxesController < Api::BaseController
def check_authorization def check_authorization
authorize(Inbox) authorize(Inbox)
end end
def inbox_update_params
params.require(:inbox).permit(:enable_auto_assignment)
end
end end

View file

@ -1,5 +1,5 @@
class Api::V1::ProfilesController < Api::BaseController class Api::V1::ProfilesController < Api::BaseController
before_action :fetch_user before_action :set_user
def show def show
render json: @user render json: @user
@ -7,12 +7,11 @@ class Api::V1::ProfilesController < Api::BaseController
def update def update
@user.update!(profile_params) @user.update!(profile_params)
render json: @user
end end
private private
def fetch_user def set_user
@user = current_user @user = current_user
end end

View file

@ -1,6 +1,8 @@
class Api::V1::SubscriptionsController < ApplicationController class Api::V1::SubscriptionsController < Api::BaseController
skip_before_action :check_subscription skip_before_action :check_subscription
before_action :check_billing_enabled
def index def index
render json: current_account.subscription_data render json: current_account.subscription_data
end end

View file

@ -0,0 +1,29 @@
class Api::V1::User::NotificationSettingsController < Api::BaseController
before_action :set_user, :load_notification_setting
def show; end
def update
update_flags
@notification_setting.save!
render action: 'show'
end
private
def set_user
@user = current_user
end
def load_notification_setting
@notification_setting = @user.notification_settings.find_by(account_id: current_account.id)
end
def notification_setting_params
params.require(:notification_settings).permit(selected_email_flags: [])
end
def update_flags
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
end
end

View file

@ -4,6 +4,7 @@ class Api::V1::WebhooksController < ApplicationController
skip_before_action :check_subscription skip_before_action :check_subscription
before_action :login_from_basic_auth, only: [:chargebee] before_action :login_from_basic_auth, only: [:chargebee]
before_action :check_billing_enabled, only: [:chargebee]
def chargebee def chargebee
chargebee_consumer.consume chargebee_consumer.consume
head :ok head :ok
@ -13,7 +14,7 @@ class Api::V1::WebhooksController < ApplicationController
end end
def twitter_crc def twitter_crc
render json: { response_token: "sha256=#{$twitter.generate_crc(params[:crc_token])}" } render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" }
end end
def twitter_events def twitter_events
@ -26,6 +27,12 @@ class Api::V1::WebhooksController < ApplicationController
private private
def twitter_client
Twitty::Facade.new do |config|
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
end
end
def login_from_basic_auth def login_from_basic_auth
authenticate_or_request_with_http_basic do |username, password| authenticate_or_request_with_http_basic do |username, password|
username == ENV['CHARGEBEE_WEBHOOK_USERNAME'] && password == ENV['CHARGEBEE_WEBHOOK_PASSWORD'] username == ENV['CHARGEBEE_WEBHOOK_USERNAME'] && password == ENV['CHARGEBEE_WEBHOOK_PASSWORD']

View file

@ -31,9 +31,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def message_params def message_params
{ {
account_id: conversation.account_id, account_id: conversation.account_id,
contact_id: @contact.id,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
message_type: :incoming, message_type: :incoming
content: permitted_params[:message][:content]
} }
end end

View file

@ -67,7 +67,7 @@ class ApplicationController < ActionController::Base
def check_subscription def check_subscription
# This block is left over from the initial version of chatwoot # This block is left over from the initial version of chatwoot
# We might reuse this later in the hosted version of chatwoot. # We might reuse this later in the hosted version of chatwoot.
return unless ENV['BILLING_ENABLED'] return if !ENV['BILLING_ENABLED'] || !current_user
if current_subscription.trial? && current_subscription.expiry < Date.current if current_subscription.trial? && current_subscription.expiry < Date.current
render json: { error: 'Trial Expired' }, status: :trial_expired render json: { error: 'Trial Expired' }, status: :trial_expired

View file

@ -2,4 +2,8 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
# Prevent session parameter from being passed # Prevent session parameter from being passed
# Unpermitted parameter: session # Unpermitted parameter: session
wrap_parameters format: [] wrap_parameters format: []
def render_create_success
render 'devise/auth.json'
end
end end

View file

@ -1,13 +1,9 @@
require 'rest-client' require 'rest-client'
require 'telegram/bot' require 'telegram/bot'
class HomeController < ApplicationController class HomeController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:telegram] skip_before_action :verify_authenticity_token, only: [:telegram]
skip_before_action :authenticate_user!, only: [:telegram], raise: false skip_before_action :authenticate_user!, only: [:telegram], raise: false
skip_before_action :set_current_user skip_before_action :set_current_user
skip_before_action :check_subscription skip_before_action :check_subscription
def index; end
def status
head :ok
end
end end

View file

@ -0,0 +1,18 @@
class SwaggerController < ApplicationController
def respond
if Rails.env.development? || Rails.env.test?
render inline: File.read(Rails.root.join('swagger', derived_path))
else
head 404
end
end
private
def derived_path
params[:path] ||= 'index.html'
path = params[:path]
path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s)
path
end
end

View file

@ -0,0 +1,30 @@
class Twitter::AuthorizationsController < Twitter::BaseController
def create
@response = twitter_client.request_oauth_token(url: twitter_callback_url)
if @response.status == '200'
::Redis::Alfred.setex(oauth_token, account.id)
redirect_to oauth_authorize_endpoint(oauth_token)
else
redirect_to app_new_twitter_inbox_url
end
end
private
def oauth_token
parsed_body['oauth_token']
end
def user
@user ||= User.find_by(id: params[:user_id])
end
def account
@account ||= user.account
end
def oauth_authorize_endpoint(oauth_token)
"#{twitter_api_base_url}/oauth/authorize?oauth_token=#{oauth_token}"
end
end

View file

@ -0,0 +1,24 @@
class Twitter::BaseController < ApplicationController
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def host
ENV.fetch('FRONTEND_URL', '')
end
def twitter_client
Twitty::Facade.new do |config|
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
config.base_url = twitter_api_base_url
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
end
end
def twitter_api_base_url
'https://api.twitter.com'
end
end

View file

@ -0,0 +1,51 @@
class Twitter::CallbacksController < Twitter::BaseController
def show
@response = twitter_client.access_token(
oauth_token: permitted_params[:oauth_token],
oauth_verifier: permitted_params[:oauth_verifier]
)
if @response.status == '200'
inbox = build_inbox
::Redis::Alfred.delete(permitted_params[:oauth_token])
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
redirect_to app_twitter_inbox_agents_url(inbox_id: inbox.id)
else
redirect_to app_new_twitter_inbox_url
end
end
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def account_id
::Redis::Alfred.get(permitted_params[:oauth_token])
end
def account
@account ||= Account.find_by!(id: account_id)
end
def build_inbox
ActiveRecord::Base.transaction do
twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id'],
name: parsed_body['screen_name']
)
account.inboxes.create(
name: parsed_body['screen_name'],
channel: twitter_profile
)
rescue StandardError => e
Rails.logger e
end
end
def permitted_params
params.permit(:oauth_token, :oauth_verifier)
end
end

View file

@ -4,9 +4,7 @@ class WidgetsController < ActionController::Base
before_action :set_contact before_action :set_contact
before_action :build_contact before_action :build_contact
def index def index; end
render
end
private private

View file

@ -5,7 +5,7 @@ class AsyncDispatcher < BaseDispatcher
end end
def listeners def listeners
listeners = [ReportingListener.instance] listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED'] listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
listeners listeners
end end

View file

@ -1,23 +1,18 @@
class ConversationFinder class ConversationFinder
attr_reader :current_user, :current_account, :params attr_reader :current_user, :current_account, :params
ASSIGNEE_TYPES = { me: 0, unassigned: 1, all: 2 }.freeze
ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert
ASSIGNEE_TYPES_BY_ID.default = :me
DEFAULT_STATUS = 'open'.freeze DEFAULT_STATUS = 'open'.freeze
# assumptions # assumptions
# inbox_id if not given, take from all conversations, else specific to inbox # inbox_id if not given, take from all conversations, else specific to inbox
# assignee_type if not given, take 'me' # assignee_type if not given, take 'all'
# conversation_status if not given, take 'open' # conversation_status if not given, take 'open'
# response of this class will be of type # response of this class will be of type
# {conversations: [array of conversations], count: {open: count, resolved: count}} # {conversations: [array of conversations], count: {open: count, resolved: count}}
# params # params
# assignee_type_id, inbox_id, :status # assignee_type, inbox_id, :status
def initialize(current_user, params) def initialize(current_user, params)
@current_user = current_user @current_user = current_user
@ -62,7 +57,7 @@ class ConversationFinder
end end
def set_assignee_type def set_assignee_type
@assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]] @assignee_type = params[:assignee_type]
end end
def find_all_conversations def find_all_conversations
@ -72,12 +67,10 @@ class ConversationFinder
end end
def filter_by_assignee_type def filter_by_assignee_type
if @assignee_type_id == ASSIGNEE_TYPES[:me] if @assignee_type == 'me'
@conversations = @conversations.assigned_to(current_user) @conversations = @conversations.assigned_to(current_user)
elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned] elsif @assignee_type == 'unassigned'
@conversations = @conversations.unassigned @conversations = @conversations.unassigned
elsif @assignee_type_id == ASSIGNEE_TYPES[:all]
@conversations
end end
@conversations @conversations
end end

View file

@ -1,2 +0,0 @@
module Api::V1::WebhooksHelper
end

View file

@ -18,8 +18,7 @@ export default {
}, },
mounted() { mounted() {
this.$store.dispatch('set_user'); this.$store.dispatch('setUser');
this.$store.dispatch('validityCheck');
}, },
}; };
</script> </script>

View file

@ -1,29 +1,10 @@
/* eslint no-console: 0 */ /* eslint no-console: 0 */
/* global axios */ /* global axios */
/* eslint no-undef: "error" */ /* eslint no-undef: "error" */
/* eslint-env browser */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import moment from 'moment';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import endPoints from './endPoints'; import endPoints from './endPoints';
import { frontendURL } from '../helper/URLHelper'; import { setAuthCredentials, clearCookiesOnLogout } from '../store/utils/api';
const setAuthCredentials = response => {
const expiryDate = moment.unix(response.headers.expiry);
Cookies.set('auth_data', response.headers, {
expires: expiryDate.diff(moment(), 'days'),
});
Cookies.set('user', response.data.data, {
expires: expiryDate.diff(moment(), 'days'),
});
};
const clearCookiesOnLogout = () => {
Cookies.remove('auth_data');
Cookies.remove('user');
window.location = frontendURL('login');
};
export default { export default {
login(creds) { login(creds) {
@ -60,20 +41,7 @@ export default {
}, },
validityCheck() { validityCheck() {
const urlData = endPoints('validityCheck'); const urlData = endPoints('validityCheck');
const fetchPromise = new Promise((resolve, reject) => { return axios.get(urlData.url);
axios
.get(urlData.url)
.then(response => {
resolve(response);
})
.catch(error => {
if (error.response.status === 401) {
clearCookiesOnLogout();
}
reject(error);
});
});
return fetchPromise;
}, },
logout() { logout() {
const urlData = endPoints('logout'); const urlData = endPoints('logout');
@ -136,13 +104,7 @@ export default {
password, password,
}) })
.then(response => { .then(response => {
const expiryDate = moment.unix(response.headers.expiry); setAuthCredentials(response);
Cookies.set('auth_data', response.headers, {
expires: expiryDate.diff(moment(), 'days'),
});
Cookies.set('user', response.data.data, {
expires: expiryDate.diff(moment(), 'days'),
});
resolve(response); resolve(response);
}) })
.catch(error => { .catch(error => {
@ -155,4 +117,22 @@ export default {
const urlData = endPoints('resetPassword'); const urlData = endPoints('resetPassword');
return axios.post(urlData.url, { email }); return axios.post(urlData.url, { email });
}, },
profileUpdate({ name, email, password, password_confirmation, avatar }) {
const formData = new FormData();
if (name) {
formData.append('profile[name]', name);
}
if (email) {
formData.append('profile[email]', email);
}
if (password && password_confirmation) {
formData.append('profile[password]', password);
formData.append('profile[password_confirmation]', password_confirmation);
}
if (avatar) {
formData.append('profile[avatar]', avatar);
}
return axios.put(endPoints('profileUpdate').url, formData);
},
}; };

View file

@ -10,8 +10,8 @@ class ConversationApi extends ApiClient {
return axios.get(`${this.url}/${conversationID}/labels`); return axios.get(`${this.url}/${conversationID}/labels`);
} }
createLabels(conversationID) { updateLabels(conversationID, labels) {
return axios.get(`${this.url}/${conversationID}/labels`); return axios.post(`${this.url}/${conversationID}/labels`, { labels });
} }
} }

View file

@ -10,6 +10,9 @@ const endPoints = {
validityCheck: { validityCheck: {
url: '/auth/validate_token', url: '/auth/validate_token',
}, },
profileUpdate: {
url: '/api/v1/profile',
},
logout: { logout: {
url: 'auth/sign_out', url: 'auth/sign_out',
}, },

View file

@ -6,12 +6,13 @@ class ConversationApi extends ApiClient {
super('conversations'); super('conversations');
} }
get({ inboxId, status, assigneeType }) { get({ inboxId, status, assigneeType, page }) {
return axios.get(this.url, { return axios.get(this.url, {
params: { params: {
inbox_id: inboxId, inbox_id: inboxId,
status, status,
assignee_type_id: assigneeType, assignee_type: assigneeType,
page,
}, },
}); });
} }

View file

@ -10,6 +10,6 @@ describe('#ConversationApi', () => {
expect(conversations).toHaveProperty('update'); expect(conversations).toHaveProperty('update');
expect(conversations).toHaveProperty('delete'); expect(conversations).toHaveProperty('delete');
expect(conversations).toHaveProperty('getLabels'); expect(conversations).toHaveProperty('getLabels');
expect(conversations).toHaveProperty('createLabels'); expect(conversations).toHaveProperty('updateLabels');
}); });
}); });

View file

@ -0,0 +1,13 @@
import userNotificationSettings from '../userNotificationSettings';
import ApiClient from '../ApiClient';
describe('#AgentAPI', () => {
it('creates correct instance', () => {
expect(userNotificationSettings).toBeInstanceOf(ApiClient);
expect(userNotificationSettings).toHaveProperty('get');
expect(userNotificationSettings).toHaveProperty('show');
expect(userNotificationSettings).toHaveProperty('create');
expect(userNotificationSettings).toHaveProperty('update');
expect(userNotificationSettings).toHaveProperty('delete');
});
});

View file

@ -0,0 +1,14 @@
/* global axios */
import ApiClient from './ApiClient';
class UserNotificationSettings extends ApiClient {
constructor() {
super('user/notification_settings');
}
update(params) {
return axios.patch(`${this.url}`, params);
}
}
export default new UserNotificationSettings();

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class WebHooks extends ApiClient {
constructor() {
super('account/webhooks');
}
}
export default new WebHooks();

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#4D4D4D;" d="M188.287,512c-41.473,0-75.213-33.74-75.213-75.213V246.75c0-4.142,3.358-7.5,7.5-7.5
s7.5,3.358,7.5,7.5v190.037c0,33.202,27.011,60.213,60.213,60.213c16.082,0,31.204-6.266,42.582-17.644
c11.37-11.37,17.631-26.488,17.631-42.569V75.213C248.5,33.74,282.24,0,323.713,0c20.088,0,38.978,7.826,53.189,22.037
c14.203,14.202,22.024,33.087,22.024,53.176V256c0,4.142-3.358,7.5-7.5,7.5s-7.5-3.358-7.5-7.5V75.213
c0-16.082-6.261-31.2-17.63-42.569C354.918,21.266,339.794,15,323.713,15C290.511,15,263.5,42.011,263.5,75.213v361.574
c0,20.088-7.822,38.973-22.024,53.176C227.265,504.174,208.376,512,188.287,512z"/>
<g>
<rect x="113.07" y="246.75" style="fill:#3B3B3B;" width="15" height="26.875"/>
<rect x="383.93" y="235.31" style="fill:#3B3B3B;" width="15" height="26.875"/>
</g>
<rect x="361.9" y="385" style="fill:#CCCCCC;" width="57.983" height="39.944"/>
<rect x="361.9" y="385" style="fill:#ADADAD;" width="57.983" height="22.19"/>
<path style="fill:#A6E2E3;" d="M432.802,298.678v86.977c0,3.616-2.932,6.548-6.548,6.548h-70.721c-3.617,0-6.548-2.932-6.548-6.548
v-87.746c0-23.439,19.239-42.39,42.803-41.899C414.709,256.486,432.802,275.751,432.802,298.678z"/>
<rect x="92.11" y="87.06" style="fill:#CCCCCC;" width="57.983" height="36.28"/>
<rect x="92.11" y="105.43" style="fill:#ADADAD;" width="57.983" height="17.907"/>
<path style="fill:#FFA638;" d="M163.015,126.345v86.977c0,22.927-18.093,42.191-41.015,42.668
c-23.564,0.49-42.803-18.461-42.803-41.899v-87.746c0-3.616,2.932-6.548,6.548-6.548l0,0h70.721l0,0
C160.083,119.797,163.015,122.729,163.015,126.345z"/>
<path style="fill:#7CCBCC;" d="M391.787,256.009c-5.066-0.105-9.93,0.693-14.447,2.236c0.396-0.081,0.781-0.166,1.142-0.257
c2.982-0.755,5.201-0.896,7.513-0.85c18.954,0.395,34.375,16.494,34.375,35.888v86.981c0,3.614-2.93,6.544-6.544,6.544H355.53
c-3.614,0-6.544-2.93-6.544-6.544l0,0v5.648c0,3.616,2.932,6.548,6.548,6.548h70.721c3.617,0,6.548-2.932,6.548-6.548v-86.977
C432.802,275.751,414.709,256.486,391.787,256.009z"/>
<path style="fill:#EB7100;" d="M79.527,217.153l-0.23-0.322c0.081,1.261,0.209,2.509,0.399,3.737
C79.586,219.444,79.527,218.305,79.527,217.153z"/>
<path style="fill:#ED8300;" d="M156.467,119.797h-11.188c2.613,0.858,4.502,3.314,4.502,6.215v90.372
c0,19.395-15.42,35.494-34.375,35.888c-0.251,0.005-0.503,0.008-0.753,0.008c-9.651,0-18.405-3.914-24.76-10.236
c7.856,8.766,19.342,14.212,32.106,13.947c22.922-0.477,41.015-19.741,41.015-42.668v-86.977
C163.015,122.728,160.083,119.797,156.467,119.797z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -28,7 +28,7 @@
code { code {
border: 0; border: 0;
font-family: 'Monaco'; font-family: 'Monaco', Verdana;
font-size: $font-size-mini; font-size: $font-size-mini;
&.hljs { &.hljs {

View file

@ -41,29 +41,34 @@
// 36. Tooltip // 36. Tooltip
// 37. Top Bar // 37. Top Bar
@import "~foundation-sites/scss/util/util"; @import '~foundation-sites/scss/util/util';
// 1. Global // 1. Global
// --------- // ---------
$global-font-size: 10px; $global-font-size: 10px;
$global-width: 100%; $global-width: 100%;
$global-lineheight: 1.5; $global-lineheight: 1.5;
$foundation-palette: ( $foundation-palette: (primary: $color-woot,
primary: $color-woot,
secondary: #777, secondary: #777,
success: #13ce66, success: #13ce66,
warning: #ffc82c, warning: #ffc82c,
alert: #ff4949 alert: #ff4949);
);
$light-gray: #c0ccda; $light-gray: #c0ccda;
$medium-gray: #8492a6; $medium-gray: #8492a6;
$dark-gray: $color-gray; $dark-gray: $color-gray;
$black: #000000; $black: #000;
$white: #fff; $white: #fff;
$body-background: $white; $body-background: $white;
$body-font-color: $color-body; $body-font-color: $color-body;
$body-font-family: 'Inter', -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", $body-font-family: 'Inter',
Roboto, "Helvetica Neue", Arial, sans-serif; -apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif;
$body-antialiased: true; $body-antialiased: true;
$global-margin: $space-one; $global-margin: $space-one;
$global-padding: $space-one; $global-padding: $space-one;
@ -79,13 +84,11 @@ $print-transparent-backgrounds: true;
// 2. Breakpoints // 2. Breakpoints
// -------------- // --------------
$breakpoints: ( $breakpoints: (small: 0,
small: 0,
medium: 640px, medium: 640px,
large: 1024px, large: 1024px,
xlarge: 1200px, xlarge: 1200px,
xxlarge: 1440px xxlarge: 1440px);
);
$print-breakpoint: large; $print-breakpoint: large;
$breakpoint-classes: (small medium large); $breakpoint-classes: (small medium large);
@ -94,10 +97,8 @@ $breakpoint-classes: (small medium large);
$grid-row-width: $global-width; $grid-row-width: $global-width;
$grid-column-count: 12; $grid-column-count: 12;
$grid-column-gutter: ( $grid-column-gutter: (small: $zero,
small: $zero, medium: $zero);
medium: $zero
);
$grid-column-align-edge: true; $grid-column-align-edge: true;
$block-grid-max: 8; $block-grid-max: 8;
@ -105,54 +106,24 @@ $block-grid-max: 8;
// ------------------ // ------------------
$header-font-family: $body-font-family; $header-font-family: $body-font-family;
$header-font-weight: $global-weight-normal; $header-font-weight: $font-weight-medium;
$header-font-style: normal; $header-font-style: normal;
$font-family-monospace: $body-font-family; $font-family-monospace: $body-font-family;
$header-color: $color-heading; $header-color: $color-heading;
$header-lineheight: 1.4; $header-lineheight: 1.4;
$header-margin-bottom: 0.5rem; $header-margin-bottom: 0.5rem;
$header-styles: ( $header-styles: (small: ("h1": ("font-size": 24),
small: ( "h2": ("font-size": 20),
"h1": ( "h3": ("font-size": 19),
"font-size": 24 "h4": ("font-size": 18),
), "h5": ("font-size": 17),
"h2": ( "h6": ("font-size": 16)),
"font-size": 20 medium: ("h1": ("font-size": 48),
), "h2": ("font-size": 40),
"h3": ( "h3": ("font-size": 31),
"font-size": 19 "h4": ("font-size": 25),
), "h5": ("font-size": 20),
"h4": ( "h6": ("font-size": 16)));
"font-size": 18
),
"h5": (
"font-size": 17
),
"h6": (
"font-size": 16
)
),
medium: (
"h1": (
"font-size": 48
),
"h2": (
"font-size": 40
),
"h3": (
"font-size": 31
),
"h4": (
"font-size": 25
),
"h5": (
"font-size": 20
),
"h6": (
"font-size": 16
)
)
);
$header-text-rendering: optimizeLegibility; $header-text-rendering: optimizeLegibility;
$small-font-size: 80%; $small-font-size: 80%;
$header-small-font-color: $medium-gray; $header-small-font-color: $medium-gray;
@ -186,7 +157,7 @@ $blockquote-padding: rem-calc(9 20 0 19);
$blockquote-border: 1px solid $medium-gray; $blockquote-border: 1px solid $medium-gray;
$cite-font-size: rem-calc(13); $cite-font-size: rem-calc(13);
$cite-color: $dark-gray; $cite-color: $dark-gray;
$cite-pseudo-content: "\2014 \0020"; $cite-pseudo-content: '\2014 \0020';
$keystroke-font: $font-family-monospace; $keystroke-font: $font-family-monospace;
$keystroke-color: $black; $keystroke-color: $black;
$keystroke-background: $light-gray; $keystroke-background: $light-gray;
@ -272,24 +243,23 @@ $button-background-hover: scale-color($button-background, $lightness: -15%);
$button-color: $white; $button-color: $white;
$button-color-alt: $white; $button-color-alt: $white;
$button-radius: $global-radius; $button-radius: $global-radius;
$button-sizes: ( $button-sizes: (tiny: $font-size-micro,
tiny: $font-size-micro,
small: $font-size-mini, small: $font-size-mini,
default: $font-size-default, default: $font-size-default,
large: $font-size-large large: $font-size-large);
);
$button-palette: $foundation-palette; $button-palette: $foundation-palette;
$button-opacity-disabled: 0.25; $button-opacity-disabled: 0.25;
$button-background-hover-lightness: -20%; $button-background-hover-lightness: -20%;
$button-hollow-hover-lightness: -50%; $button-hollow-hover-lightness: -50%;
$button-transition: background-color 0.25s ease-out, color 0.25s ease-out; $button-transition: background-color 0.25s ease-out,
color 0.25s ease-out;
// 12. Button Group // 12. Button Group
// ---------------- // ----------------
$buttongroup-margin: 1rem; $buttongroup-margin: 1rem;
$buttongroup-spacing: 1px; $buttongroup-spacing: 1px;
$buttongroup-child-selector: ".button"; $buttongroup-child-selector: '.button';
$buttongroup-expand-max: 6; $buttongroup-expand-max: 6;
$buttongroup-radius-on-each: true; $buttongroup-radius-on-each: true;
@ -322,18 +292,12 @@ $card-margin: $global-margin;
// ---------------- // ----------------
$closebutton-position: right top; $closebutton-position: right top;
$closebutton-offset-horizontal: ( $closebutton-offset-horizontal: (small: 0.66rem,
small: 0.66rem, medium: 1rem);
medium: 1rem $closebutton-offset-vertical: (small: 0.33em,
); medium: 0.5rem);
$closebutton-offset-vertical: ( $closebutton-size: (small: 1.5em,
small: 0.33em, medium: 2em);
medium: 0.5rem
);
$closebutton-size: (
small: 1.5em,
medium: 2em
);
$closebutton-lineheight: 1; $closebutton-lineheight: 1;
$closebutton-color: $dark-gray; $closebutton-color: $dark-gray;
$closebutton-color-hover: $black; $closebutton-color-hover: $black;
@ -356,11 +320,9 @@ $dropdown-border: 1px solid $medium-gray;
$dropdown-font-size: 1rem; $dropdown-font-size: 1rem;
$dropdown-width: 300px; $dropdown-width: 300px;
$dropdown-radius: $global-radius; $dropdown-radius: $global-radius;
$dropdown-sizes: ( $dropdown-sizes: (tiny: 100px,
tiny: 100px,
small: 200px, small: 200px,
large: 400px large: 400px);
);
// 18. Dropdown Menu // 18. Dropdown Menu
// ----------------- // -----------------
@ -455,12 +417,10 @@ $meter-fill-bad: $alert-color;
// 24. Off-canvas // 24. Off-canvas
// -------------- // --------------
$offcanvas-sizes: ( $offcanvas-sizes: (small: 23rem,
small: 23rem,
medium: 23rem, medium: 23rem,
); );
$offcanvas-vertical-sizes: ( $offcanvas-vertical-sizes: (small: 23rem,
small: 23rem,
medium: 23rem, medium: 23rem,
); );
$offcanvas-background: $light-gray; $offcanvas-background: $light-gray;
@ -472,7 +432,7 @@ $offcanvas-transition-length: 0.5s;
$offcanvas-transition-timing: ease; $offcanvas-transition-timing: ease;
$offcanvas-fixed-reveal: true; $offcanvas-fixed-reveal: true;
$offcanvas-exit-background: rgba($white, 0.25); $offcanvas-exit-background: rgba($white, 0.25);
$maincontent-class: "off-canvas-content"; $maincontent-class: 'off-canvas-content';
// 25. Orbit // 25. Orbit
// --------- // ---------
@ -520,10 +480,8 @@ $progress-radius: $global-radius;
// -------------------- // --------------------
$responsive-embed-margin-bottom: rem-calc(16); $responsive-embed-margin-bottom: rem-calc(16);
$responsive-embed-ratios: ( $responsive-embed-ratios: (default: 4 by 3,
default: 4 by 3, widescreen: 16 by 9);
widescreen: 16 by 9
);
// 29. Reveal // 29. Reveal
// ---------- // ----------
@ -576,10 +534,8 @@ $table-border: 1px solid transparent;
$table-padding: rem-calc(8 10 10); $table-padding: rem-calc(8 10 10);
$table-hover-scale: 2%; $table-hover-scale: 2%;
$table-row-hover: darken($table-background, $table-hover-scale); $table-row-hover: darken($table-background, $table-hover-scale);
$table-row-stripe-hover: darken( $table-row-stripe-hover: darken($table-background,
$table-background, $table-color-scale + $table-hover-scale);
$table-color-scale + $table-hover-scale
);
$table-is-striped: false; $table-is-striped: false;
$table-striped-background: smart-scale($table-background, $table-color-scale); $table-striped-background: smart-scale($table-background, $table-color-scale);
$table-stripe: even; $table-stripe: even;

View file

@ -35,15 +35,17 @@ body {
flex-direction: column; flex-direction: column;
@include margin($zero); @include margin($zero);
@include padding($space-normal); @include padding($space-normal);
overflow-y: scroll; overflow-y: auto;
} }
.content-box { .content-box {
overflow: scroll; overflow: auto;
@include padding($space-normal); @include padding($space-normal);
} }
.back-button { .back-button {
@include flex;
align-items: center;
color: $color-woot; color: $color-woot;
font-size: $font-size-default; font-size: $font-size-default;
font-weight: $font-weight-normal; font-weight: $font-weight-normal;

View file

@ -1,3 +1,8 @@
@import '~widget/assets/scss/mixins';
$elegant-shadow-color: rgba(49, 49, 93, 0.15);
$spinner-before-border-color: rgba(255, 255, 255, 0.7);
//borders //borders
@mixin border-nil() { @mixin border-nil() {
border-color: transparent; border-color: transparent;
@ -77,8 +82,8 @@
&:active, &:active,
&:hover, &:hover,
&:focus { &:focus {
box-shadow: none;
border-color: transparent; border-color: transparent;
box-shadow: none;
} }
} }
@ -117,7 +122,6 @@
// full height // full height
@mixin full-height() { @mixin full-height() {
height: 100%; height: 100%;
// COmmenting because unneccessary scroll is apprearing on some pages eg: settings/agents / inboxes
} }
@mixin round-corner() { @mixin round-corner() {
@ -125,21 +129,20 @@
} }
@mixin scroll-on-hover() { @mixin scroll-on-hover() {
transition: all .4s $ease-in-out-cubic;
overflow: hidden; overflow: hidden;
&:hover { &:hover {
overflow-y: scroll; overflow-y: auto;
} }
} }
@mixin horizontal-scroll() { @mixin horizontal-scroll() {
overflow-y: scroll; overflow-y: auto;
} }
@mixin elegent-shadow() { @mixin elegent-shadow() {
box-shadow: 0 10px 25px 0 rgba(49,49,93,0.15); box-shadow: 0 10px 25px 0 $elegant-shadow-color;
} }
@mixin elegant-card() { @mixin elegant-card() {
@ -154,20 +157,20 @@
} }
} }
&:before { &::before {
content: ''; animation: spinner .9s linear infinite;
border: 2px solid $spinner-before-border-color;
border-radius: 50%;
border-top-color: lighten($color-woot, 10%);
box-sizing: border-box; box-sizing: border-box;
content: '';
height: $space-medium;
left: 50%;
margin-left: -$space-one;
margin-top: -$space-one;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%;
width: $space-medium; width: $space-medium;
height: $space-medium;
margin-top: -$space-one;
margin-left: -$space-one;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.7);
border-top-color: lighten($color-woot, 10%);
animation: spinner .9s linear infinite;
} }
} }
@ -188,27 +191,27 @@
content: ''; content: '';
@if $direction == 'top' { @if $direction == 'top' {
border-bottom: $size solid $color;
border-left: $size solid transparent; border-left: $size solid transparent;
border-right: $size solid transparent; border-right: $size solid transparent;
border-bottom: $size solid $color;
} @else if $direction == 'right' { } @else if $direction == 'right' {
border-top: $size solid transparent;
border-bottom: $size solid transparent; border-bottom: $size solid transparent;
border-left: $size solid $color; border-left: $size solid $color;
} @else if $direction == 'bottom' {
border-top: $size solid $color;
border-right: $size solid transparent;
border-left: $size solid transparent;
} @else if $direction == 'left' {
border-top: $size solid transparent; border-top: $size solid transparent;
border-right: $size solid $color; } @else if $direction == 'bottom' {
border-bottom: $size solid transparent;
} @else if $direction == 'top-left' {
border-top: $size solid $color;
border-right: $size solid transparent;
} @else if $direction == 'top-right' {
border-top: $size solid $color;
border-left: $size solid transparent; border-left: $size solid transparent;
border-right: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'left' {
border-bottom: $size solid transparent;
border-right: $size solid $color;
border-top: $size solid transparent;
} @else if $direction == 'top-left' {
border-right: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'top-right' {
border-left: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'bottom-left' { } @else if $direction == 'bottom-left' {
border-bottom: $size solid $color; border-bottom: $size solid $color;
border-right: $size solid transparent; border-right: $size solid transparent;

View file

@ -12,7 +12,7 @@ $font-size-mega: 3.4rem;
$font-size-giga: 4.0rem; $font-size-giga: 4.0rem;
// spaces // spaces
$zero: 0rem; $zero: 0;
$space-micro: 0.2rem; $space-micro: 0.2rem;
$space-smaller: 0.4rem; $space-smaller: 0.4rem;
$space-small: 0.8rem; $space-small: 0.8rem;
@ -42,16 +42,27 @@ $woot-logo-padding: $space-large $space-two;
// Colors // Colors
$color-woot: #1f93ff; $color-woot: #1f93ff;
$color-gray: #6E6F73; $color-gray: #6e6f73;
$color-light-gray: #999A9B; $color-light-gray: #999a9b;
$color-border: #E0E6ED; $color-border: #e0e6ed;
$color-border-light: #f0f4f5; $color-border-light: #f0f4f5;
$color-background: #EFF2F7; $color-background: #eff2f7;
$color-background-light: #F9FAFC; $color-background-light: #f9fafc;
$color-white: #FFF; $color-white: #fff;
$color-body: #3C4858; $color-body: #3c4858;
$color-heading: #1F2D3D; $color-heading: #1f2d3d;
$color-extra-light-blue: #F5F7F9; $color-extra-light-blue: #f5f7f9;
$primary-color: $color-woot;
$secondary-color: #ff5216;
$success-color: #13ce66;
$warning-color: #ffc82c;
$alert-color: #ff4949;
// Color-palettes
$color-primary-light: #c7e3ff;
$color-primary-dark: darken($color-woot, 20%);
// Thumbnail // Thumbnail
$thumbnail-radius: 4rem; $thumbnail-radius: 4rem;
@ -81,3 +92,6 @@ $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !d
// Ionicons // Ionicons
$ionicons-font-path: '~ionicons/fonts'; $ionicons-font-path: '~ionicons/fonts';
// Transitions
$transition-ease-in: all 0.250s ease-in;

View file

@ -25,6 +25,7 @@
@import 'views/settings/inbox'; @import 'views/settings/inbox';
@import 'views/settings/channel'; @import 'views/settings/channel';
@import 'views/settings/integrations';
@import 'views/signup'; @import 'views/signup';
@import 'plugins/multiselect'; @import 'plugins/multiselect';

View file

@ -1,32 +1,24 @@
@mixin label-multiselect-hover {
&::after {
color: $color-primary-dark;
}
&:hover {
background: $color-background;
&::after {
color: $color-woot;
}
}
}
.multiselect { .multiselect {
margin-bottom: $space-normal; margin-bottom: $space-normal;
min-height: 38px; min-height: 38px;
&.multiselect--active {
>.multiselect__tags { >.multiselect__tags {
@include margin(0); border-color: $color-woot;
border: 1px solid $color-border;
min-height: 44px;
padding-top: $zero;
.multiselect__placeholder {
padding-top: $space-small;
}
.multiselect__tag {
margin-top: $space-one;
}
.multiselect__input {
@include ghost-input;
@include padding($zero);
margin-bottom: $zero;
}
.multiselect__single {
@include padding($space-one);
margin-bottom: 0;
} }
} }
@ -41,4 +33,93 @@
top: 60%; top: 60%;
} }
} }
.multiselect__content .multiselect__option {
font-size: $font-size-small;
font-weight: $font-weight-normal;
&.multiselect__option--highlight {
font-weight: $font-weight-medium;
}
}
}
.multiselect>.multiselect__tags {
@include margin(0);
border: 1px solid $color-border;
min-height: 44px;
padding-top: $zero;
.multiselect__tags-wrap {
display: inline-block;
line-height: 1;
margin-top: $space-smaller;
}
.multiselect__placeholder {
color: $color-gray;
font-weight: $font-weight-normal;
padding-top: $space-small;
}
.multiselect__tag {
$vertical-space: $space-smaller + $space-micro;
background: $color-background;
color: $color-heading;
margin-top: $space-smaller;
padding: $vertical-space $space-medium $vertical-space $space-one;
}
.multiselect__tag-icon {
@include label-multiselect-hover;
line-height: $space-medium + $space-micro;
}
.multiselect__input {
@include ghost-input;
@include padding($zero);
font-size: $font-size-small;
margin-bottom: $zero;
}
.multiselect__single {
@include padding($space-one);
margin-bottom: 0;
}
}
.sidebar-labels-wrap {
&.has-edited,
&:hover {
.multiselect {
cursor: pointer;
}
.multiselect>.multiselect__tags {
border-color: $color-border;
}
.multiselect>.multiselect__select {
visibility: visible;
}
}
.multiselect {
margin-top: $space-small;
>.multiselect__select {
visibility: hidden;
}
>.multiselect__tags {
border-color: transparent;
}
&.multiselect--active>.multiselect__tags {
border-color: $color-woot;
}
}
} }

View file

@ -212,7 +212,7 @@
.code { .code {
max-height: $space-mega; max-height: $space-mega;
overflow: scroll; overflow: auto;
white-space: nowrap; white-space: nowrap;
@include padding($space-one); @include padding($space-one);
background: $color-background; background: $color-background;

View file

@ -0,0 +1,37 @@
.integrations-wrap {
.integration {
background: $color-white;
border: 2px solid $color-border;
border-radius: $space-slab;
padding: $space-normal;
.integration--image {
display: flex;
margin-right: $space-normal;
width: 8rem;
img {
max-width: 8rem;
padding: $space-small;
}
}
.integration--title {
font-size: $font-size-large;
}
.integration--description {
padding-right: $space-medium;
}
.button-wrap {
@include flex;
@include flex-align(center, middle);
margin-bottom: 0;
}
}
}
.help-wrap {
padding-left: $space-large;
}

View file

@ -27,6 +27,15 @@
>.icon { >.icon {
font-size: $font-size-default; font-size: $font-size-default;
} }
&.tiny {
font-size: $font-size-mini;
padding: $space-small $space-slab;
}
&.round {
border-radius: $space-larger;
}
} }
.button--fixed-right-top { .button--fixed-right-top {

View file

@ -5,6 +5,7 @@
@include flex; @include flex;
@include flex-align($x: justify, $y: middle); @include flex-align($x: justify, $y: middle);
@include border-normal-bottom; @include border-normal-bottom;
// Resolve Button // Resolve Button
.button { .button {
@include margin(0); @include margin(0);
@ -44,6 +45,7 @@
.user--name { .user--name {
@include margin(0); @include margin(0);
font-size: $font-size-medium; font-size: $font-size-medium;
text-transform: capitalize;
} }
.user--profile__meta { .user--profile__meta {

View file

@ -2,8 +2,8 @@
@include flex; @include flex;
@include flex-shrink; @include flex-shrink;
@include padding($space-normal $zero $zero $space-normal); @include padding($space-normal $zero $zero $space-normal);
position: relative;
cursor: pointer; cursor: pointer;
position: relative;
&.active { &.active {
background: $color-background; background: $color-background;
@ -18,63 +18,63 @@
.conversation--user { .conversation--user {
font-size: $font-size-small; font-size: $font-size-small;
margin-bottom: $zero; margin-bottom: $zero;
text-transform: capitalize;
.label { .label {
position: relative;
top: $space-micro;
left: $space-micro; left: $space-micro;
max-width: $space-jumbo; max-width: $space-jumbo;
overflow: hidden; overflow: hidden;
position: relative;
text-overflow: ellipsis; text-overflow: ellipsis;
top: $space-micro;
white-space: nowrap; white-space: nowrap;
} }
} }
.conversation--message { .conversation--message {
height: $space-medium;
margin: $zero;
font-size: $font-size-small;
line-height: $space-medium;
font-weight: $font-weight-light;
text-overflow: ellipsis;
overflow: hidden;
color: $color-body; color: $color-body;
width: 27rem; font-size: $font-size-small;
white-space: nowrap; font-weight: $font-weight-normal;
height: $space-medium;
line-height: $space-medium;
margin: $zero;
max-width: 96%; max-width: 96%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 27rem;
} }
.conversation--meta { .conversation--meta {
@include flex;
display: block; display: block;
flex-direction: column;
position: absolute; position: absolute;
right: $space-normal; right: $space-normal;
top: $space-normal; top: $space-normal;
@include flex;
flex-direction: column;
.unread { .unread {
$unread-size: $space-two - $space-micro; $unread-size: $space-two - $space-micro;
display: none;
height: $unread-size;
min-width: $unread-size;
background: darken($success-color, 3%);
text-align: center;
padding: 0 $space-smaller;
line-height: $unread-size;
color: $color-white;
font-weight: $font-weight-medium;
font-size: $font-size-mini;
margin-left: auto;
@include round-corner; @include round-corner;
background: darken($success-color, 3%);
color: $color-white;
display: none;
font-size: $font-size-mini;
font-weight: $font-weight-medium;
height: $unread-size;
line-height: $unread-size;
margin-left: auto;
margin-top: $space-smaller; margin-top: $space-smaller;
min-width: $unread-size;
padding: 0 $space-smaller;
text-align: center;
} }
.timestamp { .timestamp {
font-size: $font-size-mini;
color: $dark-gray; color: $dark-gray;
line-height: $space-normal;
font-weight: $font-weight-normal;
font-size: $font-size-micro; font-size: $font-size-micro;
font-weight: $font-weight-normal;
line-height: $space-normal;
margin-left: auto; margin-left: auto;
} }
} }

View file

@ -1,7 +1,108 @@
@mixin bubble-with-tyes {
@include padding($space-smaller $space-one);
@include margin($zero);
background: $color-primary-light;
border-radius: $space-small;
color: $color-heading;
font-size: $font-size-small;
position: relative;
.icon {
bottom: $space-smaller;
position: absolute;
right: $space-small;
}
.message-text__wrap {
position: relative;
}
.message-text {
&::after {
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
display: inline;
}
}
.audio {
.time {
margin-top: -$space-two;
}
}
.image {
@include flex;
align-items: flex-end;
justify-content: center;
text-align: center;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
margin-left: -$space-large;
white-space: nowrap;
}
.modal-image {
max-height: 80%;
max-width: 80%;
}
}
.map {
@include flex;
flex-direction: column;
text-align: right;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
@include padding($space-small);
margin-left: -$space-smaller;
margin-top: -$space-two;
white-space: nowrap;
}
.locname {
font-weight: $font-weight-medium;
padding: $space-smaller;
}
}
}
.conversations-sidebar { .conversations-sidebar {
@include flex; @include flex;
flex-direction: column; flex-direction: column;
.load-more-conversations {
color: $color-woot;
cursor: pointer;
font-size: $font-size-small;
padding: $space-normal;
&:hover {
background: $color-background;
}
}
.end-of-list-text {
font-style: italic;
padding: $space-normal;
}
.conversations-list {
@include flex-weight(1);
@include scroll-on-hover;
}
.chat-list__top { .chat-list__top {
@include flex; @include flex;
@include padding($space-normal $zero $space-small $zero); @include padding($space-normal $zero $space-small $zero);
@ -28,10 +129,7 @@
} }
} }
.conversations-list {
@include flex-weight(1);
@include scroll-on-hover;
}
.content-box { .content-box {
text-align: center; text-align: center;
@ -47,16 +145,19 @@
@include background-gray; @include background-gray;
@include margin(0); @include margin(0);
@include border-normal-left; @include border-normal-left;
.current-chat { .current-chat {
@include flex; @include flex;
@include full-height; @include full-height;
flex-direction: column;
@include flex-align(center, middle); @include flex-align(center, middle);
flex-direction: column;
div { div {
@include flex; @include flex;
@include full-height; @include full-height;
flex-direction: column;
@include flex-align(center, middle); @include flex-align(center, middle);
flex-direction: column;
img { img {
@include margin($space-normal); @include margin($space-normal);
width: 10rem; width: 10rem;
@ -73,19 +174,20 @@
.conv-empty-state { .conv-empty-state {
@include flex; @include flex;
@include full-height; @include full-height;
flex-direction: column;
@include flex-align(center, middle); @include flex-align(center, middle);
flex-direction: column;
} }
} }
.conversation-panel { .conversation-panel {
@include flex; @include flex;
@include flex-weight(1); @include flex-weight(1);
flex-direction: column;
@include margin($zero); @include margin($zero);
flex-direction: column;
// Firefox flexbox fix // Firefox flexbox fix
height: 100%; height: 100%;
overflow-y: scroll; margin-bottom: $space-small;
overflow-y: auto;
>li { >li {
@include flex; @include flex;
@ -114,6 +216,7 @@
} }
.bubble { .bubble {
@include bubble-with-tyes;
max-width: 50rem; max-width: 50rem;
text-align: left; text-align: left;
word-wrap: break-word; word-wrap: break-word;
@ -147,7 +250,7 @@
@include flex-align(right, null); @include flex-align(right, null);
.wrap { .wrap {
margin-right: $space-small; margin-right: $space-normal;
text-align: right; text-align: right;
} }
@ -205,6 +308,7 @@
@include padding($space-smaller $space-normal); @include padding($space-smaller $space-normal);
@include flex-align($x: center, $y: null); @include flex-align($x: center, $y: null);
background: lighten($warning-color, 32%); background: lighten($warning-color, 32%);
border: 1px solid lighten($warning-color, 26%);
border-radius: $space-smaller; border-radius: $space-smaller;
font-size: $font-size-small; font-size: $font-size-small;
@ -237,87 +341,6 @@
} }
} }
.bubble {
@include padding($space-smaller $space-one);
@include margin($zero);
background: #c7e3ff;
border-radius: $space-small;
box-shadow: 0 .5px .5px rgba(0, 0, 0, .05);
color: $color-heading;
font-size: $font-size-small;
position: relative;
.icon {
bottom: $space-smaller;
position: absolute;
right: $space-small;
}
.message-text__wrap {
position: relative;
}
.message-text {
&::after {
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
display: inline;
}
}
.audio {
.time {
margin-top: -$space-two;
}
}
.image {
@include flex;
justify-content: center;
align-items: flex-end;
text-align: center;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
margin-left: -$space-large;
white-space: nowrap;
}
.modal-image {
max-height: 80%;
max-width: 80%;
}
}
.map {
@include flex;
flex-direction: column;
text-align: right;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
@include padding($space-small);
margin-left: -$space-smaller;
margin-top: -$space-two;
white-space: nowrap;
}
.locname {
font-weight: $font-weight-medium;
padding: $space-smaller;
}
}
}
.time { .time {
bottom: -$space-micro; bottom: -$space-micro;
color: $color-gray; color: $color-gray;

View file

@ -23,7 +23,7 @@
@include padding($space-small); @include padding($space-small);
box-sizing: border-box; box-sizing: border-box;
height: 180px; height: 180px;
overflow-y: scroll; overflow-y: auto;
.emoji { .emoji {
border-radius: 4px; border-radius: 4px;

View file

@ -5,7 +5,7 @@
// Outside login wrapper // Outside login wrapper
.login { .login {
@include full-height; @include full-height;
overflow-y: scroll; overflow-y: auto;
padding-top: $space-larger * 1.2; padding-top: $space-larger * 1.2;
.login__hero { .login__hero {

View file

@ -40,7 +40,7 @@
background-color: $color-white; background-color: $color-white;
border-radius: $space-small; border-radius: $space-small;
max-height: 100%; max-height: 100%;
overflow: scroll; overflow: auto;
position: relative; position: relative;
width: 60rem; width: 60rem;

View file

@ -1,10 +1,10 @@
.reply-box { .reply-box {
@include elegant-card;
border-bottom: 0;
margin: $space-normal; margin: $space-normal;
margin-top: 0; margin-top: 0;
border-bottom: 0;
@include elegant-card;
transition: height 2s $ease-in-out-cubic;
max-height: $space-jumbo * 2; max-height: $space-jumbo * 2;
transition: height 2s $ease-in-out-cubic;
.reply-box__top { .reply-box__top {
@include flex; @include flex;
@ -12,28 +12,27 @@
@include padding($space-one $space-normal); @include padding($space-one $space-normal);
@include background-white; @include background-white;
@include margin(0); @include margin(0);
position: relative;
border-top-left-radius: $space-small; border-top-left-radius: $space-small;
border-top-right-radius: $space-small; border-top-right-radius: $space-small;
position: relative;
.canned { .canned {
@include elegant-card; @include elegant-card;
z-index: 100; background: $color-white;
position: absolute;
background: #fff;
width: 24rem;
left: 0;
border-top: $space-small solid $color-white;
border-bottom: $space-small solid $color-white; border-bottom: $space-small solid $color-white;
max-height: 14rem; border-top: $space-small solid $color-white;
overflow: scroll; left: 0;
.active { max-height: 14rem;
a { overflow: auto;
position: absolute;
width: 24rem;
z-index: 100;
.active a {
background: $color-woot; background: $color-woot;
} }
} }
}
&.is-active { &.is-active {
border-bottom-left-radius: $space-small; border-bottom-left-radius: $space-small;
@ -49,10 +48,10 @@
} }
>.icon { >.icon {
font-size: $font-size-medium;
color: $medium-gray; color: $medium-gray;
margin-right: $space-small;
cursor: pointer; cursor: pointer;
font-size: $font-size-medium;
margin-right: $space-small;
&.active { &.active {
color: $color-woot; color: $color-woot;
@ -62,11 +61,11 @@
>textarea { >textarea {
@include ghost-input(); @include ghost-input();
@include margin(0); @include margin(0);
resize: none;
background: transparent; background: transparent;
// Override min-height : 50px in foundation // Override min-height : 50px in foundation
// //
min-height: 1rem; min-height: 1rem;
resize: none;
} }
} }
@ -80,19 +79,30 @@
.tabs { .tabs {
border: 0; border: 0;
padding: 0;
flex: 1; flex: 1;
padding: 0;
.tabs-title { .tabs-title {
margin: 0; margin: 0;
transition: background .2s $ease-in-out-cubic; transition: all .2s $ease-in-out-cubic;
transition: color .2s $ease-in-out-cubic; transition-property: color, background;
a { a {
font-weight: $font-weight-medium;
padding: $space-one $space-two; padding: $space-one $space-two;
} }
&:first-child { &.is-private.is-active {
background: lighten($warning-color, 38%);
a {
border-bottom-color: darken($warning-color, 15%);
color: darken($warning-color, 15%);
}
}
}
.tabs-title:first-child {
border-bottom-left-radius: $space-small; border-bottom-left-radius: $space-small;
&.is-active { &.is-active {
@ -105,23 +115,11 @@
} }
} }
&.is-private {
&.is-active {
background: lighten($warning-color, 38%);
a {
border-bottom-color: darken($warning-color, 15%);
color: darken($warning-color, 15%);
}
}
}
}
.is-active { .is-active {
@include background-white; @include background-white;
margin-top: -1px;
@include border-light-left; @include border-light-left;
@include border-light-right; @include border-light-right;
margin-top: -1px;
} }
.message-length { .message-length {
@ -138,11 +136,11 @@
} }
.send-button { .send-button {
height: 3.6rem;
border-bottom-right-radius: $space-small; border-bottom-right-radius: $space-small;
padding-top: $space-small; height: 3.6rem;
padding-right: $space-two;
padding-left: $space-two; padding-left: $space-two;
padding-right: $space-two;
padding-top: $space-small;
.icon { .icon {
margin-left: $space-small; margin-left: $space-small;

View file

@ -1,7 +1,7 @@
.side-menu { .side-menu {
i { i {
min-width: $space-two;
margin-right: $space-smaller; margin-right: $space-smaller;
min-width: $space-two;
} }
} }
@ -27,6 +27,26 @@
border-radius: $space-smaller; border-radius: $space-smaller;
color: $color-gray; color: $color-gray;
font-size: $font-size-default; font-size: $font-size-default;
font-weight: $font-weight-medium;
}
.active a {
color: $color-woot;
}
}
.nested {
a {
font-size: $font-size-small;
margin-bottom: $space-micro;
margin-top: $space-micro;
>.inbox-icon {
display: inline-block;
margin-right: $space-micro;
min-width: $space-normal;
text-align: center;
}
} }
} }
} }
@ -36,17 +56,17 @@
@include flex; @include flex;
@include space-between-column; @include space-between-column;
@include padding($space-one $space-normal $space-one $space-one); @include padding($space-one $space-normal $space-one $space-one);
flex-direction: column;
@include border-normal-top; @include border-normal-top;
flex-direction: column;
position: relative; position: relative;
.dropdown-pane { .dropdown-pane {
@include elegant-card; @include elegant-card;
@include border-light; @include border-light;
display: block;
left: 18%; left: 18%;
top: -110%; top: -110%;
visibility: visible; visibility: visible;
display: block;
width: 80%; width: 80%;
z-index: 999; z-index: 999;
@ -92,10 +112,10 @@
} }
.current-user { .current-user {
align-items: center;
@include flex; @include flex;
flex-direction: row; align-items: center;
cursor: pointer; cursor: pointer;
flex-direction: row;
.current-user--data { .current-user--data {
@include flex; @include flex;
@ -105,7 +125,7 @@
font-size: $font-size-small; font-size: $font-size-small;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
line-height: 1; line-height: 1;
margin-bottom: $zero; margin-bottom: $space-smaller;
margin-left: $space-one; margin-left: $space-one;
margin-top: $space-micro; margin-top: $space-micro;
} }

View file

@ -13,6 +13,7 @@
.tabs-title { .tabs-title {
a { a {
font-size: $font-size-default; font-size: $font-size-default;
font-weight: $font-weight-medium;
padding-bottom: $space-slab; padding-bottom: $space-slab;
padding-top: $space-slab; padding-top: $space-slab;
} }

View file

@ -3,40 +3,52 @@
<div class="chat-list__top"> <div class="chat-list__top">
<h1 class="page-title"> <h1 class="page-title">
<woot-sidemenu-icon /> <woot-sidemenu-icon />
{{ inbox.name || pageTitle }} {{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }}
</h1> </h1>
<chat-filter @statusFilterChange="getDataForStatusTab" /> <chat-filter @statusFilterChange="updateStatusType" />
</div> </div>
<chat-type-tabs <chat-type-tabs
:items="assigneeTabItems" :items="assigneeTabItems"
:active-tab-index="activeAssigneeTab" :active-tab="activeAssigneeTab"
class="tab--chat-type" class="tab--chat-type"
@chatTabChange="getDataForTab" @chatTabChange="updateAssigneeTab"
/> />
<p <p v-if="!chatListLoading && !getChatsForTab().length" class="content-box">
v-if="!chatListLoading && !getChatsForTab(activeStatus).length"
class="content-box"
>
{{ $t('CHAT_LIST.LIST.404') }} {{ $t('CHAT_LIST.LIST.404') }}
</p> </p>
<div v-if="chatListLoading" class="text-center"> <div class="conversations-list">
<span class="spinner message"></span>
</div>
<transition-group
name="conversations-list"
tag="div"
class="conversations-list"
>
<conversation-card <conversation-card
v-for="chat in getChatsForTab(activeStatus)" v-for="chat in getChatsForTab()"
:key="chat.id" :key="chat.id"
:chat="chat" :chat="chat"
/> />
</transition-group>
<div v-if="chatListLoading" class="text-center">
<span class="spinner"></span>
</div>
<div
v-if="!hasCurrentPageEndReached && !chatListLoading"
class="text-center load-more-conversations"
@click="fetchConversations"
>
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
</div>
<p
v-if="
getChatsForTab().length &&
hasCurrentPageEndReached &&
!chatListLoading
"
class="text-center text-muted end-of-list-text"
>
{{ $t('CHAT_LIST.EOF') }}
</p>
</div>
</div> </div>
</template> </template>
@ -59,11 +71,11 @@ export default {
ChatFilter, ChatFilter,
}, },
mixins: [timeMixin, conversationMixin], mixins: [timeMixin, conversationMixin],
props: ['conversationInbox', 'pageTitle'], props: ['conversationInbox'],
data() { data() {
return { return {
activeAssigneeTab: 0, activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: 0, activeStatus: wootConstants.STATUS_TYPE.OPEN,
}; };
}, },
computed: { computed: {
@ -78,66 +90,69 @@ export default {
convStats: 'getConvTabStats', convStats: 'getConvTabStats',
}), }),
assigneeTabItems() { assigneeTabItems() {
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map((item, index) => ({ return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({
id: index, key: item.KEY,
name: item.NAME, name: item.NAME,
count: this.convStats[item.KEY] || 0, count: this.convStats[item.COUNT_KEY] || 0,
})); }));
}, },
inbox() { inbox() {
return this.$store.getters['inboxes/getInbox'](this.activeInbox); return this.$store.getters['inboxes/getInbox'](this.activeInbox);
}, },
getToggleStatus() { currentPage() {
if (this.toggleType) { return this.$store.getters['conversationPage/getCurrentPage'](
return 'Open'; this.activeAssigneeTab
} );
return 'Resolved'; },
hasCurrentPageEndReached() {
return this.$store.getters['conversationPage/getHasEndReached'](
this.activeAssigneeTab
);
},
},
watch: {
conversationInbox() {
this.resetAndFetchData();
}, },
}, },
mounted() { mounted() {
this.$watch('$store.state.route', () => { this.$store.dispatch('setChatFilter', this.activeStatus);
if (this.$store.state.route.name !== 'inbox_conversation') { this.resetAndFetchData();
this.$store.dispatch('emptyAllConversations');
this.fetchData();
}
});
this.$store.dispatch('emptyAllConversations');
this.fetchData();
this.$store.dispatch('agents/get'); this.$store.dispatch('agents/get');
}, },
methods: { methods: {
fetchData() { resetAndFetchData() {
if (this.chatLists.length === 0) { this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.fetchConversations(); this.fetchConversations();
}
}, },
fetchConversations() { fetchConversations() {
this.$store.dispatch('fetchAllConversations', { this.$store.dispatch('fetchAllConversations', {
inboxId: this.conversationInbox ? this.conversationInbox : undefined, inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab, assigneeType: this.activeAssigneeTab,
status: this.activeStatus ? 'resolved' : 'open', status: this.activeStatus,
page: this.currentPage + 1,
}); });
}, },
getDataForTab(index) { updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== index) { if (this.activeAssigneeTab !== selectedTab) {
this.activeAssigneeTab = index; this.activeAssigneeTab = selectedTab;
if (!this.currentPage) {
this.fetchConversations(); this.fetchConversations();
} }
}
}, },
getDataForStatusTab(index) { updateStatusType(index) {
if (this.activeStatus !== index) { if (this.activeStatus !== index) {
this.activeStatus = index; this.activeStatus = index;
this.fetchConversations(); this.resetAndFetchData();
} }
}, },
getChatsForTab() { getChatsForTab() {
let copyList = []; let copyList = [];
if (this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.MINE) { if (this.activeAssigneeTab === 'me') {
copyList = this.mineChatsList.slice(); copyList = this.mineChatsList.slice();
} else if ( } else if (this.activeAssigneeTab === 'unassigned') {
this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.UNASSIGNED
) {
copyList = this.unAssignedChatsList.slice(); copyList = this.unAssignedChatsList.slice();
} else { } else {
copyList = this.allChatList.slice(); copyList = this.allChatList.slice();

View file

@ -1,6 +1,11 @@
<template> <template>
<transition name="modal-fade"> <transition name="modal-fade">
<div v-if="show" class="modal-mask" transition="modal" @click="close"> <div
v-if="show"
class="modal-mask"
transition="modal"
@click="onBackDropClick"
>
<i class="ion-android-close modal--close" @click="close"></i> <i class="ion-android-close modal--close" @click="close"></i>
<div class="modal-container" :class="className" @click.stop> <div class="modal-container" :class="className" @click.stop>
<slot /> <slot />
@ -12,9 +17,19 @@
<script> <script>
export default { export default {
props: { props: {
closeOnBackdropClick: {
type: Boolean,
default: true,
},
show: Boolean, show: Boolean,
onClose: Function, onClose: {
className: String, type: Function,
required: true,
},
className: {
type: String,
default: '',
},
}, },
mounted() { mounted() {
document.addEventListener('keydown', e => { document.addEventListener('keydown', e => {
@ -27,6 +42,11 @@ export default {
close() { close() {
this.onClose(); this.onClose();
}, },
onBackDropClick() {
if (this.closeOnBackdropClick) {
this.onClose();
}
},
}, },
}; };
</script> </script>

View file

@ -16,8 +16,12 @@
/* global bus */ /* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner';
import wootConstants from '../../constants';
export default { export default {
components: {
Spinner,
},
props: ['conversationId'], props: ['conversationId'],
data() { data() {
return { return {
@ -29,19 +33,23 @@ export default {
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
}), }),
currentStatus() { currentStatus() {
const ButtonName = this.currentChat.status === 0 ? 'Resolve' : 'Reopen'; const ButtonName =
this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
? this.$t('CONVERSATION.HEADER.RESOLVE_ACTION')
: this.$t('CONVERSATION.HEADER.REOPEN_ACTION');
return ButtonName; return ButtonName;
}, },
buttonClass() { buttonClass() {
return this.currentChat.status === 0 ? 'success' : 'warning'; return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
? 'success'
: 'warning';
}, },
buttonIconClass() { buttonIconClass() {
return this.currentChat.status === 0 ? 'ion-checkmark' : 'ion-refresh'; return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
? 'ion-checkmark'
: 'ion-refresh';
}, },
}, },
components: {
Spinner,
},
methods: { methods: {
toggleStatus() { toggleStatus() {
this.isLoading = true; this.isLoading = true;

View file

@ -42,12 +42,21 @@
class="dropdown-pane top" class="dropdown-pane top"
> >
<ul class="vertical dropdown menu"> <ul class="vertical dropdown menu">
<li><a href="#" @click.prevent="logout()">Logout</a></li> <li>
<router-link to="/app/profile/settings">
{{ $t('SIDEBAR.PROFILE_SETTINGS') }}
</router-link>
</li>
<li>
<a href="#" @click.prevent="logout()">
{{ $t('SIDEBAR.LOGOUT') }}
</a>
</li>
</ul> </ul>
</div> </div>
</transition> </transition>
<div class="current-user" @click.prevent="showOptions()"> <div class="current-user" @click.prevent="showOptions()">
<thumbnail :src="gravatarUrl()" :username="currentUser.name" /> <thumbnail :src="currentUser.avatar_url" :username="currentUser.name" />
<div class="current-user--data"> <div class="current-user--data">
<h3 class="current-user--name"> <h3 class="current-user--name">
{{ currentUser.name }} {{ currentUser.name }}
@ -65,7 +74,6 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import md5 from 'md5';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from '../../mixins/isAdmin'; import adminMixin from '../../mixins/isAdmin';
@ -99,6 +107,7 @@ export default {
daysLeft: 'getTrialLeft', daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription', subscriptionData: 'getSubscription',
inboxes: 'inboxes/getInboxes', inboxes: 'inboxes/getInboxes',
currentUser: 'getCurrentUser',
}), }),
accessibleMenuItems() { accessibleMenuItems() {
// get all keys in menuGroup // get all keys in menuGroup
@ -141,12 +150,10 @@ export default {
id: inbox.id, id: inbox.id,
label: inbox.name, label: inbox.name,
toState: frontendURL(`inbox/${inbox.id}`), toState: frontendURL(`inbox/${inbox.id}`),
type: inbox.channel_type,
})), })),
}; };
}, },
currentUser() {
return Auth.getCurrentUser();
},
dashboardPath() { dashboardPath() {
return frontendURL('dashboard'); return frontendURL('dashboard');
}, },
@ -174,10 +181,6 @@ export default {
this.$store.dispatch('inboxes/get'); this.$store.dispatch('inboxes/get');
}, },
methods: { methods: {
gravatarUrl() {
const hash = md5(this.currentUser.email);
return `${window.WootConstants.GRAVATAR_URL}${hash}?default=404`;
},
filterBillingRoutes(menuItems) { filterBillingRoutes(menuItems) {
return menuItems.filter( return menuItems.filter(
menuItem => !menuItem.toState.includes('billing') menuItem => !menuItem.toState.includes('billing')
@ -185,6 +188,9 @@ export default {
}, },
filterMenuItemsByRole(menuItems) { filterMenuItemsByRole(menuItems) {
const { role } = this.currentUser; const { role } = this.currentUser;
if (!role) {
return [];
}
return menuItems.filter( return menuItems.filter(
menuItem => menuItem =>
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1 window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1

View file

@ -24,11 +24,17 @@
v-for="child in menuItem.children" v-for="child in menuItem.children"
:key="child.id" :key="child.id"
active-class="active flex-container" active-class="active flex-container"
:class="computedInboxClass(child)"
tag="li" tag="li"
:to="child.toState" :to="child.toState"
> >
<a>{{ child.label }}</a> <a href="#">
<i
v-if="computedInboxClass(child)"
class="inbox-icon"
:class="computedInboxClass(child)"
></i>
{{ child.label }}
</a>
</router-link> </router-link>
</ul> </ul>
</router-link> </router-link>
@ -41,6 +47,27 @@ import { mapGetters } from 'vuex';
import router from '../../routes'; import router from '../../routes';
import auth from '../../api/auth'; import auth from '../../api/auth';
const INBOX_TYPES = {
WEB: 'Channel::WebWidget',
FB: 'Channel::FacebookPage',
TWITTER: 'Channel::TwitterProfile',
};
const getInboxClassByType = type => {
switch (type) {
case INBOX_TYPES.WEB:
return 'ion-earth';
case INBOX_TYPES.FB:
return 'ion-social-facebook';
case INBOX_TYPES.TWITTER:
return 'ion-social-twitter';
default:
return '';
}
};
export default { export default {
props: { props: {
menuItem: { menuItem: {
@ -75,10 +102,9 @@ export default {
}, },
methods: { methods: {
computedInboxClass(child) { computedInboxClass(child) {
if (parseInt(this.activeInbox, 10) === child.channel_id) { const { type } = child;
return 'active flex-container'; const classByType = getInboxClassByType(type);
} return classByType;
return ' ';
}, },
newLinkClick() { newLinkClick() {
router.push({ name: 'settings_inbox_new', params: { page: 'new' } }); router.push({ name: 'settings_inbox_new', params: { page: 'new' } });

View file

@ -14,15 +14,19 @@ export default {
props: { props: {
username: { username: {
type: String, type: String,
default: '',
}, },
backgroundColor: { backgroundColor: {
type: String, type: String,
default: 'white',
}, },
color: { color: {
type: String, type: String,
default: '#1f93ff',
}, },
customStyle: { customStyle: {
type: Object, type: Object,
default: undefined,
}, },
size: { size: {
type: Number, type: Number,
@ -30,6 +34,7 @@ export default {
}, },
src: { src: {
type: String, type: String,
default: '',
}, },
rounded: { rounded: {
type: Boolean, type: Boolean,
@ -72,7 +77,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.avatar-container { .avatar-container {
display: flex; display: flex;
font-weight: bold; font-weight: 500;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center; text-align: center;

View file

@ -39,7 +39,7 @@ export default {
}, },
methods: { methods: {
isActive(channel) { isActive(channel) {
return ['facebook', 'website'].includes(channel); return ['facebook', 'website', 'twitter'].includes(channel);
}, },
onItemClick() { onItemClick() {
if (this.isActive(this.channel)) { if (this.isActive(this.channel)) {

View file

@ -1,15 +1,15 @@
<template> <template>
<woot-tabs :index="tabsIndex" @change="onTabChange"> <woot-tabs :index="activeTabIndex" @change="onTabChange">
<woot-tabs-item <woot-tabs-item
v-for="item in items" v-for="item in items"
:key="item.name" :key="item.key"
:name="item.name" :name="item.name"
:count="item.count" :count="item.count"
/> />
</woot-tabs> </woot-tabs>
</template> </template>
<script> <script>
/* eslint no-console: 0 */ import wootConstants from '../../constants';
export default { export default {
props: { props: {
@ -17,24 +17,25 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
activeTabIndex: { activeTab: {
type: Number, type: String,
default: 0, default: wootConstants.ASSIGNEE_TYPE.ME,
}, },
}, },
data() { data() {
return { return {
tabsIndex: 0, tabsIndex: wootConstants.ASSIGNEE_TYPE.ME,
}; };
}, },
created() { computed: {
this.tabsIndex = this.activeTabIndex; activeTabIndex() {
return this.items.findIndex(item => item.key === this.activeTab);
},
}, },
methods: { methods: {
onTabChange(selectedTabIndex) { onTabChange(selectedTabIndex) {
if (selectedTabIndex !== this.tabsIndex) { if (this.items[selectedTabIndex].key !== this.activeTab) {
this.$emit('chatTabChange', selectedTabIndex); this.$emit('chatTabChange', this.items[selectedTabIndex].key);
this.tabsIndex = selectedTabIndex;
} }
}, },
}, },

View file

@ -4,19 +4,19 @@
v-if="!imgError && Boolean(src)" v-if="!imgError && Boolean(src)"
id="image" id="image"
:src="src" :src="src"
class="user-thumbnail" :class="thumbnailClass"
@error="onImgError()" @error="onImgError()"
/> />
<Avatar <Avatar
v-else v-else
:username="username" :username="username"
class="user-thumbnail" :class="thumbnailClass"
background-color="#1f93ff" background-color="#1f93ff"
color="white" color="white"
:size="avatarSize" :size="avatarSize"
/> />
<img <img
v-if="badge === 'Channel::FacebookPage' && status !== ''" v-if="badge === 'Channel::FacebookPage'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
@ -28,7 +28,7 @@
:style="statusStyle" :style="statusStyle"
></div> ></div>
<img <img
v-if="badge === 'Channel::TwitterProfile' && status !== ''" v-if="badge === 'Channel::TwitterProfile'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
@ -71,6 +71,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
hasBorder: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@ -89,6 +93,10 @@ export default {
const statusSize = `${this.avatarSize / 4}px`; const statusSize = `${this.avatarSize / 4}px`;
return { width: statusSize, height: statusSize }; return { width: statusSize, height: statusSize };
}, },
thumbnailClass() {
const classname = this.hasBorder ? 'border' : '';
return `user-thumbnail ${classname}`;
},
}, },
methods: { methods: {
onImgError() { onImgError() {
@ -111,6 +119,11 @@ export default {
border-radius: 50%; border-radius: 50%;
height: 100%; height: 100%;
width: 100%; width: 100%;
box-sizing: border-box;
&.border {
border: 1px solid white;
}
} }
.source-badge { .source-badge {

View file

@ -1,5 +1,5 @@
<template> <template>
<select v-model="activeIndex" class="status--filter" @change="onTabChange()"> <select v-model="activeStatus" class="status--filter" @change="onTabChange()">
<option <option
v-for="item in $t('CHAT_LIST.CHAT_STATUS_ITEMS')" v-for="item in $t('CHAT_LIST.CHAT_STATUS_ITEMS')"
:key="item['VALUE']" :key="item['VALUE']"
@ -11,15 +11,16 @@
</template> </template>
<script> <script>
import wootConstants from '../../../constants';
export default { export default {
data: () => ({ data: () => ({
activeIndex: 0, activeStatus: wootConstants.STATUS_TYPE.OPEN,
}), }),
mounted() {},
methods: { methods: {
onTabChange() { onTabChange() {
this.$store.dispatch('setChatFilter', this.activeIndex); this.$store.dispatch('setChatFilter', this.activeStatus);
this.$emit('statusFilterChange', this.activeIndex); this.$emit('statusFilterChange', this.activeStatus);
}, },
}, },
}; };

View file

@ -46,7 +46,7 @@ import getEmojiSVG from '../emoji/utils';
import conversationMixin from '../../../mixins/conversations'; import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time'; import timeMixin from '../../../mixins/time';
import router from '../../../routes'; import router from '../../../routes';
import { frontendURL } from '../../../helper/URLHelper'; import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
export default { export default {
components: { components: {
@ -95,9 +95,9 @@ export default {
methods: { methods: {
cardClick(chat) { cardClick(chat) {
router.push({ const { activeInbox } = this;
path: frontendURL(`conversations/${chat.id}`), const path = conversationUrl(activeInbox, chat.id);
}); router.push({ path: frontendURL(path) });
}, },
extractMessageText(chatItem) { extractMessageText(chatItem) {
if (chatItem.content) { if (chatItem.content) {

View file

@ -93,7 +93,11 @@ export default {
]; ];
}, },
viewProfileButtonLabel() { viewProfileButtonLabel() {
return `${this.isContactPanelOpen ? 'Hide' : 'View'} Profile`; return `${
this.isContactPanelOpen
? this.$t('CONVERSATION.HEADER.CLOSE')
: this.$t('CONVERSATION.HEADER.OPEN')
} ${this.$t('CONVERSATION.HEADER.DETAILS')}`;
}, },
}, },

View file

@ -22,7 +22,6 @@
:label="data.attachment.fallback_title" :label="data.attachment.fallback_title"
:readable-time="readableTime" :readable-time="readableTime"
/> />
<i v-if="data.message_type === 2" class="icon ion-person" />
<bubble-text <bubble-text
v-if="data.content" v-if="data.content"
:message="message" :message="message"

View file

@ -4,9 +4,13 @@ export default {
return `${this.APP_BASE_URL}/`; return `${this.APP_BASE_URL}/`;
}, },
GRAVATAR_URL: 'https://www.gravatar.com/avatar/', GRAVATAR_URL: 'https://www.gravatar.com/avatar/',
ASSIGNEE_TYPE_SLUG: { ASSIGNEE_TYPE: {
MINE: 0, ME: 'me',
UNASSIGNED: 1, UNASSIGNED: 'unassigned',
OPEN: 0, ALL: 'all',
},
STATUS_TYPE: {
OPEN: 'open',
RESOLVED: 'resolved',
}, },
}; };

View file

@ -4,3 +4,10 @@ export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${queryString.stringify(params)}` : ''; const stringifiedParams = params ? `?${queryString.stringify(params)}` : '';
return `/app/${path}${stringifiedParams}`; return `/app/${path}${stringifiedParams}`;
}; };
export const conversationUrl = (activeInbox, id) => {
const path = activeInbox
? `inbox/${activeInbox}/conversations/${id}`
: `conversations/${id}`;
return path;
};

View file

@ -0,0 +1,21 @@
import { frontendURL, conversationUrl } from '../URLHelper';
describe('#URL Helpers', () => {
describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => {
expect(conversationUrl(undefined, 1)).toBe('conversations/1');
});
it('should return ibox conversation URL if activeInbox is not nil', () => {
expect(conversationUrl(2, 1)).toBe('inbox/2/conversations/1');
});
});
describe('frontendURL', () => {
it('should return url without params if params passed is nil', () => {
expect(frontendURL('main', null)).toBe('/app/main');
});
it('should return url without params if params passed is not nil', () => {
expect(frontendURL('main', { ping: 'pong' })).toBe('/app/main?ping=pong');
});
});
});

View file

@ -6,8 +6,11 @@ export default {
'home', 'home',
'inbox_dashboard', 'inbox_dashboard',
'inbox_conversation', 'inbox_conversation',
'conversation_through_inbox',
'settings_account_reports', 'settings_account_reports',
'billing_deactivated', 'billing_deactivated',
'profile_settings',
'profile_settings_index',
], ],
menuItems: { menuItems: {
assignedToMe: { assignedToMe: {
@ -49,6 +52,8 @@ export default {
'settings_inboxes_add_agents', 'settings_inboxes_add_agents',
'settings_inbox_finish', 'settings_inbox_finish',
'billing', 'billing',
'settings_integrations',
'settings_integrations_webhook',
], ],
menuItems: { menuItems: {
back: { back: {
@ -86,12 +91,12 @@ export default {
toState: frontendURL('settings/billing'), toState: frontendURL('settings/billing'),
toStateName: 'billing', toStateName: 'billing',
}, },
account: { settings_integrations: {
icon: 'ion-beer', icon: 'ion-flash',
label: 'Account Settings', label: 'Integrations',
hasSubMenu: false, hasSubMenu: false,
toState: frontendURL('settings/account'), toState: frontendURL('settings/integrations'),
toStateName: 'account', toStateName: 'settings_integrations',
}, },
}, },
}, },

View file

@ -45,6 +45,7 @@
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Agent added successfully", "SUCCESS_MESSAGE": "Agent added successfully",
"EXIST_MESSAGE": "Agent email already in use, Please try another email address",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
} }
}, },
@ -58,7 +59,7 @@
"TITLE": "Confirm Deletion", "TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ", "MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ", "YES": "Yes, Delete ",
"NO": "No, Keep " "NO": "No, Keep it "
} }
}, },
"EDIT": { "EDIT": {

View file

@ -67,7 +67,7 @@
"TITLE": "Confirm Deletion", "TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ", "MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ", "YES": "Yes, Delete ",
"NO": "No, Keep " "NO": "No, Keep it "
} }
} }
} }

View file

@ -1,6 +1,8 @@
{ {
"CHAT_LIST": { "CHAT_LIST": {
"LOADING": "Fetching conversations", "LOADING": "Fetching conversations",
"LOAD_MORE_CONVERSATIONS": "Load more conversations...",
"EOF": "You have reached the end of the list",
"LIST": { "LIST": {
"404": "There are no active conversations in this group." "404": "There are no active conversations in this group."
}, },
@ -14,20 +16,14 @@
], ],
"ASSIGNEE_TYPE_TABS": [ "ASSIGNEE_TYPE_TABS": [
{ "NAME": "Mine", "KEY": "mineCount"}, { "NAME": "Mine", "KEY": "me", "COUNT_KEY": "mineCount" },
{ "NAME": "Unassigned", "KEY": "unAssignedCount"}, { "NAME": "Unassigned", "KEY": "unassigned", "COUNT_KEY": "unAssignedCount"},
{ "NAME": "All", "KEY": "allCount"} { "NAME": "All", "KEY": "all", "COUNT_KEY": "allCount" }
], ],
"ASSIGNEE_TYPE_SLUG": {
"MINE": 0,
"UNASSIGNED": 1,
"ALL": 2
},
"CHAT_STATUS_ITEMS": [ "CHAT_STATUS_ITEMS": [
{ "TEXT": "Open", "VALUE": 0 }, { "TEXT": "Open", "VALUE": "open" },
{ "TEXT": "Resolved", "VALUE": 1 } { "TEXT": "Resolved", "VALUE": "resolved" }
], ],
"ATTACHMENTS": { "ATTACHMENTS": {

View file

@ -1,5 +1,6 @@
{ {
"CONTACT_PANEL": { "CONTACT_PANEL": {
"CONVERSATION_TITLE": "Conversation Details",
"BROWSER": "Browser", "BROWSER": "Browser",
"OS": "Operating System", "OS": "Operating System",
"INITIATED_FROM": "Initiated from", "INITIATED_FROM": "Initiated from",
@ -9,8 +10,11 @@
"TITLE": "Previous Conversations" "TITLE": "Previous Conversations"
}, },
"LABELS": { "LABELS": {
"NO_RECORDS_FOUND": "There are no labels associated to this conversation.", "TITLE": "Conversation Labels",
"TITLE": "Labels" "UPDATE_BUTTON": "Update Labels",
"UPDATE_ERROR": "Couldn't update labels, try again.",
"TAG_PLACEHOLDER": "Add new label",
"PLACEHOLDER": "Search or add a label"
} }
} }
} }

View file

@ -10,7 +10,11 @@
"LOADING_INBOXES": "Loading inboxes", "LOADING_INBOXES": "Loading inboxes",
"LOADING_CONVERSATIONS": "Loading Conversations", "LOADING_CONVERSATIONS": "Loading Conversations",
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "Resolve" "RESOLVE_ACTION": "Resolve",
"REOPEN_ACTION": "Reopen",
"OPEN": "More",
"CLOSE": "Close",
"DETAILS": "details"
}, },
"FOOTER": { "FOOTER": {
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.", "MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",

View file

@ -15,6 +15,9 @@
"FB": { "FB": {
"HELP": "PS: By signing in, we only get access to your Page's messages. Your private messages can never be accessed by Chatwoot." "HELP": "PS: By signing in, we only get access to your Page's messages. Your private messages can never be accessed by Chatwoot."
}, },
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' "
},
"WEBSITE_CHANNEL": { "WEBSITE_CHANNEL": {
"TITLE": "Website channel", "TITLE": "Website channel",
"DESC": "Create a channel for your website and start supporting your customers via our website widget.", "DESC": "Create a channel for your website and start supporting your customers via our website widget.",
@ -34,11 +37,11 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "Channels", "TITLE": "Channels",
"DESC": "Currently we support website live chat widgets and Facebook Pages as platforms. We have more platforms like Twitter, Telegram and Line in the works, which will be out soon." "DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "Agents", "TITLE": "Agents",
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents whcih are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create." "DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents which are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create."
}, },
"DETAILS": { "DETAILS": {
"TITLE": "Inbox Details", "TITLE": "Inbox Details",
@ -71,7 +74,12 @@
"EDIT": { "EDIT": {
"API": { "API": {
"SUCCESS_MESSAGE": "Widget color updated successfully", "SUCCESS_MESSAGE": "Widget color updated successfully",
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Auto assignment updated successfully",
"ERROR_MESSAGE": "Could not update widget color. Please try again later." "ERROR_MESSAGE": "Could not update widget color. Please try again later."
},
"AUTO_ASSIGNMENT": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
} }
}, },
"DELETE": { "DELETE": {
@ -80,7 +88,7 @@
"TITLE": "Confirm Deletion", "TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ", "MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ", "YES": "Yes, Delete ",
"NO": "No, Keep " "NO": "No, Keep it "
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Inbox deleted successfully", "SUCCESS_MESSAGE": "Inbox deleted successfully",
@ -93,7 +101,9 @@
"MESSENGER_SUB_HEAD": "Place this button inside your body tag", "MESSENGER_SUB_HEAD": "Place this button inside your body tag",
"INBOX_AGENTS": "Agents", "INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox", "INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
"UPDATE": "Update" "UPDATE": "Update",
"AUTO_ASSIGNMENT": "Enable auto assignment",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of available agents on new conversations"
} }
} }
} }

View file

@ -10,7 +10,9 @@ import { default as _login } from './login.json';
import { default as _report } from './report.json'; import { default as _report } from './report.json';
import { default as _resetPassword } from './resetPassword.json'; import { default as _resetPassword } from './resetPassword.json';
import { default as _setNewPassword } from './setNewPassword.json'; import { default as _setNewPassword } from './setNewPassword.json';
import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json'; import { default as _signup } from './signup.json';
import { default as _integrations } from './integrations.json';
export default { export default {
..._agentMgmt, ..._agentMgmt,
@ -24,5 +26,7 @@ export default {
..._report, ..._report,
..._resetPassword, ..._resetPassword,
..._setNewPassword, ..._setNewPassword,
..._settings,
..._signup, ..._signup,
..._integrations,
}; };

View file

@ -0,0 +1,54 @@
{
"INTEGRATION_SETTINGS": {
"HEADER": "Integrations",
"WEBHOOK": {
"TITLE": "Webhook",
"CONFIGURE": "Configure",
"HEADER": "Webhook settings",
"HEADER_BTN_TXT": "Add new webhook",
"INTEGRATION_TXT": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks.",
"LOADING": "Fetching attached webhooks",
"SEARCH_404": "There are no items matching this query",
"SIDEBAR_TXT": "<p><b>Webhooks</b> </p> <p>Webhooks are HTTP callbacks which can be defined for every account. They are triggered by events like message creation in Chatwoot. You can create more than one webhook for this account. <br /><br /> For creating a <b>webhook</b>, click on the <b>Add new webhook</b> button. You can also remove any existing webhook by clicking on the Delete button.</p>",
"LIST": {
"404": "There are no webhooks configured for this account.",
"TITLE": "Manage webhooks",
"DESC": "Webhooks are predefined reply templates which can be used to quickly send out replies to tickets.",
"TABLE_HEADER": [
"Webhook endpoint",
"Actions"
]
},
"ADD": {
"CANCEL": "Cancel",
"TITLE": "Add new webhook",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
"FORM": {
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: https://example/api/webhook",
"ERROR": "Please enter a valid URL"
},
"SUBMIT": "Create webhook"
},
"API": {
"SUCCESS_MESSAGE": "Webhook added successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"API": {
"SUCCESS_MESSAGE": "Webhook deleted successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
},
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep it "
}
}
}
}
}

View file

@ -0,0 +1,58 @@
{
"PROFILE_SETTINGS": {
"LINK": "Profile Settings",
"TITLE": "Profile Settings",
"BTN_TEXT": "Update Profile",
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
"FORM": {
"AVATAR": "Profile Image",
"ERROR": "Please fix form errors",
"REMOVE_IMAGE": "Remove",
"UPLOAD_IMAGE": "Upload image",
"UPDATE_IMAGE": "Update image",
"PROFILE_SECTION" : {
"TITLE": "Profile",
"NOTE": "Your email address is your identity and is used to log in."
},
"PASSWORD_SECTION" : {
"TITLE": "Password",
"NOTE": "Updating your password would reset your logins in multiple devices."
},
"EMAIL_NOTIFICATIONS_SECTION" : {
"TITLE": "Email Notifications",
"NOTE": "Update your email notification preferences here",
"CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me",
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
"UPDATE_SUCCESS": "Your email notification preferences are updated successfully",
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"
},
"PROFILE_IMAGE":{
"LABEL": "Profile Image"
},
"NAME": {
"LABEL": "Your name",
"ERROR": "Please enter a valid name",
"PLACEHOLDER": "Please enter your name, this would be displayed in conversations"
},
"EMAIL": {
"LABEL": "Your email address",
"ERROR": "Please enter a valid email address",
"PLACEHOLDER": "Please enter your email address, this would be displayed in conversations"
},
"PASSWORD": {
"LABEL": "Password",
"ERROR": "Please enter a password of length 6 or more",
"PLACEHOLDER": "Please enter a new password"
},
"PASSWORD_CONFIRMATION": {
"LABEL": "Confirm new password",
"ERROR": "Confirm password should match the password",
"PLACEHOLDER": "Please re-enter your password"
}
}
},
"SIDEBAR": {
"PROFILE_SETTINGS": "Profile Settings",
"LOGOUT": "Logout"
}
}

View file

@ -1,7 +1,6 @@
{ {
"REGISTER": { "REGISTER": {
"TRY_WOOT": "Try Chatwoot free for 14 days", "TRY_WOOT": "Register an account",
"TRY_WOOT_SUB": "No credit card required. Cancel anytime.",
"TITLE": "Register", "TITLE": "Register",
"TERMS_ACCEPT": "By signing up, you agree to our <a href=\"https://www.chatwoot.com/terms\">T & C</a> and <a href=\"https://www.chatwoot.com/privacy-policy\">Privacy policy</a>", "TERMS_ACCEPT": "By signing up, you agree to our <a href=\"https://www.chatwoot.com/terms\">T & C</a> and <a href=\"https://www.chatwoot.com/privacy-policy\">Privacy policy</a>",
"ACCOUNT_NAME": { "ACCOUNT_NAME": {

View file

@ -0,0 +1,5 @@
{
"WEBHOOKS_SETTINGS": {
"HEADER": "Webhook Settings"
}
}

View file

@ -11,7 +11,8 @@ export default {
return m.messages.filter( return m.messages.filter(
chat => chat =>
chat.created_at * 1000 > m.agent_last_seen_at * 1000 && chat.created_at * 1000 > m.agent_last_seen_at * 1000 &&
(chat.message_type === 0 && chat.private !== true) chat.message_type === 0 &&
chat.private !== true
).length; ).length;
}, },
readMessages(m) { readMessages(m) {

View file

@ -1,23 +1,40 @@
<template> <template>
<form class="login-box medium-4 column align-self-middle" v-on:submit.prevent="login()"> <form
class="login-box medium-4 column align-self-middle"
@submit.prevent="login()"
>
<div class="column log-in-form"> <div class="column log-in-form">
<h4>{{ $t('SET_NEW_PASSWORD.TITLE') }}</h4> <h4>{{ $t('SET_NEW_PASSWORD.TITLE') }}</h4>
<label :class="{ 'error': $v.credentials.password.$error }"> <label :class="{ error: $v.credentials.password.$error }">
{{ $t('LOGIN.PASSWORD.LABEL') }} {{ $t('LOGIN.PASSWORD.LABEL') }}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')" v-model.trim="credentials.password" @input="$v.credentials.password.$touch"> <input
<span class="message" v-if="$v.credentials.password.$error"> v-model.trim="credentials.password"
type="password"
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
@input="$v.credentials.password.$touch"
/>
<span v-if="$v.credentials.password.$error" class="message">
{{ $t('SET_NEW_PASSWORD.PASSWORD.ERROR') }} {{ $t('SET_NEW_PASSWORD.PASSWORD.ERROR') }}
</span> </span>
</label> </label>
<label :class="{ 'error': $v.credentials.confirmPassword.$error }"> <label :class="{ error: $v.credentials.confirmPassword.$error }">
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL') }} {{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL') }}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')" v-model.trim="credentials.confirmPassword" @input="$v.credentials.confirmPassword.$touch"> <input
<span class="message" v-if="$v.credentials.confirmPassword.$error"> v-model.trim="credentials.confirmPassword"
type="password"
:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')"
@input="$v.credentials.confirmPassword.$touch"
/>
<span v-if="$v.credentials.confirmPassword.$error" class="message">
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR') }} {{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR') }}
</span> </span>
</label> </label>
<woot-submit-button <woot-submit-button
:disabled="$v.credentials.password.$invalid || $v.credentials.confirmPassword.$invalid || newPasswordAPI.showLoading" :disabled="
$v.credentials.password.$invalid ||
$v.credentials.confirmPassword.$invalid ||
newPasswordAPI.showLoading
"
:button-text="$t('SET_NEW_PASSWORD.SUBMIT')" :button-text="$t('SET_NEW_PASSWORD.SUBMIT')"
:loading="newPasswordAPI.showLoading" :loading="newPasswordAPI.showLoading"
button-class="expanded" button-class="expanded"
@ -99,7 +116,7 @@ export default {
resetPasswordToken: this.resetPasswordToken, resetPasswordToken: this.resetPasswordToken,
}; };
Auth.setNewPassword(credentials) Auth.setNewPassword(credentials)
.then((res) => { .then(res => {
if (res.status === 200) { if (res.status === 200) {
window.location = res.data.redirect_url; window.location = res.data.redirect_url;
} }

View file

@ -9,9 +9,6 @@
<h2 class="hero--title"> <h2 class="hero--title">
{{ $t('REGISTER.TRY_WOOT') }} {{ $t('REGISTER.TRY_WOOT') }}
</h2> </h2>
<p class="hero--sub">
{{ $t('REGISTER.TRY_WOOT_SUB') }}
</p>
</div> </div>
<div class="row align-center"> <div class="row align-center">
<div class="medium-5 column"> <div class="medium-5 column">

View file

@ -36,6 +36,7 @@ export default {
path: 'signup', path: 'signup',
name: 'auth_signup', name: 'auth_signup',
component: Signup, component: Signup,
meta: { requireSignupEnabled: true },
}, },
{ {
path: 'reset/password', path: 'reset/password',

View file

@ -1,13 +1,10 @@
<template> <template>
<div class="contact-conversation--panel"> <div class="contact-conversation--panel">
<contact-details-item <contact-details-item :title="$t('CONTACT_PANEL.CONVERSATIONS.TITLE')" />
icon="ion-chatbubbles"
:title="$t('CONTACT_PANEL.CONVERSATIONS.TITLE')"
/>
<div v-if="!uiFlags.isFetching"> <div v-if="!uiFlags.isFetching">
<i v-if="!previousConversations.length"> <p v-if="!previousConversations.length" class="no-results">
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }} {{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
</i> </p>
<div v-else class="contact-conversation--list"> <div v-else class="contact-conversation--list">
<conversation-card <conversation-card
v-for="conversation in previousConversations" v-for="conversation in previousConversations"
@ -78,11 +75,13 @@ export default {
@import '~dashboard/assets/scss/mixins'; @import '~dashboard/assets/scss/mixins';
.contact-conversation--panel { .contact-conversation--panel {
@include border-normal-top; padding: $space-normal $space-normal $space-normal $space-medium;
padding: $space-medium; padding-top: 0;
} }
.contact-conversation--list { .no-results {
margin-top: -$space-normal; margin: 0;
color: $color-gray;
padding: 0 $space-small;
} }
</style> </style>

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="conv-details--item"> <div class="conv-details--item">
<div class="conv-details--item__label"> <h4 class="conv-details--item__label">
<i :class="icon" class="conv-details--item__icon"></i> <i v-if="icon" :class="icon" class="conv-details--item__icon"></i>
{{ title }} {{ title }}
</div> </h4>
<div v-if="value" class="conv-details--item__value"> <div v-if="value" class="conv-details--item__value">
{{ value }} {{ value }}
</div> </div>
@ -14,7 +14,7 @@
export default { export default {
props: { props: {
title: { type: String, required: true }, title: { type: String, required: true },
icon: { type: String, required: true }, icon: { type: String, default: '' },
value: { type: [String, Number], default: '' }, value: { type: [String, Number], default: '' },
}, },
}; };
@ -25,23 +25,25 @@ export default {
@import '~dashboard/assets/scss/mixins'; @import '~dashboard/assets/scss/mixins';
.conv-details--item { .conv-details--item {
padding-bottom: $space-normal; padding-bottom: $space-medium;
&:last-child { &:last-child {
padding-bottom: 0; padding-bottom: 0;
} }
.conv-details--item__icon { .conv-details--item__icon {
padding-right: $space-micro; padding-right: $space-smaller;
} }
.conv-details--item__label { .conv-details--item__label {
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
margin-bottom: $space-micro; margin-bottom: $space-micro;
font-size: $font-size-small;
} }
.conv-details--item__value { .conv-details--item__value {
word-break: break-all; word-break: break-all;
margin-top: $space-small;
} }
} }
</style> </style>

View file

@ -1,6 +1,9 @@
<template> <template>
<div class="medium-3 bg-white contact--panel"> <div class="medium-3 bg-white contact--panel">
<div class="contact--profile"> <div class="contact--profile">
<span class="close-button" @click="onPanelToggle">
<i class="ion-close-round"></i>
</span>
<div class="contact--info"> <div class="contact--info">
<thumbnail <thumbnail
:src="contact.thumbnail" :src="contact.thumbnail"
@ -20,6 +23,16 @@
> >
{{ contact.email }} {{ contact.email }}
</a> </a>
<div
v-if="
contact.additional_attributes &&
contact.additional_attributes.screen_name
"
class="contact--location"
>
{{ `@${contact.additional_attributes.screen_name}` }}
</div>
<div class="contact--location"> <div class="contact--location">
{{ contact.location }} {{ contact.location }}
</div> </div>
@ -28,7 +41,22 @@
<div v-if="contact.bio" class="contact--bio"> <div v-if="contact.bio" class="contact--bio">
{{ contact.bio }} {{ contact.bio }}
</div> </div>
<div
v-if="
contact.additional_attributes &&
contact.additional_attributes.description
"
class="contact--bio"
>
{{ contact.additional_attributes.description }}
</div> </div>
</div>
<conversation-labels :conversation-id="conversationId" />
<contact-conversations
v-if="contact.id"
:contact-id="contact.id"
:conversation-id="conversationId"
/>
<div v-if="browser" class="conversation--details"> <div v-if="browser" class="conversation--details">
<contact-details-item <contact-details-item
v-if="browser.browser_name" v-if="browser.browser_name"
@ -55,13 +83,6 @@
icon="ion-clock" icon="ion-clock"
/> />
</div> </div>
<contact-conversations
v-if="contact.id"
:contact-id="contact.id"
:conversation-id="conversationId"
/>
<conversation-labels :conversation-id="conversationId" />
</div> </div>
</template> </template>
@ -83,6 +104,10 @@ export default {
type: [Number, String], type: [Number, String],
required: true, required: true,
}, },
onToggle: {
type: Function,
default: () => {},
},
}, },
computed: { computed: {
currentConversationMetaData() { currentConversationMetaData() {
@ -134,6 +159,11 @@ export default {
id: this.currentConversationMetaData.contact_id, id: this.currentConversationMetaData.contact_id,
}); });
}, },
methods: {
onPanelToggle() {
this.onToggle();
},
},
}; };
</script> </script>
@ -145,15 +175,21 @@ export default {
@include border-normal-left; @include border-normal-left;
font-size: $font-size-small; font-size: $font-size-small;
overflow-y: auto; overflow-y: auto;
background: $color-white; background: white;
overflow: auto; overflow: auto;
position: relative;
} }
.close-button {
position: absolute;
right: $space-slab;
top: $space-slab;
font-size: $font-size-default;
color: $color-heading;
}
.contact--profile { .contact--profile {
width: 100%; padding: $space-medium $space-normal 0 $space-medium;
padding: $space-normal $space-medium $zero;
align-items: center; align-items: center;
.user-thumbnail-box { .user-thumbnail-box {
margin-right: $space-normal; margin-right: $space-normal;
} }
@ -172,9 +208,10 @@ export default {
.contact--name { .contact--name {
@include text-ellipsis; @include text-ellipsis;
text-transform: capitalize;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
font-size: $font-size-default; font-size: $font-size-medium;
} }
.contact--email { .contact--email {
@ -191,8 +228,7 @@ export default {
} }
.conversation--details { .conversation--details {
padding: $space-medium; padding: $space-two $space-normal $space-two $space-medium;
width: 100%;
} }
.conversation--labels { .conversation--labels {

Some files were not shown because too many files have changed in this diff Show more