diff --git a/.circleci/config.yml b/.circleci/config.yml index 7b72fb0c0..9579c23a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.env.example b/.env.example index b80e3577d..d4fb1ea12 100644 --- a/.env.example +++ b/.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 diff --git a/.rubocop.yml b/.rubocop.yml index 8d2405e3f..fac44c67a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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/**/*' diff --git a/.ruby-version b/.ruby-version index 24ba9a38d..860487ca1 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.0 +2.7.1 diff --git a/.scss-lint.yml b/.scss-lint.yml index 9f5f4fe10..481d94c70 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -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' diff --git a/Gemfile b/Gemfile index e8e2fa85b..50e3e2ce8 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index f26503fa4..48e4e12a8 100644 --- a/Gemfile.lock +++ b/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 diff --git a/Procfile b/Procfile index 16a5cc0c1..34f447dda 100644 --- a/Procfile +++ b/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 \ No newline at end of file +worker: bundle exec sidekiq -C config/sidekiq.yml diff --git a/README.md b/README.md index e71b8c6d1..75a254058 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ ___ Docker Build Badge License Commits-per-month - Discord - Chat on Discord + + Discord

![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png) diff --git a/app.json b/app.json index 0b00d971e..300f363db 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "description": "Chatwoot is a customer support tool for instant messaging channels", "website": "https://www.chatwoot.com/", "repository": "https://github.com/chatwoot/chatwoot", - "logo": "data:image/jpeg;base64,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", + "logo": "https://app.chatwoot.com/brand-assets/logo_thumbnail.svg", "keywords": [ "live chat", "customer support", @@ -27,6 +27,10 @@ "RAILS_ENV": { "description": "Environment for rails middleware.", "value": "production" + }, + "FRONTEND_URL": { + "description": "Public root URL of the Chatwoot installation. This will be used in the emails.", + "value": "https://CHANGE.herokuapp.com" } }, "formation": { diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index ac907b367..f5e0f5476 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1 +1,4 @@ //= link_tree ../images +//= link administrate/application.css +//= link administrate/application.js +//= link dashboardChart.js diff --git a/app/assets/javascripts/dashboardChart.js b/app/assets/javascripts/dashboardChart.js new file mode 100644 index 000000000..6bfe56bda --- /dev/null +++ b/app/assets/javascripts/dashboardChart.js @@ -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(), + }); +} diff --git a/app/assets/stylesheets/administrate/application.scss b/app/assets/stylesheets/administrate/application.scss new file mode 100644 index 000000000..79738bbf3 --- /dev/null +++ b/app/assets/stylesheets/administrate/application.scss @@ -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'; diff --git a/app/assets/stylesheets/administrate/base/_forms.scss b/app/assets/stylesheets/administrate/base/_forms.scss new file mode 100644 index 000000000..bf014a746 --- /dev/null +++ b/app/assets/stylesheets/administrate/base/_forms.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/administrate/base/_layout.scss b/app/assets/stylesheets/administrate/base/_layout.scss new file mode 100644 index 000000000..c4c081a82 --- /dev/null +++ b/app/assets/stylesheets/administrate/base/_layout.scss @@ -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%; +} diff --git a/app/assets/stylesheets/administrate/base/_lists.scss b/app/assets/stylesheets/administrate/base/_lists.scss new file mode 100644 index 000000000..70eae5203 --- /dev/null +++ b/app/assets/stylesheets/administrate/base/_lists.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/administrate/base/_tables.scss b/app/assets/stylesheets/administrate/base/_tables.scss new file mode 100644 index 000000000..1772e8cf4 --- /dev/null +++ b/app/assets/stylesheets/administrate/base/_tables.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/base/_typography.scss b/app/assets/stylesheets/administrate/base/_typography.scss new file mode 100644 index 000000000..bf2c2d9c2 --- /dev/null +++ b/app/assets/stylesheets/administrate/base/_typography.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/components/_app-container.scss b/app/assets/stylesheets/administrate/components/_app-container.scss new file mode 100644 index 000000000..80873272f --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_app-container.scss @@ -0,0 +1,8 @@ +.app-container { + align-items: stretch; + display: flex; + margin-left: auto; + margin-right: auto; + max-width: 100rem; + min-height: 100vh; +} diff --git a/app/assets/stylesheets/administrate/components/_attributes.scss b/app/assets/stylesheets/administrate/components/_attributes.scss new file mode 100644 index 000000000..713d9f523 --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_attributes.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/components/_buttons.scss b/app/assets/stylesheets/administrate/components/_buttons.scss new file mode 100644 index 000000000..3e021e658 --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_buttons.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/components/_cells.scss b/app/assets/stylesheets/administrate/components/_cells.scss new file mode 100644 index 000000000..2f7e27c4a --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_cells.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/components/_field-unit.scss b/app/assets/stylesheets/administrate/components/_field-unit.scss new file mode 100644 index 000000000..856c1872c --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_field-unit.scss @@ -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%; + } +} diff --git a/app/assets/stylesheets/administrate/components/_flashes.scss b/app/assets/stylesheets/administrate/components/_flashes.scss new file mode 100644 index 000000000..48c3e685c --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_flashes.scss @@ -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%); + } + } + } +} diff --git a/app/assets/stylesheets/administrate/components/_form-actions.scss b/app/assets/stylesheets/administrate/components/_form-actions.scss new file mode 100644 index 000000000..d87d17435 --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_form-actions.scss @@ -0,0 +1,3 @@ +.form-actions { + margin-left: calc(15% + 2rem); +} diff --git a/app/assets/stylesheets/administrate/components/_main-content.scss b/app/assets/stylesheets/administrate/components/_main-content.scss new file mode 100644 index 000000000..d03229828 --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_main-content.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/components/_navigation.scss b/app/assets/stylesheets/administrate/components/_navigation.scss new file mode 100644 index 000000000..f6b1a641d --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_navigation.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/components/_pagination.scss b/app/assets/stylesheets/administrate/components/_pagination.scss new file mode 100644 index 000000000..cb3e12f21 --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_pagination.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/administrate/components/_reports.scss b/app/assets/stylesheets/administrate/components/_reports.scss new file mode 100644 index 000000000..0c8c133b7 --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_reports.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/administrate/components/_search.scss b/app/assets/stylesheets/administrate/components/_search.scss new file mode 100644 index 000000000..f3a259618 --- /dev/null +++ b/app/assets/stylesheets/administrate/components/_search.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/administrate/library/_clearfix.scss b/app/assets/stylesheets/administrate/library/_clearfix.scss new file mode 100644 index 000000000..ea852351f --- /dev/null +++ b/app/assets/stylesheets/administrate/library/_clearfix.scss @@ -0,0 +1,7 @@ +@mixin administrate-clearfix { + &::after { + clear: both; + content: ''; + display: block; + } +} diff --git a/app/assets/stylesheets/administrate/library/_data-label.scss b/app/assets/stylesheets/administrate/library/_data-label.scss new file mode 100644 index 000000000..2efcd2836 --- /dev/null +++ b/app/assets/stylesheets/administrate/library/_data-label.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/library/_variables.scss b/app/assets/stylesheets/administrate/library/_variables.scss new file mode 100644 index 000000000..3fdfcfd8d --- /dev/null +++ b/app/assets/stylesheets/administrate/library/_variables.scss @@ -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; diff --git a/app/assets/stylesheets/administrate/reset/_normalize.scss b/app/assets/stylesheets/administrate/reset/_normalize.scss new file mode 100644 index 000000000..fa4e73dd4 --- /dev/null +++ b/app/assets/stylesheets/administrate/reset/_normalize.scss @@ -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; +} diff --git a/app/assets/stylesheets/administrate/utilities/_text-color.scss b/app/assets/stylesheets/administrate/utilities/_text-color.scss new file mode 100644 index 000000000..afa3bcca3 --- /dev/null +++ b/app/assets/stylesheets/administrate/utilities/_text-color.scss @@ -0,0 +1,3 @@ +.text-color-red { + color: $alert-color; +} diff --git a/app/assets/stylesheets/administrate/utilities/_variables.scss b/app/assets/stylesheets/administrate/utilities/_variables.scss new file mode 100644 index 000000000..1798d918b --- /dev/null +++ b/app/assets/stylesheets/administrate/utilities/_variables.scss @@ -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; diff --git a/app/bot/bot.rb b/app/bot/bot.rb deleted file mode 100644 index c834e7774..000000000 --- a/app/bot/bot.rb +++ /dev/null @@ -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 diff --git a/app/bot/facebook_bot.rb b/app/bot/facebook_bot.rb new file mode 100644 index 000000000..f0e04ebc2 --- /dev/null +++ b/app/bot/facebook_bot.rb @@ -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 diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index 126eedce0..1c7c1f63d 100644 --- a/app/builders/account_builder.rb +++ b/app/builders/account_builder.rb @@ -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 diff --git a/app/builders/contact_builder.rb b/app/builders/contact_builder.rb index 9bb3ef3ac..3e362c9b5 100644 --- a/app/builders/contact_builder.rb +++ b/app/builders/contact_builder.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/accounts_controller.rb b/app/controllers/api/v1/accounts/accounts_controller.rb index 0fd5dc7cf..adb8bf11c 100644 --- a/app/controllers/api/v1/accounts/accounts_controller.rb +++ b/app/controllers/api/v1/accounts/accounts_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index ca796ceef..85a05fbc1 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index bf42b215a..54c7bace4 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -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) diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index c0c121900..8011d3891 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController + before_action :current_account before_action :authorize_request def create diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 05eaa5861..fcda432a5 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -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,8 +21,18 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController def show; end + def mute + @conversation.mute! + head :ok + end + def toggle_status - @status = @conversation.toggle_status + if params[:status] + @conversation.status = params[:status] + @status = @conversation.save + else + @status = @conversation.toggle_status + end end def toggle_typing_status diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 4b3ed836e..8fdde8cc0 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -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) diff --git a/app/controllers/api/v1/accounts/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb index dbdd953ed..d3afba2af 100644 --- a/app/controllers/api/v1/accounts/webhooks_controller.rb +++ b/app/controllers/api/v1/accounts/webhooks_controller.rb @@ -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] diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 9a0bbfc17..bd415ffeb 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -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 diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 8f8e372fe..cdfbd9d4a 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -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? diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index fe94db4e1..6aa0355a4 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -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, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b6473cf13..ae3e5e857 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index e7af9e116..3d6f55674 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -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') diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 407cf7d69..272c2f546 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -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 diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb index 4289d5af2..ed9d012eb 100644 --- a/app/controllers/devise_overrides/passwords_controller.rb +++ b/app/controllers/devise_overrides/passwords_controller.rb @@ -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 diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index b9cec5447..9ebb3b435 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -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 diff --git a/app/controllers/super_admin/access_tokens_controller.rb b/app/controllers/super_admin/access_tokens_controller.rb new file mode 100644 index 000000000..a8a2669c4 --- /dev/null +++ b/app/controllers/super_admin/access_tokens_controller.rb @@ -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 diff --git a/app/controllers/super_admin/account_users_controller.rb b/app/controllers/super_admin/account_users_controller.rb new file mode 100644 index 000000000..b210dea19 --- /dev/null +++ b/app/controllers/super_admin/account_users_controller.rb @@ -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('
') + 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 diff --git a/app/controllers/super_admin/accounts_controller.rb b/app/controllers/super_admin/accounts_controller.rb new file mode 100644 index 000000000..4d35fa1d8 --- /dev/null +++ b/app/controllers/super_admin/accounts_controller.rb @@ -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 diff --git a/app/controllers/super_admin/agent_bots_controller.rb b/app/controllers/super_admin/agent_bots_controller.rb new file mode 100644 index 000000000..8e094e752 --- /dev/null +++ b/app/controllers/super_admin/agent_bots_controller.rb @@ -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 diff --git a/app/controllers/super_admin/application_controller.rb b/app/controllers/super_admin/application_controller.rb new file mode 100644 index 000000000..69b61b913 --- /dev/null +++ b/app/controllers/super_admin/application_controller.rb @@ -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 diff --git a/app/controllers/super_admin/dashboard_controller.rb b/app/controllers/super_admin/dashboard_controller.rb new file mode 100644 index 000000000..b5f3d34eb --- /dev/null +++ b/app/controllers/super_admin/dashboard_controller.rb @@ -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 diff --git a/app/controllers/super_admin/devise/sessions_controller.rb b/app/controllers/super_admin/devise/sessions_controller.rb new file mode 100644 index 000000000..de7002206 --- /dev/null +++ b/app/controllers/super_admin/devise/sessions_controller.rb @@ -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 diff --git a/app/controllers/super_admin/super_admins_controller.rb b/app/controllers/super_admin/super_admins_controller.rb new file mode 100644 index 000000000..16d91a151 --- /dev/null +++ b/app/controllers/super_admin/super_admins_controller.rb @@ -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 diff --git a/app/controllers/super_admin/users_controller.rb b/app/controllers/super_admin/users_controller.rb new file mode 100644 index 000000000..613670849 --- /dev/null +++ b/app/controllers/super_admin/users_controller.rb @@ -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 diff --git a/app/controllers/twitter/callbacks_controller.rb b/app/controllers/twitter/callbacks_controller.rb index 6065e3546..f317f3ffa 100644 --- a/app/controllers/twitter/callbacks_controller.rb +++ b/app/controllers/twitter/callbacks_controller.rb @@ -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,20 +34,23 @@ class Twitter::CallbacksController < Twitter::BaseController app_new_twitter_inbox_url(account_id: account.id) end - def build_inbox - ActiveRecord::Base.transaction do - twitter_profile = account.twitter_profiles.create( - twitter_access_token: parsed_body['oauth_token'], - twitter_access_token_secret: parsed_body['oauth_token_secret'], - profile_id: parsed_body['user_id'] - ) - account.inboxes.create( - name: parsed_body['screen_name'], - channel: twitter_profile - ) - rescue StandardError => e - Rails.logger e - end + 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'], + profile_id: parsed_body['user_id'] + ) + account.inboxes.create( + name: parsed_body['screen_name'], + channel: twitter_profile + ) end def permitted_params diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 7b7cb4f52..4e784ac60 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -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 diff --git a/app/dashboards/access_token_dashboard.rb b/app/dashboards/access_token_dashboard.rb new file mode 100644 index 000000000..6864b99d4 --- /dev/null +++ b/app/dashboards/access_token_dashboard.rb @@ -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 diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb new file mode 100644 index 000000000..3cc35d0d3 --- /dev/null +++ b/app/dashboards/account_dashboard.rb @@ -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 diff --git a/app/dashboards/account_user_dashboard.rb b/app/dashboards/account_user_dashboard.rb new file mode 100644 index 000000000..d757f4da5 --- /dev/null +++ b/app/dashboards/account_user_dashboard.rb @@ -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 diff --git a/app/dashboards/agent_bot_dashboard.rb b/app/dashboards/agent_bot_dashboard.rb new file mode 100644 index 000000000..ca6ca7ad1 --- /dev/null +++ b/app/dashboards/agent_bot_dashboard.rb @@ -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 diff --git a/app/dashboards/super_admin_dashboard.rb b/app/dashboards/super_admin_dashboard.rb new file mode 100644 index 000000000..ab467a255 --- /dev/null +++ b/app/dashboards/super_admin_dashboard.rb @@ -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 diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb new file mode 100644 index 000000000..8ed132c61 --- /dev/null +++ b/app/dashboards/user_dashboard.rb @@ -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 diff --git a/app/fields/avatar_field.rb b/app/fields/avatar_field.rb new file mode 100644 index 000000000..50633ccd2 --- /dev/null +++ b/app/fields/avatar_field.rb @@ -0,0 +1,7 @@ +require 'administrate/field/base' + +class AvatarField < Administrate::Field::Base + def avatar_url + data.presence || '/admin/avatar.png' + end +end diff --git a/app/fields/count_field.rb b/app/fields/count_field.rb new file mode 100644 index 000000000..de5c4ae42 --- /dev/null +++ b/app/fields/count_field.rb @@ -0,0 +1,7 @@ +require 'administrate/field/base' + +class CountField < Administrate::Field::Base + def to_s + data.count + end +end diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 18ca9a60b..9a50ef1c8 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -39,6 +39,10 @@ class ConversationApi extends ApiClient { typing_status: status, }); } + + mute(conversationId) { + return axios.post(`${this.url}/${conversationId}/mute`); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/assets/images/agent.svg b/app/javascript/dashboard/assets/images/agent.svg deleted file mode 100644 index b0e83fbf4..000000000 --- a/app/javascript/dashboard/assets/images/agent.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - telemarketer - Created with Sketch. - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/javascript/dashboard/assets/images/canned.svg b/app/javascript/dashboard/assets/images/canned.svg deleted file mode 100644 index 29dabd34c..000000000 --- a/app/javascript/dashboard/assets/images/canned.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - canned - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/app/javascript/dashboard/assets/scss/_variables.scss b/app/javascript/dashboard/assets/scss/_variables.scss index 09a639648..1798d918b 100644 --- a/app/javascript/dashboard/assets/scss/_variables.scss +++ b/app/javascript/dashboard/assets/scss/_variables.scss @@ -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; diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 05d822b5a..947204ca6 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -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'; diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 6d522dc9b..480e31c61 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -91,7 +91,6 @@ } .sidebar-labels-wrap { - &.has-edited, &:hover { .multiselect { @@ -108,8 +107,6 @@ } .multiselect { - margin-top: $space-small; - >.multiselect__select { visibility: hidden; } diff --git a/app/javascript/dashboard/assets/scss/super_admin/index.scss b/app/javascript/dashboard/assets/scss/super_admin/index.scss new file mode 100644 index 000000000..a646bf5f9 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/super_admin/index.scss @@ -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; +} diff --git a/app/javascript/dashboard/assets/scss/super_admin/pages.scss b/app/javascript/dashboard/assets/scss/super_admin/pages.scss new file mode 100644 index 000000000..2bc31db1c --- /dev/null +++ b/app/javascript/dashboard/assets/scss/super_admin/pages.scss @@ -0,0 +1,3 @@ +@import 'shared/assets/fonts/inter'; +@import '../variables'; +@import '~ionicons/scss/ionicons'; diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 5d3486aec..7e141d30e 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -1,4 +1,6 @@ .button { + margin-bottom: 0; + &.icon { padding-left: $space-normal; padding-right: $space-normal; diff --git a/app/javascript/dashboard/assets/scss/widgets/_colorpicker.scss b/app/javascript/dashboard/assets/scss/widgets/_colorpicker.scss deleted file mode 100644 index 9d761807a..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_colorpicker.scss +++ /dev/null @@ -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; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index 641400b1e..0ae8ecf21 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -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 { - opacity: 0; +.modal-enter, +.modal-leave { + opacity: 0; } .modal-enter .modal-container, .modal-leave .modal-container { - -webkit-transform: scale(1.1); - transform: scale(1.1); + transform: scale(1.1); } diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss index 83e4ff5ee..3971ec6fd 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss @@ -72,7 +72,7 @@ @include border-light; display: block; left: 18%; - top: -110%; + top: -110px; visibility: visible; width: 80%; z-index: 999; diff --git a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss index d229c0738..d47d6ee4c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss @@ -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 { diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index c0ddb1987..59012d516 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -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; }, }, }; diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue index b7d9489fb..6838b308c 100644 --- a/app/javascript/dashboard/components/Modal.vue +++ b/app/javascript/dashboard/components/Modal.vue @@ -6,8 +6,8 @@ transition="modal" @click="onBackDropClick" > - diff --git a/app/javascript/dashboard/components/SettingsSection.vue b/app/javascript/dashboard/components/SettingsSection.vue index ebf79bf3e..adf6f9912 100644 --- a/app/javascript/dashboard/components/SettingsSection.vue +++ b/app/javascript/dashboard/components/SettingsSection.vue @@ -1,6 +1,6 @@ @@ -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; + }, }, }; + + diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue index 8e8297e01..35356a685 100644 --- a/app/javascript/dashboard/components/layout/SidebarItem.vue +++ b/app/javascript/dashboard/components/layout/SidebarItem.vue @@ -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; }, }, }; diff --git a/app/javascript/dashboard/components/ui/Wizard.vue b/app/javascript/dashboard/components/ui/Wizard.vue index f34b8f182..71848ecb0 100644 --- a/app/javascript/dashboard/components/ui/Wizard.vue +++ b/app/javascript/dashboard/components/ui/Wizard.vue @@ -27,11 +27,17 @@ + + diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 9110939ea..25b707c04 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -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; diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 4afe61c31..a2a914bb2 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -6,15 +6,15 @@ >

- {{ chat.meta.sender.name }} + {{ currentContact.name }}

- + v-html="extractMessageText(lastMessageInChat)" + />
- {{ dynamicTime(lastMessage(chat).created_at) }} + {{ + lastMessageInChat ? dynamicTime(lastMessageInChat.created_at) : '' + }} {{ getUnreadCount }}
@@ -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) { diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index 29215362f..8be30ff68 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -2,14 +2,14 @@

- {{ chat.meta.sender.name }} + {{ currentContact.name }}

- +
@@ -176,11 +192,13 @@ export default { .contact--panel { @include border-normal-left; + + background: white; font-size: $font-size-small; overflow-y: auto; - background: white; overflow: auto; position: relative; + padding: $space-normal; } .close-button { @@ -190,23 +208,29 @@ export default { font-size: $font-size-default; color: $color-heading; } + .contact--profile { - padding: $space-medium $space-normal 0 $space-medium; align-items: center; + padding: $space-medium 0 $space-one; + .user-thumbnail-box { margin-right: $space-normal; } } .contact--details { + margin-top: $space-small; + p { margin-bottom: 0; } } .contact--info { - display: flex; align-items: center; + display: flex; + flex-direction: column; + text-align: center; } .contact--name { @@ -220,10 +244,13 @@ export default { .contact--email { @include text-ellipsis; - color: $color-body; + color: $color-gray; display: block; line-height: $space-medium; - text-decoration: underline; + + &:hover { + color: $color-woot; + } } .contact--bio { @@ -231,7 +258,8 @@ export default { } .conversation--details { - padding: $space-two $space-normal $space-two $space-medium; + border-top: 1px solid $color-border-light; + padding: $space-large $space-normal; } .conversation--labels { @@ -248,4 +276,19 @@ export default { padding: 0.2rem; } } + +.contact-conversation--panel { + border-top: 1px solid $color-border-light; +} + +.contact--mute { + color: $alert-color; + display: block; + text-align: center; +} + +.contact--actions { + display: flex; + justify-content: center; +} diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationLabels.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationLabels.vue index 9bc10e8e8..728815a0e 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationLabels.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationLabels.vue @@ -3,40 +3,44 @@ class="contact-conversation--panel sidebar-labels-wrap" :class="hasEditedClass" > -
-
- -
- {{ - $t('CONTACT_PANEL.LABELS.UPDATE_ERROR') - }} - -
+
+ +
+ {{ + $t('CONTACT_PANEL.LABELS.UPDATE_ERROR') + }} +
@@ -45,10 +49,12 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue index f4bac9d12..d8d86a052 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue @@ -94,12 +94,12 @@ import Vue from 'vue'; import { required } from 'vuelidate/lib/validators'; import { mapGetters } from 'vuex'; -import { accountIdFromPathname } from 'dashboard/helper/URLHelper'; import alertMixin from 'shared/mixins/alertMixin'; import configMixin from 'shared/mixins/configMixin'; +import accountMixin from '../../../../mixins/account'; export default { - mixins: [alertMixin, configMixin], + mixins: [accountMixin, alertMixin, configMixin], data() { return { id: '', @@ -140,10 +140,7 @@ export default { }, methods: { async initializeAccount() { - const { pathname } = window.location; - const accountId = accountIdFromPathname(pathname); - - if (accountId) { + try { await this.$store.dispatch('accounts/get'); const { name, @@ -153,7 +150,7 @@ export default { support_email, domain_emails_enabled, features, - } = this.getAccount(accountId); + } = this.getAccount(this.accountId); Vue.config.lang = locale; this.name = name; @@ -163,6 +160,8 @@ export default { this.supportEmail = support_email; this.domainEmailsEnabled = domain_emails_enabled; this.features = features; + } catch (error) { + // Ignore error } }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js index 6957af495..70aff7c36 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js @@ -6,7 +6,6 @@ export default { routes: [ { path: frontendURL('accounts/:accountId/settings/general'), - name: 'general_settings', roles: ['administrator'], component: SettingsContent, props: { diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue index 0fa469d9b..4c5b571e7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue @@ -2,7 +2,6 @@
@@ -59,7 +58,9 @@ :button-text="$t('AGENT_MGMT.ADD.FORM.SUBMIT')" :loading="uiFlags.isCreating" /> - Cancel +
@@ -73,10 +74,13 @@ import { required, minLength, email } from 'vuelidate/lib/validators'; import { mapGetters } from 'vuex'; -const agentImg = require('assets/images/agent.svg'); - export default { - props: ['onClose'], + props: { + onClose: { + type: Function, + default: () => {}, + }, + }, data() { return { agentName: '', @@ -89,9 +93,6 @@ export default { }; }, computed: { - headerImage() { - return agentImg; - }, ...mapGetters({ uiFlags: 'agents/getUIFlags', }), diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/EditAgent.vue b/app/javascript/dashboard/routes/dashboard/settings/agents/EditAgent.vue index 87ceed354..7ed909aff 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/EditAgent.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/EditAgent.vue @@ -42,15 +42,15 @@ :button-text="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')" :loading="uiFlags.isUpdating" /> - +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue index 06e4685bf..25a12134b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/Index.vue @@ -74,7 +74,14 @@

- +
@@ -93,7 +100,7 @@ /> -
@@ -22,8 +21,9 @@
@@ -57,14 +59,17 @@ import { required, minLength } from 'vuelidate/lib/validators'; import WootSubmitButton from '../../../../components/buttons/FormSubmitButton'; import Modal from '../../../../components/Modal'; -const cannedImg = require('assets/images/canned.svg'); - export default { components: { WootSubmitButton, Modal, }, - props: ['onClose'], + props: { + onClose: { + type: Function, + default: () => {}, + }, + }, data() { return { shortCode: '', @@ -81,11 +86,6 @@ export default { show: true, }; }, - computed: { - headerImage() { - return cannedImg; - }, - }, validations: { shortCode: { required, diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/DeleteCanned.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/DeleteCanned.vue deleted file mode 100644 index 9b9b9d3ef..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/canned/DeleteCanned.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue index dd578a56a..5645d375c 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue @@ -18,8 +18,9 @@
diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue index db8e77971..95065f547 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue @@ -87,7 +87,7 @@ - - - - - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/InboxChannels.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/InboxChannels.vue index 27dfe2c24..ddc6f6e20 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/InboxChannels.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/InboxChannels.vue @@ -1,6 +1,33 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue index 1d4ab689b..faead1f76 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue @@ -6,7 +6,7 @@

{{ $t('INBOX_MGMT.LIST.404') }} {{ $t('SETTINGS.INBOXES.NEW_INBOX') }} @@ -54,7 +54,7 @@ :to="addAccountScoping(`settings/inboxes/${item.id}`)" >

- +
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- -
- -
-
-
- -
+ + + + + + + + - + /> @@ -150,8 +125,7 @@ :button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')" :loading="isAgentListUpdating" @click="updateAgents" - > - + /> @@ -196,26 +170,36 @@ /* global bus */ import { mapGetters } from 'vuex'; import { createMessengerScript } from 'dashboard/helper/scriptGenerator'; -import { Compact } from 'vue-color'; import configMixin from 'shared/mixins/configMixin'; import SettingsSection from '../../../../components/SettingsSection'; export default { components: { - Compact, SettingsSection, }, mixins: [configMixin], data() { return { + avatarFile: null, + avatarUrl: '', selectedAgents: [], autoAssignment: false, - isUpdating: false, isAgentListUpdating: false, + selectedInboxName: '', channelWebsiteUrl: '', channelWelcomeTitle: '', channelWelcomeTagline: '', channelAgentAwayMessage: '', + autoAssignmentOptions: [ + { + value: true, + label: this.$t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.ENABLED'), + }, + { + value: false, + label: this.$t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.DISABLED'), + }, + ], }; }, computed: { @@ -229,6 +213,9 @@ export default { inbox() { return this.$store.getters['inboxes/getInbox'](this.currentInboxId); }, + isAWidgetInbox() { + return this.inbox.channel_type === 'Channel::WebWidget'; + }, inboxName() { if (this.inbox.channel_type === 'Channel::TwilioSms') { return `${this.inbox.name} (${this.inbox.phone_number})`; @@ -258,6 +245,8 @@ export default { this.$store.dispatch('agents/get'); this.$store.dispatch('inboxes/get').then(() => { this.fetchAttachedAgents(); + this.avatarUrl = this.inbox.avatar_url; + this.selectedInboxName = this.inbox.name; this.autoAssignment = this.inbox.enable_auto_assignment; this.channelWebsiteUrl = this.inbox.website_url; this.channelWelcomeTitle = this.inbox.welcome_title; @@ -301,27 +290,31 @@ export default { }, async updateInbox() { try { - await this.$store.dispatch('inboxes/updateInbox', { + const payload = { id: this.currentInboxId, - name: this.inboxName, + name: this.selectedInboxName, enable_auto_assignment: this.autoAssignment, channel: { - widget_color: this.getWidgetColor(this.inbox.widget_color), + widget_color: this.inbox.widget_color, website_url: this.channelWebsiteUrl, welcome_title: this.channelWelcomeTitle, welcome_tagline: this.channelWelcomeTagline, agent_away_message: this.channelAgentAwayMessage, }, - }); + }; + if (this.avatarFile) { + payload.avatar = this.avatarFile; + } + await this.$store.dispatch('inboxes/updateInbox', payload); this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); } catch (error) { this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); } }, - getWidgetColor() { - return typeof this.inbox.widget_color !== 'object' - ? this.inbox.widget_color - : this.inbox.widget_color.hex; + handleImageUpload({ file, url }) { + this.avatarFile = file; + this.avatarUrl = url; + console.log(this.avatarUrl); }, }, validations: { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue index 07cdfa0f9..bcd9cda9d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue @@ -7,24 +7,33 @@ alt="Facebook-logo" /> -

{{ $t('INBOX_MGMT.ADD.FB.HELP') }}

+

+ {{ + useInstallationName( + $t('INBOX_MGMT.ADD.FB.HELP'), + globalConfig.installationName + ) + }} +

- +
- Choose Page + {{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PAGE') }} - Select a page from the list + {{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PLACEHOLDER') }}
@@ -72,13 +81,15 @@ import { mapGetters } from 'vuex'; import ChannelApi from '../../../../../api/channels'; import PageHeader from '../../SettingsSubPageHeader'; import router from '../../../../index'; +import globalConfigMixin from 'shared/mixins/globalConfigMixin'; +import accountMixin from '../../../../../mixins/account'; export default { components: { LoadingState, PageHeader, }, - + mixins: [globalConfigMixin, accountMixin], data() { return { isCreating: false, @@ -114,10 +125,8 @@ export default { }, ...mapGetters({ currentUser: 'getCurrentUser', + globalConfig: 'globalConfig/get', }), - accountId() { - return this.currentUser.account_id; - }, }, created() { @@ -152,7 +161,7 @@ export default { FB.init({ appId: window.chatwootConfig.fbAppId, xfbml: true, - version: 'v4.0', + version: 'v7.0', status: true, }); window.fbSDKLoaded = true; @@ -195,7 +204,7 @@ export default { } }, { - scope: 'manage_pages,pages_messaging,pages_messaging_phone_number', + scope: 'pages_manage_metadata,pages_messaging', } ); }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue index e6d93ae41..644eb10a1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue @@ -12,7 +12,7 @@
+ +
+ +
+
-
- -
-
-
diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index 67b7bbeb2..d70aaf342 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -13,12 +13,16 @@ const sendAttachmentAPI = async attachment => { return result; }; -const getConversationAPI = async ({ before }) => { +const getMessagesAPI = async ({ before }) => { const urlData = endPoints.getConversation({ before }); const result = await API.get(urlData.url, { params: urlData.params }); return result; }; +const getConversationAPI = async () => { + return API.get(`/api/v1/widget/conversations${window.location.search}`); +}; + const toggleTyping = async ({ typingStatus }) => { return API.post( `/api/v1/widget/conversations/toggle_typing${window.location.search}`, @@ -26,4 +30,10 @@ const toggleTyping = async ({ typingStatus }) => { ); }; -export { sendMessageAPI, getConversationAPI, sendAttachmentAPI, toggleTyping }; +export { + sendMessageAPI, + getConversationAPI, + getMessagesAPI, + sendAttachmentAPI, + toggleTyping, +}; diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index c86f915e4..ef51af907 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -1,13 +1,26 @@ -const sendMessage = content => ({ - url: `/api/v1/widget/messages${window.location.search}`, - params: { - message: { - content, - timestamp: new Date().toString(), - referer_url: window.refererURL || '', +import Vue from 'vue'; + +const sendMessage = content => { + const locale = Vue.config.lang; + const refererURL = window.refererURL || ''; + let search = window.location.search; + if (search) { + search = `${search}&locale=${locale}`; + } else { + search = `?locale=${locale}`; + } + + return { + url: `/api/v1/widget/messages${search}`, + params: { + message: { + content, + timestamp: new Date().toString(), + referer_url: refererURL, + }, }, - }, -}); + }; +}; const sendAttachment = ({ attachment }) => { const { refererURL = '' } = window; diff --git a/app/javascript/widget/api/specs/endPoints.spec.js b/app/javascript/widget/api/specs/endPoints.spec.js index d17b315ba..396df2c71 100644 --- a/app/javascript/widget/api/specs/endPoints.spec.js +++ b/app/javascript/widget/api/specs/endPoints.spec.js @@ -1,12 +1,14 @@ import endPoints from '../endPoints'; +jest.mock('vue', () => ({ config: { lang: 'ar' } })); + describe('#sendMessage', () => { it('returns correct payload', () => { const spy = jest.spyOn(global, 'Date').mockImplementation(() => ({ toString: () => 'mock date', })); expect(endPoints.sendMessage('hello')).toEqual({ - url: `/api/v1/widget/messages`, + url: `/api/v1/widget/messages?locale=ar`, params: { message: { content: 'hello', diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue index 261afbf3c..70aa01b1c 100755 --- a/app/javascript/widget/components/AgentMessage.vue +++ b/app/javascript/widget/components/AgentMessage.vue @@ -1,9 +1,9 @@ @@ -53,7 +60,8 @@ import ImageBubble from 'widget/components/ImageBubble'; import FileBubble from 'widget/components/FileBubble'; import Thumbnail from 'dashboard/components/widgets/Thumbnail'; import { MESSAGE_TYPE } from 'widget/helpers/constants'; - +import configMixin from '../mixins/configMixin'; +import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper'; export default { name: 'AgentMessage', components: { @@ -63,7 +71,7 @@ export default { UserMessage, FileBubble, }, - mixins: [timeMixin], + mixins: [timeMixin, configMixin], props: { message: { type: Object, @@ -112,11 +120,17 @@ export default { avatarUrl() { // eslint-disable-next-line const BotImage = require('dashboard/assets/images/chatwoot_bot.png'); + const displayImage = this.useInboxAvatarForBot + ? this.inboxAvatarUrl + : BotImage; + if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) { - return BotImage; + return displayImage; } - return this.message.sender ? this.message.sender.avatar_url : BotImage; + return this.message.sender + ? this.message.sender.avatar_url + : displayImage; }, hasRecordedResponse() { return ( @@ -140,6 +154,17 @@ export default { } return ''; }, + isASubmittedForm() { + return isASubmittedFormMessage(this.message); + }, + submittedFormValues() { + return this.messageContentAttributes.submitted_values.map( + submittedValue => ({ + id: submittedValue.name, + content: submittedValue.value, + }) + ); + }, }, }; @@ -207,6 +232,10 @@ export default { border-top-right-radius: $space-smaller; } } + + &.has-response + .agent-message-wrap { + margin-top: $space-normal; + } } } diff --git a/app/javascript/widget/components/AgentMessageBubble.vue b/app/javascript/widget/components/AgentMessageBubble.vue index cc2044980..1062614da 100755 --- a/app/javascript/widget/components/AgentMessageBubble.vue +++ b/app/javascript/widget/components/AgentMessageBubble.vue @@ -21,7 +21,7 @@ import GroupedAvatars from 'widget/components/GroupedAvatars.vue'; -import { getAvailableAgentsText } from 'widget/helpers/utils'; +import agentMixin from '../mixins/agentMixin'; export default { name: 'AvailableAgents', components: { GroupedAvatars }, + mixins: [agentMixin], props: { agents: { type: Array, @@ -37,7 +38,7 @@ export default { })); }, title() { - return getAvailableAgentsText(this.agents); + return this.getAvailableAgentsText(this.agents); }, }, }; diff --git a/app/javascript/widget/components/Branding.vue b/app/javascript/widget/components/Branding.vue index f1c79e62e..4c6c787bf 100644 --- a/app/javascript/widget/components/Branding.vue +++ b/app/javascript/widget/components/Branding.vue @@ -1,15 +1,34 @@ + +