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
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
- image: circleci/postgres:9.4
- image: circleci/redis:5.0.7-alpine
- image: circleci/postgres:alpine
- image: circleci/redis:alpine
environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
@ -95,24 +95,24 @@ jobs:
command: yarn run eslint
# Run rails tests
- run:
- run:
name: Run backend tests
command: |
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json
- persist_to_workspace:
root: tmp
paths:
paths:
- codeclimate.backend.json
- run:
- run:
name: Run frontend tests
command: |
yarn test:coverage
./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info
- persist_to_workspace:
root: tmp
paths:
paths:
- codeclimate.frontend.json
# collect reports
@ -126,4 +126,4 @@ jobs:
name: Upload coverage results to Code Climate
command: |
./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 2 -o tmp/codeclimate.total.json
./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json
./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json

View file

@ -1,4 +1,12 @@
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_URL=redis://redis:6379
@ -20,14 +28,16 @@ FB_APP_SECRET=
FB_APP_ID=
#twitter app
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#mail
MAILER_SENDER_EMAIL=accounts@chatwoot.com
SMTP_PORT=1025
SMTP_DOMAIN=chatwoot.com
# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog",
# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog",
# else set the value as "localhost"
SMTP_ADDRESS=mailhog
SMTP_USERNAME=
@ -59,4 +69,4 @@ ENABLE_BILLING=
CHARGEBEE_API_KEY=
CHARGEBEE_SITE=
CHARGEBEE_WEBHOOK_USERNAME=
CHARGEBEE_WEBHOOK_PASSWORD=
CHARGEBEE_WEBHOOK_PASSWORD=

View file

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

4
.gitignore vendored
View file

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

View file

@ -20,14 +20,12 @@ Style/GlobalVars:
Exclude:
- 'config/initializers/redis.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:
Exclude:
- spec/**/*
- '**/routes.rb'
- 'config/environments/*'
- db/schema.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
@ -41,6 +39,8 @@ Style/ClassAndModuleChildren:
RSpec/NestedGroups:
Enabled: true
Max: 4
RSpec/MessageSpies:
Enabled: false
AllCops:
Exclude:
- 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:
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:
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:
- 'app/javascript/widget/assets/scss/_reset.scss'
- 'app/javascript/widget/assets/scss/sdk.css'

12
Gemfile
View file

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

View file

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

View file

@ -52,16 +52,10 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under
## Docker
You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot)
```bash
docker pull chatwoot/chatwoot
```
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`.
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 ✨
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`
# 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
# 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,
message_type: @message_type,
content: response.content,
fb_id: response.identifier
source_id: response.identifier
}
end

View file

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

View file

@ -1,16 +1,14 @@
class Api::BaseController < ApplicationController
respond_to :json
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
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end
def check_billing_enabled
raise ActionController::RoutingError, 'Not Found' unless ENV['BILLING_ENABLED']
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 :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
only: [:create], raise: false
before_action :check_signup_enabled
rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::UserExists,
@ -30,4 +31,8 @@ class Api::V1::AccountsController < Api::BaseController
def account_params
params.permit(:account_name, :email)
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
end
end

View file

@ -1,8 +1,7 @@
require 'rest-client'
require 'telegram/bot'
class Api::V1::CallbacksController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:register_facebook_page]
skip_before_action :authenticate_user!, only: [:register_facebook_page], raise: false
class Api::V1::CallbacksController < Api::BaseController
before_action :inbox, only: [:reauthorize_page]
def register_facebook_page
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]
def reauthorize_page
if @inbox&.first&.facebook?
if @inbox&.facebook?
fb_page_id = @inbox.channel.page_id
page_details = fb_object.get_connections('me', 'accounts')
(page_details || []).each do |page_detail|
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'])
head :ok
end
if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] })
update_fb_page(fb_page_id, page_detail['access_token'])
return head :ok
end
end
@ -46,18 +43,13 @@ class Api::V1::CallbacksController < ApplicationController
@inbox = current_account.inboxes.find_by(id: params[:inbox_id])
end
def update_fb_page
if fb_page(fb_page_id)
fb_page.update_attributes!(
user_access_token: @user_access_token, page_access_token: access_token
)
head :ok
else
head :unprocessable_entity
end
def update_fb_page(fb_page_id, access_token)
get_fb_page(fb_page_id)&.update!(
user_access_token: @user_access_token, page_access_token: access_token
)
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)
end
@ -68,7 +60,7 @@ class Api::V1::CallbacksController < ApplicationController
def long_lived_token(omniauth_token)
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
def mark_already_existing_facebook_pages(data)
@ -81,7 +73,11 @@ class Api::V1::CallbacksController < ApplicationController
end
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)
end
@ -98,7 +94,6 @@ class Api::V1::CallbacksController < ApplicationController
raise
end
pic_url = response.base_uri.to_s
Rails.logger.info(pic_url)
rescue StandardError => e
pic_url = nil
end

View file

@ -1,11 +1,5 @@
class Api::V1::ConversationsController < Api::BaseController
before_action :set_conversation, except: [:index, :get_messages]
# 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]
before_action :set_conversation, except: [:index]
def index
result = conversation_finder.perform
@ -27,11 +21,6 @@ class Api::V1::ConversationsController < Api::BaseController
head :ok
end
def get_messages
@conversation = Conversation.find(params[:id])
@messages = messages_finder.perform
end
private
def parsed_last_seen_at

View file

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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
class Api::V1::SubscriptionsController < ApplicationController
class Api::V1::SubscriptionsController < Api::BaseController
skip_before_action :check_subscription
before_action :check_billing_enabled
def index
render json: current_account.subscription_data
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
before_action :login_from_basic_auth, only: [:chargebee]
before_action :check_billing_enabled, only: [:chargebee]
def chargebee
chargebee_consumer.consume
head :ok
@ -13,7 +14,7 @@ class Api::V1::WebhooksController < ApplicationController
end
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
def twitter_events
@ -26,6 +27,12 @@ class Api::V1::WebhooksController < ApplicationController
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
authenticate_or_request_with_http_basic do |username, 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
{
account_id: conversation.account_id,
contact_id: @contact.id,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
message_type: :incoming,
content: permitted_params[:message][:content]
message_type: :incoming
}
end

View file

@ -67,7 +67,7 @@ class ApplicationController < ActionController::Base
def check_subscription
# This block is left over from the initial 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
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
# Unpermitted parameter: session
wrap_parameters format: []
def render_create_success
render 'devise/auth.json'
end
end

View file

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

View file

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

View file

@ -1,23 +1,18 @@
class ConversationFinder
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
# assumptions
# 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'
# response of this class will be of type
# {conversations: [array of conversations], count: {open: count, resolved: count}}
# params
# assignee_type_id, inbox_id, :status
# assignee_type, inbox_id, :status
def initialize(current_user, params)
@current_user = current_user
@ -62,7 +57,7 @@ class ConversationFinder
end
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
def find_all_conversations
@ -72,12 +67,10 @@ class ConversationFinder
end
def filter_by_assignee_type
if @assignee_type_id == ASSIGNEE_TYPES[:me]
if @assignee_type == 'me'
@conversations = @conversations.assigned_to(current_user)
elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned]
elsif @assignee_type == 'unassigned'
@conversations = @conversations.unassigned
elsif @assignee_type_id == ASSIGNEE_TYPES[:all]
@conversations
end
@conversations
end

View file

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

View file

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

View file

@ -1,29 +1,10 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint-env browser */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import moment from 'moment';
import Cookies from 'js-cookie';
import endPoints from './endPoints';
import { frontendURL } from '../helper/URLHelper';
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');
};
import { setAuthCredentials, clearCookiesOnLogout } from '../store/utils/api';
export default {
login(creds) {
@ -60,20 +41,7 @@ export default {
},
validityCheck() {
const urlData = endPoints('validityCheck');
const fetchPromise = new Promise((resolve, reject) => {
axios
.get(urlData.url)
.then(response => {
resolve(response);
})
.catch(error => {
if (error.response.status === 401) {
clearCookiesOnLogout();
}
reject(error);
});
});
return fetchPromise;
return axios.get(urlData.url);
},
logout() {
const urlData = endPoints('logout');
@ -136,13 +104,7 @@ export default {
password,
})
.then(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'),
});
setAuthCredentials(response);
resolve(response);
})
.catch(error => {
@ -155,4 +117,22 @@ export default {
const urlData = endPoints('resetPassword');
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`);
}
createLabels(conversationID) {
return axios.get(`${this.url}/${conversationID}/labels`);
updateLabels(conversationID, labels) {
return axios.post(`${this.url}/${conversationID}/labels`, { labels });
}
}

View file

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

View file

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

View file

@ -10,6 +10,6 @@ describe('#ConversationApi', () => {
expect(conversations).toHaveProperty('update');
expect(conversations).toHaveProperty('delete');
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 {
border: 0;
font-family: 'Monaco';
font-family: 'Monaco', Verdana;
font-size: $font-size-mini;
&.hljs {

View file

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

View file

@ -35,15 +35,17 @@ body {
flex-direction: column;
@include margin($zero);
@include padding($space-normal);
overflow-y: scroll;
overflow-y: auto;
}
.content-box {
overflow: scroll;
overflow: auto;
@include padding($space-normal);
}
.back-button {
@include flex;
align-items: center;
color: $color-woot;
font-size: $font-size-default;
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
@mixin border-nil() {
border-color: transparent;
@ -77,8 +82,8 @@
&:active,
&:hover,
&:focus {
box-shadow: none;
border-color: transparent;
box-shadow: none;
}
}
@ -117,7 +122,6 @@
// full height
@mixin full-height() {
height: 100%;
// COmmenting because unneccessary scroll is apprearing on some pages eg: settings/agents / inboxes
}
@mixin round-corner() {
@ -125,21 +129,20 @@
}
@mixin scroll-on-hover() {
transition: all .4s $ease-in-out-cubic;
overflow: hidden;
&:hover {
overflow-y: scroll;
overflow-y: auto;
}
}
@mixin horizontal-scroll() {
overflow-y: scroll;
overflow-y: auto;
}
@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() {
@ -154,20 +157,20 @@
}
}
&:before {
content: '';
&::before {
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;
content: '';
height: $space-medium;
left: 50%;
margin-left: -$space-one;
margin-top: -$space-one;
position: absolute;
top: 50%;
left: 50%;
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;
}
}
@ -181,41 +184,41 @@
// .element{
// @include arrow(top, #000, 50px);
// }
@mixin arrow($direction, $color, $size){
display: block;
height: 0;
width: 0;
content: '';
@mixin arrow($direction, $color, $size) {
display: block;
height: 0;
width: 0;
content: '';
@if $direction == 'top' {
border-left: $size solid transparent;
border-right: $size solid transparent;
border-bottom: $size solid $color;
} @else if $direction == 'right' {
border-top: $size solid transparent;
border-bottom: $size solid transparent;
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-right: $size solid $color;
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;
} @else if $direction == 'bottom-left' {
border-bottom: $size solid $color;
border-right: $size solid transparent;
} @else if $direction == 'bottom-right' {
border-bottom: $size solid $color;
border-left: $size solid transparent;
}
@if $direction == 'top' {
border-bottom: $size solid $color;
border-left: $size solid transparent;
border-right: $size solid transparent;
} @else if $direction == 'right' {
border-bottom: $size solid transparent;
border-left: $size solid $color;
border-top: $size solid transparent;
} @else if $direction == 'bottom' {
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' {
border-bottom: $size solid $color;
border-right: $size solid transparent;
} @else if $direction == 'bottom-right' {
border-bottom: $size solid $color;
border-left: $size solid transparent;
}
}
@mixin text-ellipsis {

View file

@ -12,7 +12,7 @@ $font-size-mega: 3.4rem;
$font-size-giga: 4.0rem;
// spaces
$zero: 0rem;
$zero: 0;
$space-micro: 0.2rem;
$space-smaller: 0.4rem;
$space-small: 0.8rem;
@ -42,16 +42,27 @@ $woot-logo-padding: $space-large $space-two;
// Colors
$color-woot: #1f93ff;
$color-gray: #6E6F73;
$color-light-gray: #999A9B;
$color-border: #E0E6ED;
$color-gray: #6e6f73;
$color-light-gray: #999a9b;
$color-border: #e0e6ed;
$color-border-light: #f0f4f5;
$color-background: #EFF2F7;
$color-background-light: #F9FAFC;
$color-white: #FFF;
$color-body: #3C4858;
$color-heading: #1F2D3D;
$color-extra-light-blue: #F5F7F9;
$color-background: #eff2f7;
$color-background-light: #f9fafc;
$color-white: #fff;
$color-body: #3c4858;
$color-heading: #1f2d3d;
$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-radius: 4rem;
@ -81,3 +92,6 @@ $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !d
// Ionicons
$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/channel';
@import 'views/settings/integrations';
@import 'views/signup';
@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 {
margin-bottom: $space-normal;
min-height: 38px;
> .multiselect__tags {
@include margin(0);
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;
&.multiselect--active {
>.multiselect__tags {
border-color: $color-woot;
}
}
@ -41,4 +33,93 @@
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 {
max-height: $space-mega;
overflow: scroll;
overflow: auto;
white-space: nowrap;
@include padding($space-one);
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

@ -24,9 +24,18 @@
}
}
> .icon {
>.icon {
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 {

View file

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

View file

@ -2,8 +2,8 @@
@include flex;
@include flex-shrink;
@include padding($space-normal $zero $zero $space-normal);
position: relative;
cursor: pointer;
position: relative;
&.active {
background: $color-background;
@ -18,63 +18,63 @@
.conversation--user {
font-size: $font-size-small;
margin-bottom: $zero;
text-transform: capitalize;
.label {
position: relative;
top: $space-micro;
left: $space-micro;
max-width: $space-jumbo;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
top: $space-micro;
white-space: nowrap;
}
}
.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;
width: 27rem;
white-space: nowrap;
font-size: $font-size-small;
font-weight: $font-weight-normal;
height: $space-medium;
line-height: $space-medium;
margin: $zero;
max-width: 96%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 27rem;
}
.conversation--meta {
@include flex;
display: block;
flex-direction: column;
position: absolute;
right: $space-normal;
top: $space-normal;
@include flex;
flex-direction: column;
.unread {
$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;
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;
min-width: $unread-size;
padding: 0 $space-smaller;
text-align: center;
}
.timestamp {
font-size: $font-size-mini;
color: $dark-gray;
line-height: $space-normal;
font-weight: $font-weight-normal;
font-size: $font-size-micro;
font-weight: $font-weight-normal;
line-height: $space-normal;
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 {
@include flex;
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 {
@include flex;
@include padding($space-normal $zero $space-small $zero);
@ -28,10 +129,7 @@
}
}
.conversations-list {
@include flex-weight(1);
@include scroll-on-hover;
}
.content-box {
text-align: center;
@ -47,16 +145,19 @@
@include background-gray;
@include margin(0);
@include border-normal-left;
.current-chat {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
flex-direction: column;
div {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
flex-direction: column;
img {
@include margin($space-normal);
width: 10rem;
@ -73,21 +174,22 @@
.conv-empty-state {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
flex-direction: column;
}
}
.conversation-panel {
@include flex;
@include flex-weight(1);
flex-direction: column;
@include margin($zero);
flex-direction: column;
// Firefox flexbox fix
height: 100%;
overflow-y: scroll;
margin-bottom: $space-small;
overflow-y: auto;
> li {
>li {
@include flex;
@include flex-shrink;
@include margin($zero $zero $space-smaller);
@ -114,6 +216,7 @@
}
.bubble {
@include bubble-with-tyes;
max-width: 50rem;
text-align: left;
word-wrap: break-word;
@ -147,7 +250,7 @@
@include flex-align(right, null);
.wrap {
margin-right: $space-small;
margin-right: $space-normal;
text-align: right;
}
@ -205,6 +308,7 @@
@include padding($space-smaller $space-normal);
@include flex-align($x: center, $y: null);
background: lighten($warning-color, 32%);
border: 1px solid lighten($warning-color, 26%);
border-radius: $space-smaller;
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 {
bottom: -$space-micro;
color: $color-gray;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
.side-menu {
i {
min-width: $space-two;
margin-right: $space-smaller;
min-width: $space-two;
}
}
@ -27,6 +27,26 @@
border-radius: $space-smaller;
color: $color-gray;
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 space-between-column;
@include padding($space-one $space-normal $space-one $space-one);
flex-direction: column;
@include border-normal-top;
flex-direction: column;
position: relative;
.dropdown-pane {
@include elegant-card;
@include border-light;
display: block;
left: 18%;
top: -110%;
visibility: visible;
display: block;
width: 80%;
z-index: 999;
@ -79,23 +99,23 @@
font-size: $font-size-medium;
margin-top: $space-medium;
> span {
>span {
margin-left: auto;
}
}
}
.menu-title + ul > li > a {
.menu-title+ul>li>a {
@include padding($space-micro null);
color: $medium-gray;
line-height: $global-lineheight;
}
.current-user {
align-items: center;
@include flex;
flex-direction: row;
align-items: center;
cursor: pointer;
flex-direction: row;
.current-user--data {
@include flex;
@ -105,7 +125,7 @@
font-size: $font-size-small;
font-weight: $font-weight-medium;
line-height: 1;
margin-bottom: $zero;
margin-bottom: $space-smaller;
margin-left: $space-one;
margin-top: $space-micro;
}
@ -132,7 +152,7 @@
display: none;
margin-right: $space-normal;
@media screen and (max-width: 1200px){
@media screen and (max-width: 1200px) {
display: block;
}
}
@ -141,7 +161,7 @@
display: block;
margin-right: $space-normal;
@media screen and (max-width: 1200px){
@media screen and (max-width: 1200px) {
display: none;
}
}

View file

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

View file

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

View file

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

View file

@ -16,8 +16,12 @@
/* global bus */
import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner';
import wootConstants from '../../constants';
export default {
components: {
Spinner,
},
props: ['conversationId'],
data() {
return {
@ -29,19 +33,23 @@ export default {
currentChat: 'getSelectedChat',
}),
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;
},
buttonClass() {
return this.currentChat.status === 0 ? 'success' : 'warning';
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
? 'success'
: 'warning';
},
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: {
toggleStatus() {
this.isLoading = true;

View file

@ -42,12 +42,21 @@
class="dropdown-pane top"
>
<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>
</div>
</transition>
<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">
<h3 class="current-user--name">
{{ currentUser.name }}
@ -65,7 +74,6 @@
<script>
import { mapGetters } from 'vuex';
import md5 from 'md5';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from '../../mixins/isAdmin';
@ -99,6 +107,7 @@ export default {
daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription',
inboxes: 'inboxes/getInboxes',
currentUser: 'getCurrentUser',
}),
accessibleMenuItems() {
// get all keys in menuGroup
@ -141,12 +150,10 @@ export default {
id: inbox.id,
label: inbox.name,
toState: frontendURL(`inbox/${inbox.id}`),
type: inbox.channel_type,
})),
};
},
currentUser() {
return Auth.getCurrentUser();
},
dashboardPath() {
return frontendURL('dashboard');
},
@ -174,10 +181,6 @@ export default {
this.$store.dispatch('inboxes/get');
},
methods: {
gravatarUrl() {
const hash = md5(this.currentUser.email);
return `${window.WootConstants.GRAVATAR_URL}${hash}?default=404`;
},
filterBillingRoutes(menuItems) {
return menuItems.filter(
menuItem => !menuItem.toState.includes('billing')
@ -185,6 +188,9 @@ export default {
},
filterMenuItemsByRole(menuItems) {
const { role } = this.currentUser;
if (!role) {
return [];
}
return menuItems.filter(
menuItem =>
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1

View file

@ -24,11 +24,17 @@
v-for="child in menuItem.children"
:key="child.id"
active-class="active flex-container"
:class="computedInboxClass(child)"
tag="li"
: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>
</ul>
</router-link>
@ -41,6 +47,27 @@ import { mapGetters } from 'vuex';
import router from '../../routes';
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 {
props: {
menuItem: {
@ -75,10 +102,9 @@ export default {
},
methods: {
computedInboxClass(child) {
if (parseInt(this.activeInbox, 10) === child.channel_id) {
return 'active flex-container';
}
return ' ';
const { type } = child;
const classByType = getInboxClassByType(type);
return classByType;
},
newLinkClick() {
router.push({ name: 'settings_inbox_new', params: { page: 'new' } });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -93,7 +93,11 @@ export default {
];
},
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"
:readable-time="readableTime"
/>
<i v-if="data.message_type === 2" class="icon ion-person" />
<bubble-text
v-if="data.content"
:message="message"

View file

@ -4,9 +4,13 @@ export default {
return `${this.APP_BASE_URL}/`;
},
GRAVATAR_URL: 'https://www.gravatar.com/avatar/',
ASSIGNEE_TYPE_SLUG: {
MINE: 0,
UNASSIGNED: 1,
OPEN: 0,
ASSIGNEE_TYPE: {
ME: 'me',
UNASSIGNED: 'unassigned',
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)}` : '';
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',
'inbox_dashboard',
'inbox_conversation',
'conversation_through_inbox',
'settings_account_reports',
'billing_deactivated',
'profile_settings',
'profile_settings_index',
],
menuItems: {
assignedToMe: {
@ -49,6 +52,8 @@ export default {
'settings_inboxes_add_agents',
'settings_inbox_finish',
'billing',
'settings_integrations',
'settings_integrations_webhook',
],
menuItems: {
back: {
@ -86,12 +91,12 @@ export default {
toState: frontendURL('settings/billing'),
toStateName: 'billing',
},
account: {
icon: 'ion-beer',
label: 'Account Settings',
settings_integrations: {
icon: 'ion-flash',
label: 'Integrations',
hasSubMenu: false,
toState: frontendURL('settings/account'),
toStateName: 'account',
toState: frontendURL('settings/integrations'),
toStateName: 'settings_integrations',
},
},
},

View file

@ -45,6 +45,7 @@
},
"API": {
"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"
}
},
@ -58,7 +59,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
}
},
"EDIT": {

View file

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

View file

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

View file

@ -1,5 +1,6 @@
{
"CONTACT_PANEL": {
"CONVERSATION_TITLE": "Conversation Details",
"BROWSER": "Browser",
"OS": "Operating System",
"INITIATED_FROM": "Initiated from",
@ -9,8 +10,11 @@
"TITLE": "Previous Conversations"
},
"LABELS": {
"NO_RECORDS_FOUND": "There are no labels associated to this conversation.",
"TITLE": "Labels"
"TITLE": "Conversation 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_CONVERSATIONS": "Loading Conversations",
"HEADER": {
"RESOLVE_ACTION": "Resolve"
"RESOLVE_ACTION": "Resolve",
"REOPEN_ACTION": "Reopen",
"OPEN": "More",
"CLOSE": "Close",
"DETAILS": "details"
},
"FOOTER": {
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",

View file

@ -15,6 +15,9 @@
"FB": {
"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": {
"TITLE": "Website channel",
"DESC": "Create a channel for your website and start supporting your customers via our website widget.",
@ -34,11 +37,11 @@
},
"AUTH": {
"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": {
"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": {
"TITLE": "Inbox Details",
@ -71,7 +74,12 @@
"EDIT": {
"API": {
"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."
},
"AUTO_ASSIGNMENT": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
}
},
"DELETE": {
@ -80,7 +88,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
},
"API": {
"SUCCESS_MESSAGE": "Inbox deleted successfully",
@ -93,7 +101,9 @@
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
"INBOX_AGENTS": "Agents",
"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 _resetPassword } from './resetPassword.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 _integrations } from './integrations.json';
export default {
..._agentMgmt,
@ -24,5 +26,7 @@ export default {
..._report,
..._resetPassword,
..._setNewPassword,
..._settings,
..._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": {
"TRY_WOOT": "Try Chatwoot free for 14 days",
"TRY_WOOT_SUB": "No credit card required. Cancel anytime.",
"TRY_WOOT": "Register an account",
"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>",
"ACCOUNT_NAME": {
@ -30,4 +29,4 @@
},
"SUBMIT": "Submit"
}
}
}

View file

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

View file

@ -11,7 +11,8 @@ export default {
return m.messages.filter(
chat =>
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;
},
readMessages(m) {

View file

@ -1,23 +1,40 @@
<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">
<h4>{{$t('SET_NEW_PASSWORD.TITLE')}}</h4>
<label :class="{ 'error': $v.credentials.password.$error }">
{{$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">
<span class="message" v-if="$v.credentials.password.$error">
{{$t('SET_NEW_PASSWORD.PASSWORD.ERROR')}}
<h4>{{ $t('SET_NEW_PASSWORD.TITLE') }}</h4>
<label :class="{ error: $v.credentials.password.$error }">
{{ $t('LOGIN.PASSWORD.LABEL') }}
<input
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') }}
</span>
</label>
<label :class="{ 'error': $v.credentials.confirmPassword.$error }">
{{$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">
<span class="message" v-if="$v.credentials.confirmPassword.$error">
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR')}}
<label :class="{ error: $v.credentials.confirmPassword.$error }">
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL') }}
<input
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') }}
</span>
</label>
<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')"
:loading="newPasswordAPI.showLoading"
button-class="expanded"
@ -99,7 +116,7 @@ export default {
resetPasswordToken: this.resetPasswordToken,
};
Auth.setNewPassword(credentials)
.then((res) => {
.then(res => {
if (res.status === 200) {
window.location = res.data.redirect_url;
}

View file

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

View file

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

View file

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

View file

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

View file

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

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