Merge branch 'release/1.2.0'
This commit is contained in:
commit
c772c0af03
332 changed files with 11142 additions and 3705 deletions
|
@ -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
|
||||
|
|
14
.env.example
14
.env.example
|
@ -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=
|
||||
|
|
|
@ -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
4
.gitignore
vendored
|
@ -37,6 +37,10 @@ public/packs*
|
|||
*.swo
|
||||
*.un~
|
||||
.jest-cache
|
||||
|
||||
#VS Code files
|
||||
.vscode
|
||||
|
||||
# ignore jetbrains IDE files
|
||||
.idea
|
||||
|
||||
|
|
|
@ -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/*
|
||||
|
|
274
.scss-lint.yml
274
.scss-lint.yml
|
@ -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
12
Gemfile
|
@ -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'
|
||||
|
|
54
Gemfile.lock
54
Gemfile.lock
|
@ -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
|
||||
|
|
|
@ -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
1
__mocks__/fileMock.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = '';
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
36
app/controllers/api/v1/account/webhooks_controller.rb
Normal file
36
app/controllers/api/v1/account/webhooks_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
18
app/controllers/swagger_controller.rb
Normal file
18
app/controllers/swagger_controller.rb
Normal 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
|
30
app/controllers/twitter/authorizations_controller.rb
Normal file
30
app/controllers/twitter/authorizations_controller.rb
Normal 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
|
24
app/controllers/twitter/base_controller.rb
Normal file
24
app/controllers/twitter/base_controller.rb
Normal 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
|
51
app/controllers/twitter/callbacks_controller.rb
Normal file
51
app/controllers/twitter/callbacks_controller.rb
Normal 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
|
|
@ -4,9 +4,7 @@ class WidgetsController < ActionController::Base
|
|||
before_action :set_contact
|
||||
before_action :build_contact
|
||||
|
||||
def index
|
||||
render
|
||||
end
|
||||
def index; end
|
||||
|
||||
private
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
module Api::V1::WebhooksHelper
|
||||
end
|
|
@ -18,8 +18,7 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch('set_user');
|
||||
this.$store.dispatch('validityCheck');
|
||||
this.$store.dispatch('setUser');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@ const endPoints = {
|
|||
validityCheck: {
|
||||
url: '/auth/validate_token',
|
||||
},
|
||||
profileUpdate: {
|
||||
url: '/api/v1/profile',
|
||||
},
|
||||
logout: {
|
||||
url: 'auth/sign_out',
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
14
app/javascript/dashboard/api/userNotificationSettings.js
Normal file
14
app/javascript/dashboard/api/userNotificationSettings.js
Normal 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();
|
9
app/javascript/dashboard/api/webhooks.js
Normal file
9
app/javascript/dashboard/api/webhooks.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ApiClient from './ApiClient';
|
||||
|
||||
class WebHooks extends ApiClient {
|
||||
constructor() {
|
||||
super('account/webhooks');
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebHooks();
|
Binary file not shown.
|
@ -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 |
|
@ -28,7 +28,7 @@
|
|||
|
||||
code {
|
||||
border: 0;
|
||||
font-family: 'Monaco';
|
||||
font-family: 'Monaco', Verdana;
|
||||
font-size: $font-size-mini;
|
||||
|
||||
&.hljs {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
@import 'views/settings/inbox';
|
||||
@import 'views/settings/channel';
|
||||
@import 'views/settings/integrations';
|
||||
@import 'views/signup';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,7 +212,7 @@
|
|||
|
||||
.code {
|
||||
max-height: $space-mega;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
@include padding($space-one);
|
||||
background: $color-background;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
@include padding($space-small);
|
||||
box-sizing: border-box;
|
||||
height: 180px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
|
||||
.emoji {
|
||||
border-radius: 4px;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
background-color: $color-white;
|
||||
border-radius: $space-small;
|
||||
max-height: 100%;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 60rem;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' } });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')}`;
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
21
app/javascript/dashboard/helper/spec/URLHelper.spec.js
Normal file
21
app/javascript/dashboard/helper/spec/URLHelper.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
"NO": "No, Keep it "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
54
app/javascript/dashboard/i18n/locale/en/integrations.json
Normal file
54
app/javascript/dashboard/i18n/locale/en/integrations.json
Normal 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 "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
app/javascript/dashboard/i18n/locale/en/settings.json
Normal file
58
app/javascript/dashboard/i18n/locale/en/settings.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/en/webhooks.json
Normal file
5
app/javascript/dashboard/i18n/locale/en/webhooks.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"WEBHOOKS_SETTINGS": {
|
||||
"HEADER": "Webhook Settings"
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -36,6 +36,7 @@ export default {
|
|||
path: 'signup',
|
||||
name: 'auth_signup',
|
||||
component: Signup,
|
||||
meta: { requireSignupEnabled: true },
|
||||
},
|
||||
{
|
||||
path: 'reset/password',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue