Merge branch 'release/1.5.0'
This commit is contained in:
commit
89dafa366e
403 changed files with 7809 additions and 1513 deletions
|
@ -7,7 +7,7 @@ defaults: &defaults
|
|||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:2.7.0-node-browsers
|
||||
- image: circleci/ruby:2.7.1-node-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
|
11
.env.example
11
.env.example
|
@ -73,10 +73,6 @@ RAILS_LOG_TO_STDOUT=true
|
|||
LOG_LEVEL=info
|
||||
LOG_SIZE=500
|
||||
|
||||
# Credentials to access sidekiq dashboard in production
|
||||
SIDEKIQ_AUTH_USERNAME=
|
||||
SIDEKIQ_AUTH_PASSWORD=
|
||||
|
||||
### This environment variables are only required if you are setting up social media channels
|
||||
#facebook
|
||||
FB_VERIFY_TOKEN=
|
||||
|
@ -106,3 +102,10 @@ CHARGEBEE_WEBHOOK_PASSWORD=
|
|||
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||
# VAPID_PUBLIC_KEY=
|
||||
# VAPID_PRIVATE_KEY=
|
||||
|
||||
## Bot Customizations
|
||||
USE_INBOX_AVATAR_FOR_BOT=true
|
||||
|
||||
## Development Only Config
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
|
|
|
@ -12,6 +12,8 @@ Layout/LineLength:
|
|||
Max: 150
|
||||
Metrics/ClassLength:
|
||||
Max: 125
|
||||
Exclude:
|
||||
- 'app/models/conversation.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
|
@ -30,6 +32,7 @@ Style/GlobalVars:
|
|||
Exclude:
|
||||
- 'config/initializers/redis.rb'
|
||||
- 'lib/redis/alfred.rb'
|
||||
- 'lib/global_config.rb'
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
|
@ -94,6 +97,8 @@ Rails/UniqueValidationWithoutIndex:
|
|||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
AllCops:
|
||||
Exclude:
|
||||
- 'bin/**/*'
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.7.0
|
||||
2.7.1
|
||||
|
|
|
@ -177,6 +177,8 @@ linters:
|
|||
allow_element_with_attribute: false
|
||||
allow_element_with_class: false
|
||||
allow_element_with_id: false
|
||||
exclude:
|
||||
- 'app/assets/stylesheets/administrate/components/_buttons.scss'
|
||||
|
||||
SelectorDepth:
|
||||
enabled: true
|
||||
|
@ -279,3 +281,4 @@ linters:
|
|||
exclude:
|
||||
- 'app/javascript/widget/assets/scss/_reset.scss'
|
||||
- 'app/javascript/widget/assets/scss/sdk.css'
|
||||
- 'app/assets/stylesheets/administrate/reset/_normalize.scss'
|
||||
|
|
7
Gemfile
7
Gemfile
|
@ -1,6 +1,6 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
ruby '2.7.0'
|
||||
ruby '2.7.1'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
|
@ -49,6 +49,8 @@ gem 'devise_token_auth'
|
|||
# authorization
|
||||
gem 'jwt'
|
||||
gem 'pundit'
|
||||
# super admin
|
||||
gem 'administrate'
|
||||
|
||||
##--- gems for pubsub service ---##
|
||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||
|
@ -62,7 +64,8 @@ gem 'facebook-messenger'
|
|||
gem 'telegram-bot-ruby'
|
||||
gem 'twilio-ruby', '~> 5.32.0'
|
||||
# twitty will handle subscription of twitter account events
|
||||
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
gem 'twitty'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# Random name generator
|
||||
|
|
241
Gemfile.lock
241
Gemfile.lock
|
@ -1,10 +1,3 @@
|
|||
GIT
|
||||
remote: https://github.com/chatwoot/twitty
|
||||
revision: af4f3e45dca55e42c64f7741a1fedfaa94d36419
|
||||
specs:
|
||||
twitty (0.1.0)
|
||||
oauth
|
||||
|
||||
GIT
|
||||
remote: https://github.com/sds/mock_redis
|
||||
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
|
@ -25,85 +18,98 @@ GEM
|
|||
specs:
|
||||
action-cable-testing (0.6.1)
|
||||
actioncable (>= 5.0)
|
||||
actioncable (6.0.2.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
actioncable (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.2.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
activejob (= 6.0.2.2)
|
||||
activerecord (= 6.0.2.2)
|
||||
activestorage (= 6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
actionmailbox (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
activejob (= 6.0.3.1)
|
||||
activerecord (= 6.0.3.1)
|
||||
activestorage (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.2.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
actionview (= 6.0.2.2)
|
||||
activejob (= 6.0.2.2)
|
||||
actionmailer (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
actionview (= 6.0.3.1)
|
||||
activejob (= 6.0.3.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.2.2)
|
||||
actionview (= 6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
actionpack (6.0.3.1)
|
||||
actionview (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.2.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
activerecord (= 6.0.2.2)
|
||||
activestorage (= 6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
actiontext (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
activerecord (= 6.0.3.1)
|
||||
activestorage (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
actionview (6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
activejob (6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
activerecord (6.0.2.2)
|
||||
activemodel (= 6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
activestorage (6.0.2.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
activejob (= 6.0.2.2)
|
||||
activerecord (= 6.0.2.2)
|
||||
activemodel (6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
activerecord (6.0.3.1)
|
||||
activemodel (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
activestorage (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
activejob (= 6.0.3.1)
|
||||
activerecord (= 6.0.3.1)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (6.0.2.2)
|
||||
activesupport (6.0.3.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2)
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
acts-as-taggable-on (6.5.0)
|
||||
activerecord (>= 5.0, < 6.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.13.0)
|
||||
actionpack (>= 4.2)
|
||||
actionview (>= 4.2)
|
||||
activerecord (>= 4.2)
|
||||
autoprefixer-rails (>= 6.0)
|
||||
datetime_picker_rails (~> 0.0.7)
|
||||
jquery-rails (>= 4.0)
|
||||
kaminari (>= 1.0)
|
||||
momentjs-rails (~> 2.8)
|
||||
sassc-rails (~> 2.1)
|
||||
selectize-rails (~> 0.6)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.0)
|
||||
attr_extras (6.2.3)
|
||||
autoprefixer-rails (9.7.6)
|
||||
execjs
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.296.0)
|
||||
aws-sdk-core (3.94.0)
|
||||
aws-partitions (1.317.0)
|
||||
aws-sdk-core (3.96.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.30.0)
|
||||
aws-sdk-kms (1.31.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.61.2)
|
||||
aws-sdk-core (~> 3, >= 3.83.0)
|
||||
aws-sdk-s3 (1.65.0)
|
||||
aws-sdk-core (~> 3, >= 3.96.1)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.1)
|
||||
aws-sigv4 (1.1.3)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
axiom-types (0.1.1)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
|
@ -120,8 +126,8 @@ GEM
|
|||
bindex (0.8.1)
|
||||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.8.1)
|
||||
browser (4.0.0)
|
||||
brakeman (4.8.2)
|
||||
browser (4.1.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -131,7 +137,7 @@ GEM
|
|||
bundler-audit (0.6.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 0.18)
|
||||
byebug (11.1.1)
|
||||
byebug (11.1.3)
|
||||
chargebee (2.7.5)
|
||||
json_pure (~> 2.1)
|
||||
rest-client (>= 1.8, < 3.0)
|
||||
|
@ -141,6 +147,8 @@ GEM
|
|||
concurrent-ruby (1.1.6)
|
||||
connection_pool (2.2.2)
|
||||
crass (1.0.6)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
descendants_tracker (0.0.4)
|
||||
|
@ -167,13 +175,13 @@ GEM
|
|||
equalizer (0.0.11)
|
||||
erubi (1.9.0)
|
||||
execjs (2.7.0)
|
||||
facebook-messenger (1.4.1)
|
||||
facebook-messenger (1.5.0)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (5.1.2)
|
||||
factory_bot (5.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
factory_bot_rails (5.1.1)
|
||||
factory_bot (~> 5.1.0)
|
||||
factory_bot_rails (5.2.0)
|
||||
factory_bot (~> 5.2.0)
|
||||
railties (>= 4.2.0)
|
||||
faker (2.11.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
|
@ -186,7 +194,7 @@ GEM
|
|||
foreman (0.87.1)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.38.0)
|
||||
google-api-client (0.39.4)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
|
@ -200,7 +208,7 @@ GEM
|
|||
google-cloud-env (1.3.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.0.0)
|
||||
google-cloud-storage (1.26.0)
|
||||
google-cloud-storage (1.26.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
|
@ -217,7 +225,7 @@ GEM
|
|||
groupdate (5.0.0)
|
||||
activesupport (>= 5)
|
||||
haikunator (1.1.0)
|
||||
hana (1.3.5)
|
||||
hana (1.3.6)
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
http-accept (1.7.0)
|
||||
|
@ -231,25 +239,28 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
inflecto (0.0.2)
|
||||
jaro_winkler (1.5.4)
|
||||
jbuilder (2.10.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.4.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.3.0)
|
||||
json_pure (2.3.0)
|
||||
jwt (2.2.1)
|
||||
kaminari (1.2.0)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.0)
|
||||
kaminari-activerecord (= 1.2.0)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-actionview (1.2.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
kaminari-activerecord (= 1.2.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-actionview (1.2.1)
|
||||
actionview
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-activerecord (1.2.0)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-activerecord (1.2.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-core (1.2.0)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-core (1.2.1)
|
||||
koala (3.0.0)
|
||||
addressable
|
||||
faraday
|
||||
|
@ -272,12 +283,14 @@ GEM
|
|||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2019.1009)
|
||||
mimemagic (0.3.4)
|
||||
mime-types-data (3.2020.0512)
|
||||
mimemagic (0.3.5)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.14.0)
|
||||
minitest (5.14.1)
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.3.3)
|
||||
multi_json (1.14.1)
|
||||
multi_xml (0.6.0)
|
||||
|
@ -290,7 +303,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
os (1.1.0)
|
||||
parallel (1.19.1)
|
||||
parser (2.7.1.1)
|
||||
parser (2.7.1.2)
|
||||
ast (~> 2.4.0)
|
||||
pg (1.2.3)
|
||||
pry (0.13.1)
|
||||
|
@ -298,8 +311,8 @@ GEM
|
|||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.4)
|
||||
puma (4.3.3)
|
||||
public_suffix (4.0.5)
|
||||
puma (4.3.5)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -314,38 +327,38 @@ GEM
|
|||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.0.2.2)
|
||||
actioncable (= 6.0.2.2)
|
||||
actionmailbox (= 6.0.2.2)
|
||||
actionmailer (= 6.0.2.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
actiontext (= 6.0.2.2)
|
||||
actionview (= 6.0.2.2)
|
||||
activejob (= 6.0.2.2)
|
||||
activemodel (= 6.0.2.2)
|
||||
activerecord (= 6.0.2.2)
|
||||
activestorage (= 6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
rails (6.0.3.1)
|
||||
actioncable (= 6.0.3.1)
|
||||
actionmailbox (= 6.0.3.1)
|
||||
actionmailer (= 6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
actiontext (= 6.0.3.1)
|
||||
actionview (= 6.0.3.1)
|
||||
activejob (= 6.0.3.1)
|
||||
activemodel (= 6.0.3.1)
|
||||
activerecord (= 6.0.3.1)
|
||||
activestorage (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.2.2)
|
||||
railties (= 6.0.3.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
railties (6.0.2.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
activesupport (= 6.0.2.2)
|
||||
railties (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.1)
|
||||
rb-fsevent (0.10.3)
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.1.3)
|
||||
redis (4.1.4)
|
||||
redis-namespace (1.7.0)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack-cache (2.2.1)
|
||||
|
@ -367,15 +380,15 @@ GEM
|
|||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.4)
|
||||
rspec-core (3.9.1)
|
||||
rspec-support (~> 3.9.1)
|
||||
rspec-expectations (3.9.1)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (4.0.0)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
railties (>= 4.2)
|
||||
|
@ -383,9 +396,8 @@ GEM
|
|||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-support (3.9.2)
|
||||
rubocop (0.81.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
rspec-support (3.9.3)
|
||||
rubocop (0.83.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.0.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
|
@ -398,7 +410,7 @@ GEM
|
|||
activesupport
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
rubocop-rspec (1.38.1)
|
||||
rubocop-rspec (1.39.0)
|
||||
rubocop (>= 0.68.1)
|
||||
ruby-progressbar (1.10.1)
|
||||
sass (3.7.4)
|
||||
|
@ -406,6 +418,14 @@ GEM
|
|||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sassc (2.3.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
railties (>= 4.0.0)
|
||||
sassc (>= 2.0)
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (2.6.7)
|
||||
parser
|
||||
scss_lint (0.59.0)
|
||||
|
@ -413,12 +433,13 @@ GEM
|
|||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (2.3.0)
|
||||
sentry-raven (3.0.0)
|
||||
faraday (>= 1.0)
|
||||
shoulda-matchers (4.3.0)
|
||||
activesupport (>= 4.2.0)
|
||||
sidekiq (6.0.6)
|
||||
sidekiq (6.0.7)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
rack-protection (>= 2.0.0)
|
||||
|
@ -448,9 +469,10 @@ GEM
|
|||
faraday
|
||||
inflecto
|
||||
virtus
|
||||
telephone_number (1.4.6)
|
||||
telephone_number (1.4.7)
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
|
@ -458,9 +480,11 @@ GEM
|
|||
faraday (~> 1.0.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitty (0.1.1)
|
||||
oauth
|
||||
tzinfo (1.2.7)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2019.3)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
|
@ -480,12 +504,12 @@ GEM
|
|||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
web-console (4.0.1)
|
||||
web-console (4.0.2)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webpacker (5.0.1)
|
||||
webpacker (5.1.1)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
|
@ -505,6 +529,7 @@ PLATFORMS
|
|||
DEPENDENCIES
|
||||
action-cable-testing
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
annotate
|
||||
attr_extras
|
||||
aws-sdk-s3
|
||||
|
@ -566,7 +591,7 @@ DEPENDENCIES
|
|||
telephone_number
|
||||
time_diff
|
||||
twilio-ruby (~> 5.32.0)
|
||||
twitty!
|
||||
twitty
|
||||
tzinfo-data
|
||||
uglifier
|
||||
valid_email2
|
||||
|
@ -576,7 +601,7 @@ DEPENDENCIES
|
|||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.0p0
|
||||
ruby 2.7.1p83
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.2
|
||||
2.1.4
|
||||
|
|
1
Procfile
1
Procfile
|
@ -1,2 +1,3 @@
|
|||
release: bundle exec rake db:migrate
|
||||
web: bin/rails server -p $PORT -e $RAILS_ENV
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
|
@ -19,8 +19,8 @@ ___
|
|||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
|
||||
<img src="https://img.shields.io/github/license/chatwoot/chatwoot" alt="License">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
|
||||
<img src="https://img.shields.io/discord/647412545203994635" alt="Discord">
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/badge/chat-Discord-violet?logo=discord" alt="Chat on Discord"></a>
|
||||
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||
![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png)
|
||||
|
|
6
app.json
6
app.json
File diff suppressed because one or more lines are too long
|
@ -1 +1,4 @@
|
|||
//= link_tree ../images
|
||||
//= link administrate/application.css
|
||||
//= link administrate/application.js
|
||||
//= link dashboardChart.js
|
||||
|
|
55
app/assets/javascripts/dashboardChart.js
Normal file
55
app/assets/javascripts/dashboardChart.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
// eslint-disable-next-line
|
||||
function prepareData(data) {
|
||||
var labels = [];
|
||||
var dataSet = [];
|
||||
data.forEach(item => {
|
||||
labels.push(item[0]);
|
||||
dataSet.push(item[1]);
|
||||
});
|
||||
return { labels, dataSet };
|
||||
}
|
||||
|
||||
function getChartOptions() {
|
||||
var fontFamily =
|
||||
'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||
return {
|
||||
responsive: true,
|
||||
legend: { labels: { fontFamily } },
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.26,
|
||||
ticks: { fontFamily },
|
||||
gridLines: { display: false },
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
ticks: { fontFamily },
|
||||
gridLines: { display: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
function drawSuperAdminDashboard(data) {
|
||||
var ctx = document.getElementById('dashboard-chart').getContext('2d');
|
||||
var chartData = prepareData(data);
|
||||
// eslint-disable-next-line
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Conversations',
|
||||
data: chartData.dataSet,
|
||||
backgroundColor: '#1f93ff',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: getChartOptions(),
|
||||
});
|
||||
}
|
32
app/assets/stylesheets/administrate/application.scss
Normal file
32
app/assets/stylesheets/administrate/application.scss
Normal file
|
@ -0,0 +1,32 @@
|
|||
@charset 'utf-8';
|
||||
|
||||
@import 'reset/normalize';
|
||||
|
||||
@import 'utilities/variables';
|
||||
@import 'utilities/text-color';
|
||||
|
||||
@import 'selectize';
|
||||
@import 'datetime_picker';
|
||||
|
||||
@import 'library/clearfix';
|
||||
@import 'library/data-label';
|
||||
@import 'library/variables';
|
||||
|
||||
@import 'base/forms';
|
||||
@import 'base/layout';
|
||||
@import 'base/lists';
|
||||
@import 'base/tables';
|
||||
@import 'base/typography';
|
||||
|
||||
@import 'components/app-container';
|
||||
@import 'components/attributes';
|
||||
@import 'components/buttons';
|
||||
@import 'components/cells';
|
||||
@import 'components/field-unit';
|
||||
@import 'components/flashes';
|
||||
@import 'components/form-actions';
|
||||
@import 'components/main-content';
|
||||
@import 'components/navigation';
|
||||
@import 'components/pagination';
|
||||
@import 'components/search';
|
||||
@import 'components/reports';
|
103
app/assets/stylesheets/administrate/base/_forms.scss
Normal file
103
app/assets/stylesheets/administrate/base/_forms.scss
Normal file
|
@ -0,0 +1,103 @@
|
|||
fieldset {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: $font-weight-medium;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: $font-weight-medium;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
display: block;
|
||||
font-family: $base-font-family;
|
||||
font-size: $base-font-size;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
display: block;
|
||||
font-family: $base-font-family;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
[type="color"],
|
||||
[type="date"],
|
||||
[type="datetime-local"],
|
||||
[type="email"],
|
||||
[type="month"],
|
||||
[type="number"],
|
||||
[type="password"],
|
||||
[type="search"],
|
||||
[type="tel"],
|
||||
[type="text"],
|
||||
[type="time"],
|
||||
[type="url"],
|
||||
[type="week"],
|
||||
input:not([type]),
|
||||
textarea {
|
||||
appearance: none;
|
||||
background-color: $white;
|
||||
border: $base-border;
|
||||
border-radius: $base-border-radius;
|
||||
padding: 0.5em;
|
||||
transition: border-color $base-duration $base-timing;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: mix($black, $base-border-color, 20%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $action-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: mix($black, $white, 5%);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
border: $base-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
display: inline;
|
||||
margin-right: $small-spacing / 2;
|
||||
}
|
||||
|
||||
[type="file"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"],
|
||||
select {
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: $focus-outline-offset;
|
||||
}
|
||||
}
|
22
app/assets/stylesheets/administrate/base/_layout.scss
Normal file
22
app/assets/stylesheets/administrate/base/_layout.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
html {
|
||||
background-color: $color-white;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img,
|
||||
picture {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
19
app/assets/stylesheets/administrate/base/_lists.scss
Normal file
19
app/assets/stylesheets/administrate/base/_lists.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
ul,
|
||||
ol {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-bottom: $small-spacing;
|
||||
|
||||
dt {
|
||||
font-weight: $font-weight-medium;
|
||||
margin-top: $small-spacing;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
71
app/assets/stylesheets/administrate/base/_tables.scss
Normal file
71
app/assets/stylesheets/administrate/base/_tables.scss
Normal file
|
@ -0,0 +1,71 @@
|
|||
table {
|
||||
border-collapse: collapse;
|
||||
font-size: $font-size-default;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: $base-border;
|
||||
|
||||
th {
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
&.cell-label--avatar-field {
|
||||
a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
background-color: $base-background-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: -($focus-outline-width);
|
||||
}
|
||||
|
||||
td {
|
||||
&.cell-data--avatar-field {
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
height: $space-large;
|
||||
max-height: $space-large;
|
||||
width: $space-large;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: $space-slab;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
td:last-child,
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
td img {
|
||||
max-height: 2rem;
|
||||
}
|
44
app/assets/stylesheets/administrate/base/_typography.scss
Normal file
44
app/assets/stylesheets/administrate/base/_typography.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
body {
|
||||
color: $base-font-color;
|
||||
font-family: $base-font-family;
|
||||
font-size: $base-font-size;
|
||||
line-height: $base-line-height;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: $heading-font-family;
|
||||
font-size: $base-font-size;
|
||||
line-height: $heading-line-height;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $small-spacing;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $action-color;
|
||||
transition: color $base-duration $base-timing;
|
||||
|
||||
&:hover {
|
||||
color: mix($black, $action-color, 25%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: $focus-outline-offset;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: $base-border;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
margin: $base-spacing 0;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.app-container {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 100rem;
|
||||
min-height: 100vh;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
.attribute-label {
|
||||
@include data-label;
|
||||
clear: left;
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-top: 0.25em;
|
||||
text-align: right;
|
||||
width: calc(15% - 1rem);
|
||||
}
|
||||
|
||||
.preserve-whitespace {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.attribute-data {
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-left: 2rem;
|
||||
width: calc(85% - 1rem);
|
||||
}
|
||||
|
||||
.attribute--nested {
|
||||
border: $base-border;
|
||||
padding: $small-spacing;
|
||||
}
|
50
app/assets/stylesheets/administrate/components/_buttons.scss
Normal file
50
app/assets/stylesheets/administrate/components/_buttons.scss
Normal file
|
@ -0,0 +1,50 @@
|
|||
button,
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"],
|
||||
.button {
|
||||
appearance: none;
|
||||
background-color: $color-woot;
|
||||
border: 0;
|
||||
border-radius: $base-border-radius;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
padding: $space-one $space-two;
|
||||
text-decoration: none;
|
||||
transition: background-color $base-duration $base-timing;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: mix($black, $color-woot, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: $focus-outline-offset;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-woot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button--alt {
|
||||
background-color: transparent;
|
||||
border: $base-border;
|
||||
border-color: $blue;
|
||||
color: $blue;
|
||||
margin-bottom: $base-spacing;
|
||||
}
|
45
app/assets/stylesheets/administrate/components/_cells.scss
Normal file
45
app/assets/stylesheets/administrate/components/_cells.scss
Normal file
|
@ -0,0 +1,45 @@
|
|||
.cell-label {
|
||||
&:hover {
|
||||
a {
|
||||
color: $action-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $action-color;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
display: inline-block;
|
||||
transition: color $base-duration $base-timing;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-label--asc,
|
||||
.cell-label--desc {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.cell-label__sort-indicator {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
|
||||
svg {
|
||||
fill: $hint-grey;
|
||||
height: 13px;
|
||||
transition: transform $base-duration $base-timing;
|
||||
width: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-label__sort-indicator--desc {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.cell-data--number,
|
||||
.cell-label--number {
|
||||
text-align: right;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
.field-unit {
|
||||
@include administrate-clearfix;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: $base-spacing;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-unit__label {
|
||||
float: left;
|
||||
margin-left: 1rem;
|
||||
text-align: right;
|
||||
width: calc(15% - 1rem);
|
||||
}
|
||||
|
||||
.field-unit__field {
|
||||
float: left;
|
||||
margin-left: 2rem;
|
||||
max-width: 50rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-unit--nested {
|
||||
border: $base-border;
|
||||
margin-left: 7.5%;
|
||||
max-width: 60rem;
|
||||
padding: $small-spacing;
|
||||
width: 100%;
|
||||
|
||||
.field-unit__field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-unit__label {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.field-unit--required {
|
||||
label::after {
|
||||
color: $red;
|
||||
content: ' *';
|
||||
}
|
||||
}
|
||||
|
||||
.attribute-data--avatar-field {
|
||||
height: $space-larger;
|
||||
width: $space-larger;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
28
app/assets/stylesheets/administrate/components/_flashes.scss
Normal file
28
app/assets/stylesheets/administrate/components/_flashes.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
$base-spacing: 1.5em !default;
|
||||
$flashes: (
|
||||
"alert": #fff6bf,
|
||||
"error": #fbe3e4,
|
||||
"notice": #e5edf8,
|
||||
"success": #e6efc2,
|
||||
) !default;
|
||||
|
||||
@each $flash-type, $color in $flashes {
|
||||
.flash-#{$flash-type} {
|
||||
background-color: $color;
|
||||
color: mix($black, $color, 60%);
|
||||
display: block;
|
||||
margin-bottom: $base-spacing / 2;
|
||||
padding: $base-spacing / 2;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: mix($black, $color, 70%);
|
||||
text-decoration: underline;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: mix($black, $color, 90%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.form-actions {
|
||||
margin-left: calc(15% + 2rem);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
.main-content {
|
||||
font-size: $font-size-default;
|
||||
left: 23rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.main-content__body {
|
||||
padding: $space-two;
|
||||
}
|
||||
|
||||
.main-content__header {
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
border-bottom: 1px solid $color-border;
|
||||
display: flex;
|
||||
min-height: 5.6rem;
|
||||
padding: $space-small $space-normal;
|
||||
}
|
||||
|
||||
.main-content__page-title {
|
||||
font-size: $font-size-large;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-right: auto;
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
.logo-brand {
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal $space-smaller;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
background: $white;
|
||||
border-right: 1px solid $color-border;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-medium;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: $space-normal;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 23rem;
|
||||
z-index: 1023;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
color: $color-gray;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
i {
|
||||
min-width: $space-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__link {
|
||||
background-color: transparent;
|
||||
color: $color-gray;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: $space-smaller;
|
||||
padding: $space-one;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.navigation__link--active {
|
||||
background-color: $color-background;
|
||||
border-radius: $base-border-radius;
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
bottom: $space-normal;
|
||||
left: $space-normal;
|
||||
position: fixed;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
.pagination {
|
||||
font-size: $font-size-default;
|
||||
margin-top: $base-spacing;
|
||||
padding-left: $base-spacing;
|
||||
padding-right: $base-spacing;
|
||||
text-align: center;
|
||||
|
||||
.first,
|
||||
.prev,
|
||||
.page,
|
||||
.next,
|
||||
.last {
|
||||
margin: $small-spacing;
|
||||
}
|
||||
|
||||
.current {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
}
|
15
app/assets/stylesheets/administrate/components/_reports.scss
Normal file
15
app/assets/stylesheets/administrate/components/_reports.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
.report--list {
|
||||
display: flex;
|
||||
padding: 0 $space-two $space-larger;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
flex: 1;
|
||||
font-size: $font-size-small;
|
||||
text-align: center;
|
||||
|
||||
.metric {
|
||||
font-size: $font-size-bigger;
|
||||
font-weight: 200;
|
||||
}
|
||||
}
|
44
app/assets/stylesheets/administrate/components/_search.scss
Normal file
44
app/assets/stylesheets/administrate/components/_search.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
.search {
|
||||
margin-left: auto;
|
||||
margin-right: 2rem;
|
||||
max-width: 44rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search__input {
|
||||
background: $grey-1;
|
||||
padding-left: $space-normal * 2.5;
|
||||
padding-right: $space-normal * 2.5;
|
||||
}
|
||||
|
||||
.search__eyeglass-icon {
|
||||
fill: $grey-7;
|
||||
height: $space-normal;
|
||||
left: $space-normal;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: $space-normal;
|
||||
}
|
||||
|
||||
.search__clear-link {
|
||||
height: $space-normal;
|
||||
position: absolute;
|
||||
right: $space-normal * 0.75;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: $space-normal;
|
||||
}
|
||||
|
||||
.search__clear-icon {
|
||||
fill: $grey-5;
|
||||
height: $space-normal;
|
||||
position: absolute;
|
||||
transition: fill $base-duration $base-timing;
|
||||
width: $space-normal;
|
||||
|
||||
&:hover {
|
||||
fill: $action-color;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
@mixin administrate-clearfix {
|
||||
&::after {
|
||||
clear: both;
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
@mixin data-label {
|
||||
color: $hint-grey;
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.0357em;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
}
|
61
app/assets/stylesheets/administrate/library/_variables.scss
Normal file
61
app/assets/stylesheets/administrate/library/_variables.scss
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Typography
|
||||
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif !default;
|
||||
$heading-font-family: $base-font-family !default;
|
||||
|
||||
$base-font-size: 10px !default;
|
||||
|
||||
$base-line-height: 1.5 !default;
|
||||
$heading-line-height: 1.2 !default;
|
||||
|
||||
// Other Sizes
|
||||
$base-border-radius: 4px !default;
|
||||
$base-spacing: $base-line-height * 1em !default;
|
||||
$small-spacing: $base-spacing / 2 !default;
|
||||
|
||||
// Colors
|
||||
$white: #fff !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #1f93ff !default;
|
||||
$red: #ff382d !default;
|
||||
$light-yellow: #ffc532 !default;
|
||||
$light-green: #44ce4b !default;
|
||||
|
||||
$grey-0: #f6f7f7 !default;
|
||||
$grey-1: #f0f4f5 !default;
|
||||
$grey-2: #cfd8dc !default;
|
||||
$grey-5: #adb5bd !default;
|
||||
$grey-7: #293f54 !default;
|
||||
|
||||
$hint-grey: #7b808c !default;
|
||||
|
||||
// Font Colors
|
||||
$base-font-color: $grey-7 !default;
|
||||
$action-color: $blue !default;
|
||||
|
||||
// Background Colors
|
||||
$base-background-color: $grey-0 !default;
|
||||
|
||||
// Focus
|
||||
$focus-outline-color: transparentize($action-color, 0.4);
|
||||
$focus-outline-width: 3px;
|
||||
$focus-outline: $focus-outline-width solid $focus-outline-color;
|
||||
$focus-outline-offset: 1px;
|
||||
|
||||
// Flash Colors
|
||||
$flash-colors: (
|
||||
alert: $light-yellow,
|
||||
error: $red,
|
||||
notice: mix($white, $blue, 50%),
|
||||
success: $light-green
|
||||
);
|
||||
|
||||
// Border
|
||||
$base-border-color: $grey-1 !default;
|
||||
$base-border: 1px solid $base-border-color !default;
|
||||
|
||||
// Transitions
|
||||
$base-duration: 250ms !default;
|
||||
$base-timing: ease-in-out !default;
|
447
app/assets/stylesheets/administrate/reset/_normalize.scss
Normal file
447
app/assets/stylesheets/administrate/reset/_normalize.scss
Normal file
|
@ -0,0 +1,447 @@
|
|||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in
|
||||
* IE on Windows Phone and in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers (opinionated).
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
footer,
|
||||
header,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in IE.
|
||||
*/
|
||||
|
||||
figcaption,
|
||||
figure,
|
||||
main { /* 1 */
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct margin in IE 8.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Remove the gray background on active links in IE 10.
|
||||
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent; /* 1 */
|
||||
-webkit-text-decoration-skip: objects; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font style in Android 4.3-.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct background and color in IE 9-.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
audio,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in iOS 4-7.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10-.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the overflow in IE.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers (opinionated).
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: sans-serif; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||
* controls in Android 4.
|
||||
* 2. Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
html [type="button"], /* 1 */
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct display in IE 9-.
|
||||
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10-.
|
||||
* 2. Remove the padding in IE 10-.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in Edge, IE, and Firefox.
|
||||
*/
|
||||
|
||||
details, /* 1 */
|
||||
menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Scripting
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
canvas {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hidden
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10-.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.text-color-red {
|
||||
color: $alert-color;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
// Font sizes
|
||||
$font-size-nano: 0.8rem;
|
||||
$font-size-micro: 1.0rem;
|
||||
$font-size-mini: 1.2rem;
|
||||
$font-size-small: 1.4rem;
|
||||
$font-size-default: 1.6rem;
|
||||
$font-size-medium: 1.8rem;
|
||||
$font-size-large: 2.2rem;
|
||||
$font-size-big: 2.4rem;
|
||||
$font-size-bigger: 3.0rem;
|
||||
$font-size-mega: 3.4rem;
|
||||
$font-size-giga: 4.0rem;
|
||||
|
||||
// spaces
|
||||
$zero: 0;
|
||||
$space-micro: 0.2rem;
|
||||
$space-smaller: 0.4rem;
|
||||
$space-small: 0.8rem;
|
||||
$space-one: 1rem;
|
||||
$space-slab: 1.2rem;
|
||||
$space-normal: 1.6rem;
|
||||
$space-two: 2.0rem;
|
||||
$space-medium: 2.4rem;
|
||||
$space-large: 3.2rem;
|
||||
$space-larger: 4.8rem;
|
||||
$space-jumbo: 6.4rem;
|
||||
$space-mega: 10.0rem;
|
||||
|
||||
// font-weight
|
||||
$font-weight-feather: 100;
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-bold: 600;
|
||||
$font-weight-black: 700;
|
||||
|
||||
//Navbar
|
||||
$nav-bar-width: 23rem;
|
||||
$header-height: 5.6rem;
|
||||
|
||||
$woot-logo-padding: $space-large $space-two;
|
||||
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-background: #f4f6fb;
|
||||
$color-border-dark: #cad0d4;
|
||||
$color-background-light: #f9fafc;
|
||||
$color-white: #fff;
|
||||
$color-body: #3c4858;
|
||||
$color-heading: #1f2d3d;
|
||||
$color-extra-light-blue: #f5f7f9;
|
||||
|
||||
$primary-color: $color-woot;
|
||||
$secondary-color: #35c5ff;
|
||||
$success-color: #44ce4b;
|
||||
$warning-color: #ffc532;
|
||||
$alert-color: #ff382d;
|
||||
|
||||
$masked-bg: rgba(0, 0, 0, .4);
|
||||
|
||||
// Color-palettes
|
||||
|
||||
$color-primary-light: #c7e3ff;
|
||||
$color-primary-dark: darken($color-woot, 20%);
|
||||
|
||||
// Thumbnail
|
||||
$thumbnail-radius: 4rem;
|
||||
|
||||
// chat-header
|
||||
$conv-header-height: 4rem;
|
||||
|
||||
// Inbox List
|
||||
|
||||
$inbox-thumb-size: 4.8rem;
|
||||
|
||||
|
||||
// Spinner
|
||||
$spinkit-spinner-color: $color-white !default;
|
||||
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
|
||||
$spinkit-size: 1.6rem !default;
|
||||
|
||||
// Snackbar default
|
||||
$woot-snackbar-bg: #323232;
|
||||
$woot-snackbar-button: #ffeb3b;
|
||||
|
||||
$swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
|
||||
|
||||
// Ionicons
|
||||
$ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
|
@ -1,19 +0,0 @@
|
|||
# app/bot/facebook_bot.rb
|
||||
require 'facebook/messenger'
|
||||
include Facebook::Messenger
|
||||
|
||||
Bot.on :message do |message|
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
|
||||
Bot.on :delivery do |delivery|
|
||||
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
|
||||
# delivery.sender # => { 'id' => '1008372609250235' }
|
||||
# delivery.recipient # => { 'id' => '2015573629214912' }
|
||||
# delivery.at # => 2016-04-22 21:30:36 +0200
|
||||
# delivery.seq # => 37
|
||||
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
|
||||
updater.perform
|
||||
puts "Human was online at #{delivery.at}"
|
||||
end
|
22
app/bot/facebook_bot.rb
Normal file
22
app/bot/facebook_bot.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
require 'facebook/messenger'
|
||||
|
||||
class FacebookBot
|
||||
include Facebook::Messenger
|
||||
|
||||
Bot.on :message do |message|
|
||||
Rails.logger.info "MESSAGE_RECIEVED #{message}"
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
|
||||
Bot.on :delivery do |delivery|
|
||||
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
|
||||
# delivery.sender # => { 'id' => '1008372609250235' }
|
||||
# delivery.recipient # => { 'id' => '2015573629214912' }
|
||||
# delivery.at # => 2016-04-22 21:30:36 +0200
|
||||
# delivery.seq # => 37
|
||||
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
|
||||
updater.perform
|
||||
Rails.logger.info "Human was online at #{delivery.at}"
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!]
|
||||
pattr_initialize [:account_name!, :email!, :confirmed!]
|
||||
|
||||
def perform
|
||||
validate_email
|
||||
|
@ -38,6 +38,7 @@ class AccountBuilder
|
|||
|
||||
def create_account
|
||||
@account = Account.create!(name: @account_name)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
def create_and_link_user
|
||||
|
@ -46,6 +47,7 @@ class AccountBuilder
|
|||
password: password,
|
||||
password_confirmation: password,
|
||||
name: email_to_name(@email))
|
||||
@user.confirm if @confirmed
|
||||
if @user.save!
|
||||
link_user_to_account(@user, @account)
|
||||
@user
|
||||
|
|
|
@ -14,6 +14,18 @@ class ContactBuilder
|
|||
@account ||= inbox.account
|
||||
end
|
||||
|
||||
def create_contact_inbox(contact)
|
||||
::ContactInbox.create!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id,
|
||||
source_id: source_id
|
||||
)
|
||||
end
|
||||
|
||||
def update_contact_avatar(contact)
|
||||
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
||||
end
|
||||
|
||||
def build_contact
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = account.contacts.create!(
|
||||
|
@ -23,16 +35,12 @@ class ContactBuilder
|
|||
identifier: contact_attributes[:identifier],
|
||||
additional_attributes: contact_attributes[:additional_attributes]
|
||||
)
|
||||
contact_inbox = ::ContactInbox.create!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id,
|
||||
source_id: source_id
|
||||
)
|
||||
|
||||
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
||||
contact_inbox = create_contact_inbox(contact)
|
||||
update_contact_avatar(contact)
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
Rails.logger e
|
||||
Rails.logger.info e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,8 +5,8 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
|
|||
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||
CustomExceptions::Account::UserExists,
|
||||
|
@ -16,11 +16,12 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
|
|||
def create
|
||||
@user = AccountBuilder.new(
|
||||
account_name: account_params[:account_name],
|
||||
email: account_params[:email]
|
||||
email: account_params[:email],
|
||||
confirmed: confirmed?
|
||||
).perform
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render 'devise/auth.json', locals: { resource: @user }
|
||||
render partial: 'devise/auth.json', locals: { resource: @user }
|
||||
else
|
||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||
end
|
||||
|
@ -34,14 +35,25 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
|
|||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled))
|
||||
end
|
||||
|
||||
def update_active_at
|
||||
@current_account_user.active_at = Time.now.utc
|
||||
@current_account_user.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(Account)
|
||||
end
|
||||
|
||||
def confirmed?
|
||||
super_admin? && params[:confirmed]
|
||||
end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
def account_params
|
||||
|
@ -51,4 +63,12 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
|
|||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: current_user,
|
||||
account: @account,
|
||||
account_user: @current_account_user
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,18 +10,18 @@ class Api::V1::Accounts::AgentsController < Api::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@agent.account_user.destroy
|
||||
@agent.current_account_user.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.except(:role))
|
||||
@agent.account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||
render 'api/v1/models/user.json', locals: { resource: @agent }
|
||||
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
|
||||
end
|
||||
|
||||
def create
|
||||
render 'api/v1/models/user.json', locals: { resource: @user }
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -14,7 +14,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
|
|||
@facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger e
|
||||
Rails.logger.info e
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -62,7 +62,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
|
|||
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||
rescue StandardError => e
|
||||
Rails.logger e
|
||||
Rails.logger.info e
|
||||
end
|
||||
|
||||
def mark_already_existing_facebook_pages(data)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
|
||||
before_action :current_account
|
||||
before_action :authorize_request
|
||||
|
||||
def create
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class Api::V1::Accounts::ConversationsController < Api::BaseController
|
||||
include Events::Types
|
||||
before_action :current_account
|
||||
before_action :conversation, except: [:index]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
|
||||
|
@ -20,9 +21,19 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
|
|||
|
||||
def show; end
|
||||
|
||||
def mute
|
||||
@conversation.mute!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@status = @conversation.save
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_typing_status
|
||||
if params[:typing_status] == 'on'
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
class Api::V1::Accounts::InboxesController < Api::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :current_account
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@inboxes = policy_scope(current_account.inboxes)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Api::V1::Accounts::WebhooksController < Api::BaseController
|
||||
before_action :current_account
|
||||
before_action :check_authorization
|
||||
before_action :fetch_webhook, only: [:update, :destroy]
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
before_action :set_user
|
||||
|
||||
def show
|
||||
render json: @user
|
||||
render partial: 'api/v1/models/user.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
|
||||
def update
|
||||
|
|
|
@ -3,6 +3,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
||||
def index
|
||||
@conversation = conversation
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
|
|
|
@ -11,10 +11,6 @@ class Api::V2::Accounts::ReportsController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def current_account
|
||||
current_user.account
|
||||
end
|
||||
|
||||
def account_summary_params
|
||||
{
|
||||
type: :account,
|
||||
|
|
|
@ -14,7 +14,8 @@ class ApplicationController < ActionController::Base
|
|||
private
|
||||
|
||||
def current_account
|
||||
@_ ||= find_current_account
|
||||
@current_account ||= find_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
def find_current_account
|
||||
|
@ -29,11 +30,17 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def switch_locale(account)
|
||||
I18n.locale = (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil) || I18n.default_locale
|
||||
# priority is for locale set in query string (mostly for widget/from js sdk)
|
||||
locale ||= (I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil)
|
||||
# if local is not set in param, lets try account
|
||||
locale ||= (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil)
|
||||
I18n.locale = locale || I18n.default_locale
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
|
@ -98,4 +105,12 @@ class ApplicationController < ActionController::Base
|
|||
render json: { error: 'Account Suspended' }, status: :account_suspended
|
||||
end
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: Current.user,
|
||||
account: Current.account,
|
||||
account_user: Current.account_user
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,17 +4,25 @@ module AccessTokenAuthHelper
|
|||
'api/v1/accounts/conversations/messages' => ['create']
|
||||
}.freeze
|
||||
|
||||
def authenticate_access_token!
|
||||
def ensure_access_token
|
||||
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
|
||||
access_token = AccessToken.find_by(token: token)
|
||||
render_unauthorized('Invalid Access Token') && return unless access_token
|
||||
@access_token = AccessToken.find_by(token: token) if token.present?
|
||||
end
|
||||
|
||||
token_owner = access_token.owner
|
||||
@resource = token_owner
|
||||
def authenticate_access_token!
|
||||
ensure_access_token
|
||||
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
|
||||
|
||||
@resource = @access_token.owner
|
||||
end
|
||||
|
||||
def super_admin?
|
||||
@resource.present? && @resource.is_a?(SuperAdmin)
|
||||
end
|
||||
|
||||
def validate_bot_access_token!
|
||||
return if current_user.is_a?(User)
|
||||
return if super_admin?
|
||||
return if agent_bot_accessible?
|
||||
|
||||
render_unauthorized('Access to this endpoint is not authorized for bots')
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
class DashboardController < ActionController::Base
|
||||
before_action :set_global_config
|
||||
|
||||
layout 'vueapp'
|
||||
|
||||
def index; end
|
||||
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get(
|
||||
'LOGO',
|
||||
'LOGO_THUMBNAIL',
|
||||
'INSTALLATION_NAME',
|
||||
'WIDGET_BRAND_URL',
|
||||
'TERMS_URL',
|
||||
'PRIVACY_URL',
|
||||
'DISPLAY_MANIFEST'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||
@recoverable = User.find_by(reset_password_token: reset_password_token)
|
||||
if @recoverable && reset_password_and_confirmation(@recoverable)
|
||||
send_auth_headers(@recoverable)
|
||||
render 'devise/auth.json', locals: { resource: @recoverable }
|
||||
render partial: 'devise/auth.json', locals: { resource: @recoverable }
|
||||
else
|
||||
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
|
||||
end
|
||||
|
|
|
@ -4,6 +4,6 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
|||
wrap_parameters format: []
|
||||
|
||||
def render_create_success
|
||||
render 'devise/auth.json', locals: { resource: @resource }
|
||||
render partial: 'devise/auth.json', locals: { resource: @resource }
|
||||
end
|
||||
end
|
||||
|
|
44
app/controllers/super_admin/access_tokens_controller.rb
Normal file
44
app/controllers/super_admin/access_tokens_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class SuperAdmin::AccessTokensController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
56
app/controllers/super_admin/account_users_controller.rb
Normal file
56
app/controllers/super_admin/account_users_controller.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
class SuperAdmin::AccountUsersController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
def create
|
||||
resource = resource_class.new(resource_params)
|
||||
authorize_resource(resource)
|
||||
|
||||
notice = resource.save ? translate_with_resource('create.success') : resource.errors.full_messages.first
|
||||
redirect_back(fallback_location: [namespace, resource.account], notice: notice)
|
||||
end
|
||||
|
||||
def destroy
|
||||
if requested_resource.destroy
|
||||
flash[:notice] = translate_with_resource('destroy.success')
|
||||
else
|
||||
flash[:error] = requested_resource.errors.full_messages.join('<br/>')
|
||||
end
|
||||
redirect_back(fallback_location: [namespace, requested_resource.account])
|
||||
end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
44
app/controllers/super_admin/accounts_controller.rb
Normal file
44
app/controllers/super_admin/accounts_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
44
app/controllers/super_admin/agent_bots_controller.rb
Normal file
44
app/controllers/super_admin/agent_bots_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class SuperAdmin::AgentBotsController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
23
app/controllers/super_admin/application_controller.rb
Normal file
23
app/controllers/super_admin/application_controller.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# All Administrate controllers inherit from this
|
||||
# `Administrate::ApplicationController`, making it the ideal place to put
|
||||
# authentication logic or other before_actions.
|
||||
#
|
||||
# If you want to add pagination or other controller-level concerns,
|
||||
# you're free to overwrite the RESTful controller actions.
|
||||
class SuperAdmin::ApplicationController < Administrate::ApplicationController
|
||||
# authenticiation done via devise : SuperAdmin Model
|
||||
before_action :authenticate_super_admin!
|
||||
|
||||
# Override this value to specify the number of elements to display at a time
|
||||
# on index pages. Defaults to 20.
|
||||
# def records_per_page
|
||||
# params[:per_page] || 20
|
||||
# end
|
||||
|
||||
def order
|
||||
@order ||= Administrate::Order.new(
|
||||
params.fetch(resource_name, {}).fetch(:order, 'id'),
|
||||
params.fetch(resource_name, {}).fetch(:direction, 'desc')
|
||||
)
|
||||
end
|
||||
end
|
12
app/controllers/super_admin/dashboard_controller.rb
Normal file
12
app/controllers/super_admin/dashboard_controller.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
def index
|
||||
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
|
||||
@accounts_count = number_with_delimiter(Account.all.length)
|
||||
@users_count = number_with_delimiter(User.all.length)
|
||||
@inboxes_count = number_with_delimiter(Inbox.all.length)
|
||||
@conversations_count = number_with_delimiter(Conversation.all.length)
|
||||
@messages_count = number_with_delimiter(Message.all.length)
|
||||
end
|
||||
end
|
33
app/controllers/super_admin/devise/sessions_controller.rb
Normal file
33
app/controllers/super_admin/devise/sessions_controller.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SuperAdmin::Devise::SessionsController < Devise::SessionsController
|
||||
def new
|
||||
self.resource = resource_class.new(sign_in_params)
|
||||
end
|
||||
|
||||
def create
|
||||
redirect_to(super_admin_session_path, flash: { error: @error_message }) && return unless valid_credentials?
|
||||
|
||||
sign_in(@super_admin, scope: :super_admin)
|
||||
flash.discard
|
||||
redirect_to super_admin_users_path
|
||||
end
|
||||
|
||||
def destroy
|
||||
sign_out
|
||||
flash.discard
|
||||
redirect_to '/'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_credentials?
|
||||
@super_admin = SuperAdmin.find_by!(email: params[:super_admin][:email])
|
||||
raise StandardError, 'Invalid Password' unless @super_admin.valid_password?(params[:super_admin][:password])
|
||||
|
||||
true
|
||||
rescue StandardError => e
|
||||
@error_message = e.message
|
||||
false
|
||||
end
|
||||
end
|
44
app/controllers/super_admin/super_admins_controller.rb
Normal file
44
app/controllers/super_admin/super_admins_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class SuperAdmin::SuperAdminsController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
44
app/controllers/super_admin/users_controller.rb
Normal file
44
app/controllers/super_admin/users_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
|
@ -2,18 +2,18 @@ class Twitter::CallbacksController < Twitter::BaseController
|
|||
def show
|
||||
return redirect_to twitter_app_redirect_url if permitted_params[:denied]
|
||||
|
||||
@response = twitter_client.access_token(
|
||||
oauth_token: permitted_params[:oauth_token],
|
||||
oauth_verifier: permitted_params[:oauth_verifier]
|
||||
)
|
||||
if @response.status == '200'
|
||||
inbox = build_inbox
|
||||
@response = ensure_access_token
|
||||
return redirect_to twitter_app_redirect_url if @response.status != '200'
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
inbox = create_inbox
|
||||
::Redis::Alfred.delete(permitted_params[:oauth_token])
|
||||
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
|
||||
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
else
|
||||
redirect_to twitter_app_redirect_url
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
redirect_to twitter_app_redirect_url
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -34,8 +34,14 @@ class Twitter::CallbacksController < Twitter::BaseController
|
|||
app_new_twitter_inbox_url(account_id: account.id)
|
||||
end
|
||||
|
||||
def build_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
def ensure_access_token
|
||||
twitter_client.access_token(
|
||||
oauth_token: permitted_params[:oauth_token],
|
||||
oauth_verifier: permitted_params[:oauth_verifier]
|
||||
)
|
||||
end
|
||||
|
||||
def create_inbox
|
||||
twitter_profile = account.twitter_profiles.create(
|
||||
twitter_access_token: parsed_body['oauth_token'],
|
||||
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
||||
|
@ -45,9 +51,6 @@ class Twitter::CallbacksController < Twitter::BaseController
|
|||
name: parsed_body['screen_name'],
|
||||
channel: twitter_profile
|
||||
)
|
||||
rescue StandardError => e
|
||||
Rails.logger e
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class WidgetsController < ActionController::Base
|
||||
before_action :set_global_config
|
||||
before_action :set_web_widget
|
||||
before_action :set_token
|
||||
before_action :set_contact
|
||||
|
@ -8,6 +9,10 @@ class WidgetsController < ActionController::Base
|
|||
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL')
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
end
|
||||
|
|
70
app/dashboards/access_token_dashboard.rb
Normal file
70
app/dashboards/access_token_dashboard.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
require 'administrate/base_dashboard'
|
||||
|
||||
class AccessTokenDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
owner: Field::Polymorphic,
|
||||
id: Field::Number,
|
||||
token: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
owner
|
||||
id
|
||||
token
|
||||
created_at
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
owner
|
||||
id
|
||||
token
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
owner
|
||||
token
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {
|
||||
user: ->(resources) { resources.where(owner_type: 'User') },
|
||||
super_admin: ->(resources) { resources.where(owner_type: 'SuperAdmin') },
|
||||
agent_bot: ->(resources) { resources.where(owner_type: 'AgentBot') }
|
||||
}.freeze
|
||||
|
||||
# Overwrite this method to customize how access tokens are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
# def display_resource(access_token)
|
||||
# "AccessToken ##{access_token.id}"
|
||||
# end
|
||||
end
|
72
app/dashboards/account_dashboard.rb
Normal file
72
app/dashboards/account_dashboard.rb
Normal file
|
@ -0,0 +1,72 @@
|
|||
require 'administrate/base_dashboard'
|
||||
|
||||
class AccountDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
users: CountField,
|
||||
conversations: CountField,
|
||||
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
|
||||
account_users: Field::HasMany
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
name
|
||||
locale
|
||||
users
|
||||
conversations
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
name
|
||||
created_at
|
||||
updated_at
|
||||
locale
|
||||
conversations
|
||||
account_users
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
locale
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how accounts are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
def display_resource(account)
|
||||
"##{account.id} #{account.name}"
|
||||
end
|
||||
end
|
71
app/dashboards/account_user_dashboard.rb
Normal file
71
app/dashboards/account_user_dashboard.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
require 'administrate/base_dashboard'
|
||||
|
||||
class AccountUserDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||
user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||
inviter: Field::BelongsTo.with_options(class_name: 'User', searchable: true, searchable_field: 'name'),
|
||||
id: Field::Number,
|
||||
role: Field::Select.with_options(collection: AccountUser.roles.keys),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
account
|
||||
user
|
||||
inviter
|
||||
role
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
account
|
||||
user
|
||||
inviter
|
||||
id
|
||||
role
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
account
|
||||
user
|
||||
role
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how account users are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
def display_resource(account_user)
|
||||
"AccountUser ##{account_user.id}"
|
||||
end
|
||||
end
|
73
app/dashboards/agent_bot_dashboard.rb
Normal file
73
app/dashboards/agent_bot_dashboard.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
require 'administrate/base_dashboard'
|
||||
|
||||
class AgentBotDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
access_token: Field::HasOne,
|
||||
avatar_url: AvatarField,
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
description: Field::String,
|
||||
outgoing_url: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
hide_input_for_bot_conversations: Field::Boolean
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
name
|
||||
outgoing_url
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
name
|
||||
description
|
||||
outgoing_url
|
||||
hide_input_for_bot_conversations
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
description
|
||||
outgoing_url
|
||||
hide_input_for_bot_conversations
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how agent bots are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
# def display_resource(agent_bot)
|
||||
# "AgentBot ##{agent_bot.id}"
|
||||
# end
|
||||
end
|
77
app/dashboards/super_admin_dashboard.rb
Normal file
77
app/dashboards/super_admin_dashboard.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
require 'administrate/base_dashboard'
|
||||
|
||||
class SuperAdminDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
email: Field::String,
|
||||
password: Field::Password,
|
||||
access_token: Field::HasOne,
|
||||
remember_created_at: Field::DateTime,
|
||||
sign_in_count: Field::Number,
|
||||
current_sign_in_at: Field::DateTime,
|
||||
last_sign_in_at: Field::DateTime,
|
||||
current_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
last_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
access_token
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
remember_created_at
|
||||
sign_in_count
|
||||
current_sign_in_at
|
||||
last_sign_in_at
|
||||
current_sign_in_ip
|
||||
last_sign_in_ip
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
email
|
||||
password
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how super admins are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
# def display_resource(super_admin)
|
||||
# "SuperAdmin ##{super_admin.id}"
|
||||
# end
|
||||
end
|
91
app/dashboards/user_dashboard.rb
Normal file
91
app/dashboards/user_dashboard.rb
Normal file
|
@ -0,0 +1,91 @@
|
|||
require 'administrate/base_dashboard'
|
||||
|
||||
class UserDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
account_users: Field::HasMany,
|
||||
id: Field::Number,
|
||||
avatar_url: AvatarField,
|
||||
provider: Field::String,
|
||||
uid: Field::String,
|
||||
password: Field::Password,
|
||||
sign_in_count: Field::Number,
|
||||
current_sign_in_at: Field::DateTime,
|
||||
last_sign_in_at: Field::DateTime,
|
||||
current_sign_in_ip: Field::String,
|
||||
last_sign_in_ip: Field::String,
|
||||
confirmation_token: Field::String,
|
||||
confirmed_at: Field::DateTime,
|
||||
confirmation_sent_at: Field::DateTime,
|
||||
unconfirmed_email: Field::String,
|
||||
name: Field::String,
|
||||
nickname: Field::String,
|
||||
email: Field::String,
|
||||
tokens: Field::String.with_options(searchable: false),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
pubsub_token: Field::String,
|
||||
accounts: CountField
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
name
|
||||
email
|
||||
accounts
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
unconfirmed_email
|
||||
name
|
||||
nickname
|
||||
email
|
||||
created_at
|
||||
updated_at
|
||||
account_users
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
nickname
|
||||
email
|
||||
password
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how users are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
def display_resource(user)
|
||||
"##{user.id} #{user.name}"
|
||||
end
|
||||
end
|
7
app/fields/avatar_field.rb
Normal file
7
app/fields/avatar_field.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require 'administrate/field/base'
|
||||
|
||||
class AvatarField < Administrate::Field::Base
|
||||
def avatar_url
|
||||
data.presence || '/admin/avatar.png'
|
||||
end
|
||||
end
|
7
app/fields/count_field.rb
Normal file
7
app/fields/count_field.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require 'administrate/field/base'
|
||||
|
||||
class CountField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.count
|
||||
end
|
||||
end
|
|
@ -39,6 +39,10 @@ class ConversationApi extends ApiClient {
|
|||
typing_status: status,
|
||||
});
|
||||
}
|
||||
|
||||
mute(conversationId) {
|
||||
return axios.post(`${this.url}/${conversationId}/mute`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConversationApi();
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 15 KiB |
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="504px" height="470px" viewBox="0 0 504 470" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 44 (41411) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>canned</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="canned" fill-rule="nonzero">
|
||||
<path d="M329.386,362.733 L39.253,362.733 C20.48,362.733 5.12,347.373 5.12,328.6 L5.12,38.467 C5.12,19.694 20.48,4.334 39.253,4.334 L465.92,4.334 C484.693,4.334 500.053,19.694 500.053,38.467 L500.053,328.6 C500.053,347.373 484.693,362.733 465.92,362.733 L431.787,362.733 L431.787,465.133 L329.386,362.733 Z" id="Shape" fill="#B9ECFF"></path>
|
||||
<path d="M431.786,469.4 C430.933,469.4 429.226,468.547 428.373,468.547 L308.907,349.08 C307.2,347.373 307.2,344.813 308.907,343.107 C310.614,341.4 313.174,341.4 314.88,343.107 L426.667,454.894 L426.667,362.734 C426.667,360.174 428.374,358.467 430.934,358.467 L465.067,358.467 C481.28,358.467 494.934,344.814 494.934,328.6 L494.934,38.467 C494.934,22.254 481.281,8.6 465.067,8.6 L38.4,8.6 C22.187,8.6 8.533,22.253 8.533,38.467 L8.533,328.6 C8.533,344.813 22.186,358.467 38.4,358.467 L285.867,358.467 C288.427,358.467 290.134,360.174 290.134,362.734 C290.134,365.294 288.427,367.001 285.867,367.001 L38.4,367.001 C17.067,367 0,349.933 0,328.6 L0,38.467 C0,17.134 17.067,0.067 38.4,0.067 L465.067,0.067 C486.4,0.067 503.467,17.134 503.467,38.467 L503.467,328.6 C503.467,349.933 486.4,367 465.067,367 L435.2,367 L435.2,465.133 C435.2,466.84 434.347,468.546 432.64,469.4 C432.64,469.4 432.64,469.4 431.786,469.4 Z M397.653,264.6 L295.253,264.6 C292.693,264.6 290.986,262.893 290.986,260.333 C290.986,257.773 292.693,256.066 295.253,256.066 L397.653,256.066 C400.213,256.066 401.92,257.773 401.92,260.333 C401.92,262.893 400.213,264.6 397.653,264.6 Z M261.12,264.6 L107.52,264.6 C104.96,264.6 103.253,262.893 103.253,260.333 C103.253,257.773 104.96,256.066 107.52,256.066 L261.12,256.066 C263.68,256.066 265.387,257.773 265.387,260.333 C265.387,262.893 263.68,264.6 261.12,264.6 Z M380.586,213.4 L278.186,213.4 C275.626,213.4 273.919,211.693 273.919,209.133 C273.919,206.573 275.626,204.866 278.186,204.866 L380.586,204.866 C383.146,204.866 384.853,206.573 384.853,209.133 C384.853,211.693 383.147,213.4 380.586,213.4 Z M244.053,213.4 L107.52,213.4 C104.96,213.4 103.253,211.693 103.253,209.133 C103.253,206.573 104.96,204.866 107.52,204.866 L244.053,204.866 C246.613,204.866 248.32,206.573 248.32,209.133 C248.32,211.693 246.613,213.4 244.053,213.4 Z M397.653,162.2 L346.453,162.2 C343.893,162.2 342.186,160.493 342.186,157.933 C342.186,155.373 343.893,153.666 346.453,153.666 L397.653,153.666 C400.213,153.666 401.92,155.373 401.92,157.933 C401.92,160.493 400.213,162.2 397.653,162.2 Z M312.32,162.2 L244.053,162.2 C241.493,162.2 239.786,160.493 239.786,157.933 C239.786,155.373 241.493,153.666 244.053,153.666 L312.32,153.666 C314.88,153.666 316.587,155.373 316.587,157.933 C316.586,160.493 314.88,162.2 312.32,162.2 Z M209.92,162.2 L107.52,162.2 C104.96,162.2 103.253,160.493 103.253,157.933 C103.253,155.373 104.96,153.666 107.52,153.666 L209.92,153.666 C212.48,153.666 214.187,155.373 214.187,157.933 C214.186,160.493 212.48,162.2 209.92,162.2 Z M380.586,111 L209.92,111 C207.36,111 205.653,109.293 205.653,106.733 C205.653,104.173 207.36,102.466 209.92,102.466 L380.587,102.466 C383.147,102.466 384.854,104.173 384.854,106.733 C384.853,109.293 383.147,111 380.586,111 Z M175.786,111 L107.52,111 C104.96,111 103.253,109.293 103.253,106.733 C103.253,104.173 104.96,102.466 107.52,102.466 L175.787,102.466 C178.347,102.466 180.054,104.173 180.054,106.733 C180.053,109.293 178.346,111 175.786,111 Z" id="Shape" fill="#6C6C6C"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.8 KiB |
|
@ -59,6 +59,9 @@ $secondary-color: #35c5ff;
|
|||
$success-color: #44ce4b;
|
||||
$warning-color: #ffc532;
|
||||
$alert-color: #ff382d;
|
||||
|
||||
$masked-bg: rgba(0, 0, 0, .4);
|
||||
|
||||
// Color-palettes
|
||||
|
||||
$color-primary-light: #c7e3ff;
|
||||
|
@ -70,8 +73,6 @@ $thumbnail-radius: 4rem;
|
|||
// chat-header
|
||||
$conv-header-height: 4rem;
|
||||
|
||||
// login
|
||||
|
||||
// Inbox List
|
||||
|
||||
$inbox-thumb-size: 4.8rem;
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
@import 'foundation-custom';
|
||||
@import 'widgets/billing';
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/colorpicker';
|
||||
@import 'widgets/conv-header';
|
||||
@import 'widgets/conversation-card';
|
||||
@import 'widgets/conversation-view';
|
||||
|
|
|
@ -91,7 +91,6 @@
|
|||
}
|
||||
|
||||
.sidebar-labels-wrap {
|
||||
|
||||
&.has-edited,
|
||||
&:hover {
|
||||
.multiselect {
|
||||
|
@ -108,8 +107,6 @@
|
|||
}
|
||||
|
||||
.multiselect {
|
||||
margin-top: $space-small;
|
||||
|
||||
>.multiselect__select {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
15
app/javascript/dashboard/assets/scss/super_admin/index.scss
Normal file
15
app/javascript/dashboard/assets/scss/super_admin/index.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
@import '../variables';
|
||||
|
||||
.superadmin-body {
|
||||
background: $color-background;
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
background-color: $alert-color;
|
||||
border-radius: 5px;
|
||||
color: $color-white;
|
||||
font-size: 14px;
|
||||
margin-bottom: 14px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
@import 'shared/assets/fonts/inter';
|
||||
@import '../variables';
|
||||
@import '~ionicons/scss/ionicons';
|
|
@ -1,4 +1,6 @@
|
|||
.button {
|
||||
margin-bottom: 0;
|
||||
|
||||
&.icon {
|
||||
padding-left: $space-normal;
|
||||
padding-right: $space-normal;
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.widget-color--selector.vc-compact {
|
||||
border: 1px solid $color-border;
|
||||
box-shadow: none;
|
||||
margin-bottom: $space-normal;
|
||||
width: 356px;
|
||||
|
||||
.vc-compact-color-item {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
.modal-mask {
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.modal-mask {
|
||||
@include flex;
|
||||
@include flex-align(center, middle);
|
||||
background-color: $color-white;
|
||||
background-color: $masked-bg;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
|
@ -19,8 +21,8 @@
|
|||
line-height: $space-normal;
|
||||
padding: $space-normal $space-two;
|
||||
position: absolute;
|
||||
right: $space-large;
|
||||
top: $space-large;
|
||||
right: $space-micro;
|
||||
top: $space-micro;
|
||||
|
||||
&:hover {
|
||||
background: $color-background;
|
||||
|
@ -29,7 +31,7 @@
|
|||
|
||||
|
||||
.page-top-bar {
|
||||
@include padding($zero $space-two);
|
||||
@include padding($space-large $space-large $zero);
|
||||
|
||||
img {
|
||||
max-height: 6rem;
|
||||
|
@ -37,8 +39,10 @@
|
|||
}
|
||||
|
||||
.modal-container {
|
||||
@include normal-shadow;
|
||||
|
||||
background-color: $color-white;
|
||||
border-radius: $space-small;
|
||||
border-radius: $space-smaller;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
@ -52,21 +56,21 @@
|
|||
|
||||
|
||||
h2 {
|
||||
color: $color-heading;
|
||||
font-size: $font-size-medium;
|
||||
color: $color-woot;
|
||||
font-weight: $font-weight-normal;
|
||||
@include padding($space-small $zero $zero $zero);
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-size-small;
|
||||
@include padding($zero);
|
||||
@include margin($zero);
|
||||
@include padding($zero);
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
form {
|
||||
@include padding($space-large);
|
||||
align-self: center;
|
||||
@include padding($space-two);
|
||||
|
||||
a {
|
||||
@include padding($space-normal);
|
||||
}
|
||||
|
@ -74,15 +78,16 @@
|
|||
|
||||
.modal-footer {
|
||||
@include flex;
|
||||
@include flex-align($x: justify, $y: middle);
|
||||
@include padding($space-small $zero $space-medium $zero);
|
||||
@include flex-align($x: flex-start, $y: middle);
|
||||
@include padding($space-small $zero);
|
||||
|
||||
button {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-item {
|
||||
@include padding($space-normal);
|
||||
@include padding($space-large);
|
||||
button {
|
||||
@include margin($zero);
|
||||
}
|
||||
|
@ -90,12 +95,12 @@
|
|||
|
||||
}
|
||||
|
||||
.modal-enter, .modal-leave {
|
||||
.modal-enter,
|
||||
.modal-leave {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave .modal-container {
|
||||
-webkit-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
@include border-light;
|
||||
display: block;
|
||||
left: 18%;
|
||||
top: -110%;
|
||||
top: -110px;
|
||||
visibility: visible;
|
||||
width: 80%;
|
||||
z-index: 999;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
table {
|
||||
font-size: $font-size-small;
|
||||
border-spacing: 0;
|
||||
font-size: $font-size-small;
|
||||
|
||||
thead {
|
||||
th {
|
||||
|
@ -10,9 +10,12 @@ table {
|
|||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid $color-border-light;
|
||||
}
|
||||
|
||||
td {
|
||||
@include padding($space-one $space-small);
|
||||
border-bottom: 1px solid $color-border-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,22 +35,22 @@ table {
|
|||
}
|
||||
|
||||
.agent-name {
|
||||
font-weight: $font-weight-medium;
|
||||
display: block;
|
||||
font-weight: $font-weight-medium;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.woot-thumbnail {
|
||||
border-radius: 50%;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
min-width: 20rem;
|
||||
@include flex;
|
||||
@include flex-align(left, null);
|
||||
@include flex;
|
||||
flex-direction: row;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
|
|
@ -157,11 +157,7 @@ export default {
|
|||
} else {
|
||||
copyList = this.allChatList.slice();
|
||||
}
|
||||
const sorted = copyList.sort(
|
||||
(a, b) =>
|
||||
this.lastMessage(b).created_at - this.lastMessage(a).created_at
|
||||
);
|
||||
return sorted;
|
||||
return copyList;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
transition="modal"
|
||||
@click="onBackDropClick"
|
||||
>
|
||||
<i class="ion-android-close modal--close" @click="close"></i>
|
||||
<div class="modal-container" :class="className" @click.stop>
|
||||
<i class="ion-android-close modal--close" @click="close"></i>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="row settings--section">
|
||||
<div class="medium-4">
|
||||
<div class="medium-4 small-12">
|
||||
<p class="sub-block-title">
|
||||
{{ title }}
|
||||
</p>
|
||||
|
@ -8,7 +8,7 @@
|
|||
{{ subTitle }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="medium-6">
|
||||
<div class="medium-6 small-12">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
/* eslint no-plusplus: 0 */
|
||||
/* eslint-env browser */
|
||||
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
|
||||
import Bar from './widgets/chart/BarChart';
|
||||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
import LoadingState from './widgets/LoadingState';
|
||||
import Modal from './Modal';
|
||||
import ModalHeader from './ModalHeader';
|
||||
import ReportStatsCard from './widgets/ReportStatsCard';
|
||||
import SidemenuIcon from './SidemenuIcon';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import SubmitButton from './buttons/FormSubmitButton';
|
||||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
AvatarUploader,
|
||||
Bar,
|
||||
Code,
|
||||
ColorPicker,
|
||||
DeleteModal,
|
||||
Input,
|
||||
LoadingState,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
|
@ -25,6 +33,7 @@ const WootUIKit = {
|
|||
SubmitButton,
|
||||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<aside class="sidebar animated shrink columns">
|
||||
<div class="logo">
|
||||
<router-link :to="dashboardPath" replace>
|
||||
<img src="~dashboard/assets/images/woot-logo.svg" alt="Woot-logo" />
|
||||
<img :src="globalConfig.logo" :alt="globalConfig.installationName" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
|||
:key="item.toState"
|
||||
:menu-item="item"
|
||||
/>
|
||||
|
||||
<sidebar-item
|
||||
v-if="shouldShowInboxes"
|
||||
:key="inboxSection.toState"
|
||||
|
@ -30,7 +29,7 @@
|
|||
:button-text="$t('APP_GLOBAL.TRAIL_BUTTON')"
|
||||
:button-route="{ name: 'billing' }"
|
||||
:type="statusBarClass"
|
||||
:show-button="isAdmin()"
|
||||
:show-button="isAdmin"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
|
@ -42,6 +41,14 @@
|
|||
class="dropdown-pane top"
|
||||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<li v-if="currentUser.accounts.length > 1">
|
||||
<button
|
||||
class="button clear change-accounts--button"
|
||||
@click="changeAccount"
|
||||
>
|
||||
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
|
@ -62,13 +69,35 @@
|
|||
{{ currentUser.name }}
|
||||
</h3>
|
||||
<h5 class="current-user--role">
|
||||
{{ currentUser.role }}
|
||||
{{ currentRole }}
|
||||
</h5>
|
||||
</div>
|
||||
<span class="current-user--options icon ion-android-more-vertical">
|
||||
</span>
|
||||
<span class="current-user--options icon ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show="showAccountModal"
|
||||
:on-close="onClose"
|
||||
class="account-selector--modal"
|
||||
>
|
||||
<woot-modal-header
|
||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
<div
|
||||
v-for="account in currentUser.accounts"
|
||||
:key="account.id"
|
||||
class="account-selector"
|
||||
>
|
||||
<a :href="`/app/accounts/${account.id}/dashboard`">
|
||||
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
|
||||
<label :for="account.name" class="account--details">
|
||||
<div class="account--name">{{ account.name }}</div>
|
||||
<div class="account--role">{{ account.role }}</div>
|
||||
</label>
|
||||
</a>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
|
@ -82,7 +111,7 @@ import SidebarItem from './SidebarItem';
|
|||
import WootStatusBar from '../widgets/StatusBar';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import Thumbnail from '../widgets/Thumbnail';
|
||||
import sidemenuItems from '../../i18n/default-sidebar';
|
||||
import { getSidebarItems } from '../../i18n/default-sidebar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -100,23 +129,30 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
daysLeft: 'getTrialLeft',
|
||||
subscriptionData: 'getSubscription',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
currentUser: 'getCurrentUser',
|
||||
daysLeft: 'getTrialLeft',
|
||||
globalConfig: 'globalConfig/get',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
subscriptionData: 'getSubscription',
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentRole: 'getCurrentRole',
|
||||
}),
|
||||
sidemenuItems() {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
// get all keys in menuGroup
|
||||
const groupKey = Object.keys(sidemenuItems);
|
||||
const groupKey = Object.keys(this.sidemenuItems);
|
||||
|
||||
let menuItems = [];
|
||||
// Iterate over menuGroup to find the correct group
|
||||
for (let i = 0; i < groupKey.length; i += 1) {
|
||||
const groupItem = sidemenuItems[groupKey[i]];
|
||||
const groupItem = this.sidemenuItems[groupKey[i]];
|
||||
// Check if current route is included
|
||||
const isRouteIncluded = groupItem.routes.includes(this.currentRoute);
|
||||
if (isRouteIncluded) {
|
||||
|
@ -134,7 +170,7 @@ export default {
|
|||
return this.$store.state.route.name;
|
||||
},
|
||||
shouldShowInboxes() {
|
||||
return sidemenuItems.common.routes.includes(this.currentRoute);
|
||||
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
||||
},
|
||||
inboxSection() {
|
||||
return {
|
||||
|
@ -176,9 +212,6 @@ export default {
|
|||
trialMessage() {
|
||||
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
|
||||
},
|
||||
accountId() {
|
||||
return this.currentUser.account_id;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('inboxes/get');
|
||||
|
@ -190,13 +223,14 @@ export default {
|
|||
);
|
||||
},
|
||||
filterMenuItemsByRole(menuItems) {
|
||||
const { role } = this.currentUser;
|
||||
if (!role) {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
}
|
||||
return menuItems.filter(
|
||||
menuItem =>
|
||||
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1
|
||||
window.roleWiseRoutes[this.currentRole].indexOf(
|
||||
menuItem.toStateName
|
||||
) > -1
|
||||
);
|
||||
},
|
||||
logout() {
|
||||
|
@ -205,6 +239,80 @@ export default {
|
|||
showOptions() {
|
||||
this.showOptionsMenu = !this.showOptionsMenu;
|
||||
},
|
||||
changeAccount() {
|
||||
this.showAccountModal = true;
|
||||
},
|
||||
onClose() {
|
||||
this.showAccountModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.account-selector--modal {
|
||||
.modal-container {
|
||||
width: 40rem;
|
||||
}
|
||||
|
||||
.page-top-bar {
|
||||
padding-bottom: $space-two;
|
||||
}
|
||||
}
|
||||
|
||||
.change-accounts--button.button {
|
||||
font-weight: $font-weight-normal;
|
||||
font-size: $font-size-small;
|
||||
padding: $space-small $space-one;
|
||||
}
|
||||
|
||||
.dropdown-pane {
|
||||
li {
|
||||
a {
|
||||
padding: $space-small $space-one !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-selector {
|
||||
cursor: pointer;
|
||||
padding: $space-small $space-large;
|
||||
|
||||
.ion-ios-checkmark {
|
||||
font-size: $font-size-big;
|
||||
|
||||
& + .account--details {
|
||||
padding-left: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.account--details {
|
||||
padding-left: $space-large + $space-smaller;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $space-large;
|
||||
}
|
||||
|
||||
a {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
.account--name {
|
||||
cursor: pointer;
|
||||
font-size: $font-size-medium;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.account--role {
|
||||
cursor: pointer;
|
||||
font-size: $font-size-mini;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
|
||||
import router from '../../routes';
|
||||
import auth from '../../api/auth';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
|
||||
const INBOX_TYPES = {
|
||||
WEB: 'Channel::WebWidget',
|
||||
|
@ -78,6 +78,7 @@ const getInboxClassByType = type => {
|
|||
};
|
||||
|
||||
export default {
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
|
@ -119,7 +120,7 @@ export default {
|
|||
router.push({ name: 'settings_inbox_new', params: { page: 'new' } });
|
||||
},
|
||||
showItem(item) {
|
||||
return auth.isAdmin() && item.newLink !== undefined;
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -27,11 +27,17 @@
|
|||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
isFullwidth: Boolean,
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
classObject() {
|
||||
return 'full-width';
|
||||
|
@ -39,9 +45,6 @@ export default {
|
|||
activeIndex() {
|
||||
return this.items.findIndex(i => i.route === this.$route.name);
|
||||
},
|
||||
items() {
|
||||
return this.$t('INBOX_MGMT.CREATE_FLOW');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActive(item) {
|
||||
|
|
80
app/javascript/dashboard/components/widgets/ColorPicker.vue
Normal file
80
app/javascript/dashboard/components/widgets/ColorPicker.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div class="colorpicker">
|
||||
<div
|
||||
class="colorpicker--selected"
|
||||
:style="`background-color: ${value}`"
|
||||
@click.prevent="toggleColorPicker"
|
||||
/>
|
||||
<chrome
|
||||
v-if="isPickerOpen"
|
||||
v-on-clickaway="closeTogglePicker"
|
||||
:disable-alpha="true"
|
||||
:value="value"
|
||||
class="colorpicker--chrome"
|
||||
@input="updateColor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Chrome } from 'vue-color';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chrome,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isPickerOpen: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closeTogglePicker() {
|
||||
if (this.isPickerOpen) {
|
||||
this.toggleColorPicker();
|
||||
}
|
||||
},
|
||||
toggleColorPicker() {
|
||||
this.isPickerOpen = !this.isPickerOpen;
|
||||
},
|
||||
updateColor(e) {
|
||||
this.$emit('input', e.hex);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.colorpicker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.colorpicker--selected {
|
||||
border-radius: $space-smaller;
|
||||
cursor: pointer;
|
||||
height: $space-large;
|
||||
margin-bottom: $space-normal;
|
||||
width: $space-large;
|
||||
}
|
||||
|
||||
.colorpicker--chrome.vc-chrome {
|
||||
@include elegant-card;
|
||||
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $space-smaller;
|
||||
margin-top: -$space-one;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
|
@ -105,6 +105,15 @@ export default {
|
|||
return `user-thumbnail ${classname}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
src: {
|
||||
handler(value, oldValue) {
|
||||
if (value !== oldValue && this.imgError) {
|
||||
this.imgError = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onImgError() {
|
||||
this.imgError = true;
|
||||
|
|
|
@ -6,15 +6,15 @@
|
|||
>
|
||||
<Thumbnail
|
||||
v-if="!hideThumbnail"
|
||||
:src="chat.meta.sender.thumbnail"
|
||||
:badge="chat.meta.sender.channel"
|
||||
:src="currentContact.thumbnail"
|
||||
:badge="currentContact.channel"
|
||||
class="columns"
|
||||
:username="chat.meta.sender.name"
|
||||
:username="currentContact.name"
|
||||
size="40px"
|
||||
/>
|
||||
<div class="conversation--details columns">
|
||||
<h4 class="conversation--user">
|
||||
{{ chat.meta.sender.name }}
|
||||
{{ currentContact.name }}
|
||||
<span
|
||||
v-if="!hideInboxName && isInboxNameVisible"
|
||||
v-tooltip.bottom="inboxName(chat.inbox_id)"
|
||||
|
@ -25,12 +25,13 @@
|
|||
</h4>
|
||||
<p
|
||||
class="conversation--message"
|
||||
v-html="extractMessageText(lastMessage(chat))"
|
||||
></p>
|
||||
|
||||
v-html="extractMessageText(lastMessageInChat)"
|
||||
/>
|
||||
<div class="conversation--meta">
|
||||
<span class="timestamp">
|
||||
{{ dynamicTime(lastMessage(chat).created_at) }}
|
||||
{{
|
||||
lastMessageInChat ? dynamicTime(lastMessageInChat.created_at) : ''
|
||||
}}
|
||||
</span>
|
||||
<span class="unread">{{ getUnreadCount }}</span>
|
||||
</div>
|
||||
|
@ -75,8 +76,15 @@ export default {
|
|||
inboxesList: 'inboxes/getInboxes',
|
||||
activeInbox: 'getSelectedInbox',
|
||||
currentUser: 'getCurrentUser',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
|
||||
currentContact() {
|
||||
return this.$store.getters['contacts/getContact'](
|
||||
this.chat.meta.sender.id
|
||||
);
|
||||
},
|
||||
|
||||
isActiveChat() {
|
||||
return this.currentChat.id === this.chat.id;
|
||||
},
|
||||
|
@ -92,19 +100,23 @@ export default {
|
|||
isInboxNameVisible() {
|
||||
return !this.activeInbox;
|
||||
},
|
||||
|
||||
lastMessageInChat() {
|
||||
return this.lastMessage(this.chat);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cardClick(chat) {
|
||||
const { activeInbox } = this;
|
||||
const path = conversationUrl(
|
||||
this.currentUser.account_id,
|
||||
activeInbox,
|
||||
chat.id
|
||||
);
|
||||
const path = conversationUrl(this.accountId, activeInbox, chat.id);
|
||||
router.push({ path: frontendURL(path) });
|
||||
},
|
||||
extractMessageText(chatItem) {
|
||||
if (!chatItem) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { content, attachments } = chatItem;
|
||||
|
||||
if (content) {
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
<div class="conv-header">
|
||||
<div class="user">
|
||||
<Thumbnail
|
||||
:src="chat.meta.sender.thumbnail"
|
||||
:src="currentContact.thumbnail"
|
||||
size="40px"
|
||||
:badge="chat.meta.sender.channel"
|
||||
:username="chat.meta.sender.name"
|
||||
:badge="currentContact.channel"
|
||||
:username="currentContact.name"
|
||||
/>
|
||||
<div class="user--profile__meta">
|
||||
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
|
||||
{{ chat.meta.sender.name }}
|
||||
{{ currentContact.name }}
|
||||
</h3>
|
||||
<button
|
||||
class="user--profile__button clear button small"
|
||||
|
@ -79,6 +79,13 @@ export default {
|
|||
agents: 'agents/getVerifiedAgents',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
|
||||
currentContact() {
|
||||
return this.$store.getters['contacts/getContact'](
|
||||
this.chat.meta.sender.id
|
||||
);
|
||||
},
|
||||
|
||||
agentList() {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<!-- No inboxes attached -->
|
||||
<div v-if="!inboxesList.length">
|
||||
<img src="~dashboard/assets/images/inboxes.svg" alt="No Inboxes" />
|
||||
<span v-if="isAdmin()">
|
||||
<span v-if="isAdmin">
|
||||
{{ $t('CONVERSATION.NO_INBOX_1') }}
|
||||
<br />
|
||||
<router-link :to="newInboxURL">
|
||||
|
@ -17,7 +17,7 @@
|
|||
</router-link>
|
||||
{{ $t('CONVERSATION.NO_INBOX_2') }}
|
||||
</span>
|
||||
<span v-if="!isAdmin()">
|
||||
<span v-if="!isAdmin">
|
||||
{{ $t('CONVERSATION.NO_INBOX_AGENT') }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -208,7 +208,9 @@ export default {
|
|||
async sendMessage() {
|
||||
const isMessageEmpty = !this.message.replace(/\n/g, '').length;
|
||||
if (isMessageEmpty) return;
|
||||
|
||||
if (this.message.length > this.maxLength) {
|
||||
return;
|
||||
}
|
||||
if (!this.showCannedResponsesList) {
|
||||
try {
|
||||
await this.$store.dispatch('sendMessage', {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue