diff --git a/.circleci/config.yml b/.circleci/config.yml index 2c55d21b2..ce22c7c86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,8 +12,8 @@ defaults: &defaults # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: circleci/postgres:9.4 - - image: circleci/redis:5.0.7-alpine + - image: circleci/postgres:alpine + - image: circleci/redis:alpine environment: - CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f @@ -95,24 +95,24 @@ jobs: command: yarn run eslint # Run rails tests - - run: + - run: name: Run backend tests command: | bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) ./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json - persist_to_workspace: root: tmp - paths: + paths: - codeclimate.backend.json - - run: + - run: name: Run frontend tests command: | yarn test:coverage ./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info - persist_to_workspace: root: tmp - paths: + paths: - codeclimate.frontend.json # collect reports @@ -126,4 +126,4 @@ jobs: name: Upload coverage results to Code Climate command: | ./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 2 -o tmp/codeclimate.total.json - ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json \ No newline at end of file + ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json diff --git a/.env.example b/.env.example index 1b9cbb3e6..5bcb4ac9e 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,12 @@ SECRET_KEY_BASE= +# Force all access to the app over SSL, default is set to false +FORCE_SSL= + +# This lets you control new sign ups on your chatwoot installation +# true : default option, allows sign ups +# false : disables all the end points related to sign ups +# api_only: disables the UI for signup, but you can create sign ups via the account apis +ENABLE_ACCOUNT_SIGNUP= #redis config REDIS_URL=redis://redis:6379 @@ -20,14 +28,16 @@ FB_APP_SECRET= FB_APP_ID= #twitter app +TWITTER_APP_ID= TWITTER_CONSUMER_KEY= TWITTER_CONSUMER_SECRET= +TWITTER_ENVIRONMENT= #mail MAILER_SENDER_EMAIL=accounts@chatwoot.com SMTP_PORT=1025 SMTP_DOMAIN=chatwoot.com -# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog", +# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog", # else set the value as "localhost" SMTP_ADDRESS=mailhog SMTP_USERNAME= @@ -59,4 +69,4 @@ ENABLE_BILLING= CHARGEBEE_API_KEY= CHARGEBEE_SITE= CHARGEBEE_WEBHOOK_USERNAME= -CHARGEBEE_WEBHOOK_PASSWORD= \ No newline at end of file +CHARGEBEE_WEBHOOK_PASSWORD= diff --git a/.eslintrc.js b/.eslintrc.js index c6c52bb34..c65d7e2e7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,10 +24,12 @@ module.exports = { 'multiline': { 'max': 1, 'allowFirstLine': false - } + }, }], 'vue/html-self-closing': 'off', - "vue/no-v-html": 'off' + "vue/no-v-html": 'off', + 'import/extensions': ['never'] + }, settings: { 'import/resolver': { diff --git a/.gitignore b/.gitignore index 7192ba9d9..629e1676e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,10 @@ public/packs* *.swo *.un~ .jest-cache + +#VS Code files +.vscode + # ignore jetbrains IDE files .idea diff --git a/.rubocop.yml b/.rubocop.yml index a21f6eada..7c73f0191 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -20,14 +20,12 @@ Style/GlobalVars: Exclude: - 'config/initializers/redis.rb' - 'lib/redis/alfred.rb' - - 'app/controllers/api/v1/webhooks_controller.rb' - - 'app/services/twitter/send_reply_service.rb' - - 'spec/services/twitter/send_reply_service_spec.rb' Metrics/BlockLength: Exclude: - spec/**/* - '**/routes.rb' - 'config/environments/*' + - db/schema.rb Rails/ApplicationController: Exclude: - 'app/controllers/api/v1/widget/messages_controller.rb' @@ -41,6 +39,8 @@ Style/ClassAndModuleChildren: RSpec/NestedGroups: Enabled: true Max: 4 +RSpec/MessageSpies: + Enabled: false AllCops: Exclude: - db/* diff --git a/.scss-lint.yml b/.scss-lint.yml index 215e9c49e..dadb2c2cd 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -1,7 +1,281 @@ +# Default application configuration that all configurations inherit from. + +scss_files: '**/*.scss' +plugin_directories: ['.scss-linters'] + +# List of gem names to load custom linters from (make sure they are already +# installed) +plugin_gems: [] + +# Default severity of all linters. +severity: warning + linters: + BangFormat: + enabled: true + space_before_bang: true + space_after_bang: false + + BemDepth: + enabled: false + max_elements: 1 + + BorderZero: + enabled: true + convention: zero # or `none` + + ChainedClasses: + enabled: false + + ColorKeyword: + enabled: true + + ColorVariable: + enabled: true + + Comment: + enabled: true + style: silent + + DebugStatement: + enabled: true + + DeclarationOrder: + enabled: true + + DisableLinterReason: + enabled: false + + DuplicateProperty: + enabled: true + + ElsePlacement: + enabled: true + style: same_line # or 'new_line' + + EmptyLineBetweenBlocks: + enabled: true + ignore_single_line_blocks: true + + EmptyRule: + enabled: true + + ExtendDirective: + enabled: false + + FinalNewline: + enabled: true + present: true + + HexLength: + enabled: true + style: short # or 'long' + + HexNotation: + enabled: true + style: lowercase # or 'uppercase' + + HexValidation: + enabled: true + + IdSelector: + enabled: true + + ImportantRule: + enabled: true + + ImportPath: + enabled: true + leading_underscore: false + filename_extension: false + + Indentation: + enabled: true + allow_non_nested_indentation: false + character: space # or 'tab' + width: 2 + LeadingZero: enabled: false + MergeableSelector: + enabled: true + force_nesting: true + + NameFormat: + enabled: true + allow_leading_underscore: true + convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern + + NestingDepth: + enabled: true + max_depth: 6 + ignore_parent_selectors: false + + PlaceholderInExtend: + enabled: true + + PrivateNamingConvention: + enabled: false + prefix: _ + + PropertyCount: + enabled: false + include_nested: false + max_properties: 10 + + PropertySortOrder: + enabled: true + ignore_unspecified: false + min_properties: 2 + separate_groups: false + + PropertySpelling: + enabled: true + extra_properties: [] + disabled_properties: [] + + PropertyUnits: + enabled: true + global: [ + 'ch', + 'em', + 'ex', + 'rem', # Font-relative lengths + 'cm', + 'in', + 'mm', + 'pc', + 'pt', + 'px', + 'q', # Absolute lengths + 'vh', + 'vw', + 'vmin', + 'vmax', # Viewport-percentage lengths + 'fr', # Grid fractional lengths + 'deg', + 'grad', + 'rad', + 'turn', # Angle + 'ms', + 's', # Duration + 'Hz', + 'kHz', # Frequency + 'dpi', + 'dpcm', + 'dppx', # Resolution + '%', + ] # Other + properties: {} + + PseudoElement: + enabled: true + + QualifyingElement: + enabled: true + allow_element_with_attribute: false + allow_element_with_class: false + allow_element_with_id: false + + SelectorDepth: + enabled: true + max_depth: 5 + + SelectorFormat: + enabled: false + + Shorthand: + enabled: true + allowed_shorthands: [1, 2, 3, 4] + + SingleLinePerProperty: + enabled: true + allow_single_line_rule_sets: true + + SingleLinePerSelector: + enabled: true + + SpaceAfterComma: + enabled: true + style: one_space # or 'no_space', or 'at_least_one_space' + + SpaceAfterComment: + enabled: false + style: one_space # or 'no_space', or 'at_least_one_space' + allow_empty_comments: true + + SpaceAfterPropertyColon: + enabled: true + style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' + + SpaceAfterPropertyName: + enabled: true + + SpaceAfterVariableColon: + enabled: false + style: one_space # or 'no_space', 'at_least_one_space' or 'one_space_or_newline' + + SpaceAfterVariableName: + enabled: true + + SpaceAroundOperator: + enabled: true + style: one_space # or 'at_least_one_space', or 'no_space' + + SpaceBeforeBrace: + enabled: true + style: space # or 'new_line' + allow_single_line_padding: false + + SpaceBetweenParens: + enabled: true + spaces: 0 + + StringQuotes: + enabled: true + style: single_quotes # or double_quotes + + TrailingSemicolon: + enabled: true + + TrailingWhitespace: + enabled: true + + TrailingZero: + enabled: false + + TransitionAll: + enabled: false + + UnnecessaryMantissa: + enabled: false + + UnnecessaryParentReference: + enabled: true + + UrlFormat: + enabled: true + + UrlQuotes: + enabled: true + + VariableForProperty: + enabled: false + properties: [] + + VendorPrefix: + enabled: true + identifier_list: base + additional_identifiers: [] + excluded_identifiers: [] + + ZeroUnit: + enabled: true + + Compass::*: + enabled: false + exclude: - 'app/javascript/widget/assets/scss/_reset.scss' - 'app/javascript/widget/assets/scss/sdk.css' diff --git a/Gemfile b/Gemfile index 6e3944ae3..b3f1a2995 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'hashie' gem 'jbuilder' gem 'kaminari' gem 'responders' +gem 'rest-client' gem 'time_diff' gem 'tzinfo-data' gem 'valid_email2' @@ -60,7 +61,6 @@ gem 'chargebee' ##--- gems for channels ---## gem 'facebook-messenger' gem 'telegram-bot-ruby' -gem 'twitter' # twitty will handle subscription of twitter account events gem 'twitty', git: 'https://github.com/chatwoot/twitty' @@ -72,19 +72,27 @@ gem 'haikunator' ##--- gems for debugging and error reporting ---## # static analysis gem 'brakeman' +gem 'scout_apm' gem 'sentry-raven' ##-- background job processing --## gem 'sidekiq' +##-- used for single column multiple binary flags in notification settings/feature flagging --## +gem 'flag_shih_tzu' + group :development do gem 'annotate' gem 'bullet' gem 'letter_opener' gem 'web-console' + + # used in swagger build + gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0' end group :development, :test do + # locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved gem 'action-cable-testing' gem 'bundle-audit', require: false gem 'byebug', platform: :mri @@ -98,9 +106,9 @@ group :development, :test do gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false gem 'rubocop-rspec', require: false + gem 'scss_lint', require: false gem 'seed_dump' gem 'shoulda-matchers' - # locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved gem 'simplecov', '0.17.1', require: false gem 'spring' gem 'spring-watcher-listen' diff --git a/Gemfile.lock b/Gemfile.lock index 6cc4cf008..7131f36e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,18 @@ GIT remote: https://github.com/chatwoot/twitty - revision: 58b4958d7f4a58eec8fe9543caedb232308253f6 + revision: c1edd557401d1e8a197b19e738f82e39507a8e2d specs: twitty (0.1.0) oauth +GIT + remote: https://github.com/tzmfreedom/json_refs + revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d + ref: e32deb0 + specs: + json_refs (0.1.2) + hana + GEM remote: https://rubygems.org/ specs: @@ -109,7 +117,6 @@ GEM msgpack (~> 1.0) brakeman (4.7.2) browser (3.0.3) - buftok (0.2.0) builder (3.2.4) bullet (6.1.0) activesupport (>= 3.0.0) @@ -170,6 +177,7 @@ GEM faraday_middleware (0.14.0) faraday (>= 0.7.4, < 1.0) ffi (1.12.2) + flag_shih_tzu (0.3.23) foreman (0.87.0) globalid (0.4.2) activesupport (>= 4.2.0) @@ -202,17 +210,11 @@ GEM os (>= 0.9, < 2.0) signet (~> 0.12) haikunator (1.1.0) + hana (1.3.5) hashie (4.1.0) - http (3.3.0) - addressable (~> 2.3) - http-cookie (~> 1.0) - http-form_data (~> 2.0) - http_parser.rb (~> 0.6.0) http-accept (1.7.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (2.2.0) - http_parser.rb (0.6.0) httparty (0.17.3) mime-types (~> 3.0) multi_xml (>= 0.5.2) @@ -259,8 +261,6 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) memoist (0.16.2) - memoizable (0.4.2) - thread_safe (~> 0.3, >= 0.3.1) method_source (0.9.2) mime-types (3.3.1) mime-types-data (~> 3.2015) @@ -275,11 +275,10 @@ GEM multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.1.1) - naught (1.1.0) netrc (0.11.0) nightfury (1.0.1) nio4r (2.5.2) - nokogiri (1.10.7) + nokogiri (1.10.8) mini_portile2 (~> 2.4.0) oauth (0.5.4) orm_adapter (0.5.0) @@ -294,7 +293,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.3) - puma (4.3.1) + puma (4.3.2) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) @@ -393,6 +392,15 @@ GEM rubocop-rspec (1.37.1) rubocop (>= 0.68.1) ruby-progressbar (1.10.1) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + scout_apm (2.6.6) + parser + scss_lint (0.59.0) + sass (~> 3.5, >= 3.5.5) seed_dump (3.3.1) activerecord (>= 4) activesupport (>= 4) @@ -410,7 +418,6 @@ GEM faraday (~> 0.9) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simple_oauth (0.3.1) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -436,17 +443,6 @@ GEM time_diff (0.3.0) activesupport i18n - twitter (6.2.0) - addressable (~> 2.3) - buftok (~> 0.2.0) - equalizer (~> 0.0.11) - http (~> 3.0) - http-form_data (~> 2.0) - http_parser.rb (~> 0.6.0) - memoizable (~> 0.4.0) - multipart-post (~> 2.0) - naught (~> 1.0) - simple_oauth (~> 0.3.0) tzinfo (1.2.6) thread_safe (~> 0.1) tzinfo-data (1.2019.3) @@ -507,11 +503,13 @@ DEPENDENCIES facebook-messenger factory_bot_rails faker + flag_shih_tzu foreman google-cloud-storage haikunator hashie jbuilder + json_refs! jwt kaminari koala @@ -530,11 +528,14 @@ DEPENDENCIES redis-namespace redis-rack-cache responders + rest-client rspec-rails (~> 4.0.0.beta2) rubocop rubocop-performance rubocop-rails rubocop-rspec + scout_apm + scss_lint seed_dump sentry-raven shoulda-matchers @@ -544,7 +545,6 @@ DEPENDENCIES spring-watcher-listen telegram-bot-ruby time_diff - twitter twitty! tzinfo-data uglifier diff --git a/README.md b/README.md index 117a1fc9e..db4559b45 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,10 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under ## Docker -You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot) - -```bash -docker pull chatwoot/chatwoot -``` +Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`. Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker. -Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using docker composer. - ## Contributors ✨ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors): diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js new file mode 100644 index 000000000..9dc5fc1e4 --- /dev/null +++ b/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = ''; diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 8cad3f87b..6d3a7d39b 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -2,7 +2,7 @@ require 'open-uri' # This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` # Assumptions -# 1. Incase of an outgoing message which is echo, fb_id will NOT be nil, +# 1. Incase of an outgoing message which is echo, source_id will NOT be nil, # based on this we are showing "not sent from chatwoot" message in frontend # Hence there is no need to set user_id in message for outgoing echo messages. @@ -121,7 +121,7 @@ class Messages::MessageBuilder inbox_id: conversation.inbox_id, message_type: @message_type, content: response.content, - fb_id: response.identifier + source_id: response.identifier } end diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb index 8517f4504..b75e8f84b 100644 --- a/app/builders/messages/outgoing/normal_builder.rb +++ b/app/builders/messages/outgoing/normal_builder.rb @@ -23,7 +23,7 @@ class Messages::Outgoing::NormalBuilder content: @content, private: @private, user_id: @user.id, - fb_id: @fb_id + source_id: @fb_id } end end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index b2678f794..097a40dab 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -1,16 +1,14 @@ class Api::BaseController < ApplicationController respond_to :json before_action :authenticate_user! - unless Rails.env.development? - rescue_from StandardError do |exception| - Raven.capture_exception(exception) - render json: { error: '500 error', message: exception.message }.to_json, status: 500 - end - end private def set_conversation @conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id]) end + + def check_billing_enabled + raise ActionController::RoutingError, 'Not Found' unless ENV['BILLING_ENABLED'] + end end diff --git a/app/controllers/api/v1/account/webhooks_controller.rb b/app/controllers/api/v1/account/webhooks_controller.rb new file mode 100644 index 000000000..730e7b9b1 --- /dev/null +++ b/app/controllers/api/v1/account/webhooks_controller.rb @@ -0,0 +1,36 @@ +class Api::V1::Account::WebhooksController < Api::BaseController + before_action :check_authorization + before_action :fetch_webhook, only: [:update, :destroy] + + def index + @webhooks = current_account.webhooks + end + + def create + @webhook = current_account.webhooks.new(webhook_params) + @webhook.save! + end + + def update + @webhook.update!(webhook_params) + end + + def destroy + @webhook.destroy + head :ok + end + + private + + def webhook_params + params.require(:webhook).permit(:inbox_id, :url) + end + + def fetch_webhook + @webhook = current_account.webhooks.find(params[:id]) + end + + def check_authorization + authorize(Webhook) + end +end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 0bbf2f67f..96e488231 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController skip_before_action :verify_authenticity_token, only: [:create] skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception, only: [:create], raise: false + before_action :check_signup_enabled rescue_from CustomExceptions::Account::InvalidEmail, CustomExceptions::Account::UserExists, @@ -30,4 +31,8 @@ class Api::V1::AccountsController < Api::BaseController def account_params params.permit(:account_name, :email) end + + def check_signup_enabled + raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false' + end end diff --git a/app/controllers/api/v1/callbacks_controller.rb b/app/controllers/api/v1/callbacks_controller.rb index cc6b00958..031d5accc 100644 --- a/app/controllers/api/v1/callbacks_controller.rb +++ b/app/controllers/api/v1/callbacks_controller.rb @@ -1,8 +1,7 @@ require 'rest-client' require 'telegram/bot' -class Api::V1::CallbacksController < ApplicationController - skip_before_action :verify_authenticity_token, only: [:register_facebook_page] - skip_before_action :authenticate_user!, only: [:register_facebook_page], raise: false +class Api::V1::CallbacksController < Api::BaseController + before_action :inbox, only: [:reauthorize_page] def register_facebook_page user_access_token = params[:user_access_token] @@ -25,15 +24,13 @@ class Api::V1::CallbacksController < ApplicationController # get params[:inbox_id], current_account, params[:omniauth_token] def reauthorize_page - if @inbox&.first&.facebook? + if @inbox&.facebook? fb_page_id = @inbox.channel.page_id page_details = fb_object.get_connections('me', 'accounts') - (page_details || []).each do |page_detail| - if fb_page_id == page_detail['id'] # found the page which has to be reauthorised - update_fb_page(fb_page_id, page_detail['access_token']) - head :ok - end + if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] }) + update_fb_page(fb_page_id, page_detail['access_token']) + return head :ok end end @@ -46,18 +43,13 @@ class Api::V1::CallbacksController < ApplicationController @inbox = current_account.inboxes.find_by(id: params[:inbox_id]) end - def update_fb_page - if fb_page(fb_page_id) - fb_page.update_attributes!( - user_access_token: @user_access_token, page_access_token: access_token - ) - head :ok - else - head :unprocessable_entity - end + def update_fb_page(fb_page_id, access_token) + get_fb_page(fb_page_id)&.update!( + user_access_token: @user_access_token, page_access_token: access_token + ) end - def fb_page(fb_page_id) + def get_fb_page(fb_page_id) current_account.facebook_pages.find_by(page_id: fb_page_id) end @@ -68,7 +60,7 @@ class Api::V1::CallbacksController < ApplicationController def long_lived_token(omniauth_token) koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET']) - long_lived_token = koala.exchange_access_token_info(omniauth_token)['access_token'] + koala.exchange_access_token_info(omniauth_token)['access_token'] end def mark_already_existing_facebook_pages(data) @@ -81,7 +73,11 @@ class Api::V1::CallbacksController < ApplicationController end def set_avatar(facebook_channel, page_id) - avatar_resource = LocalResource.new(get_avatar_url(page_id)) + uri = get_avatar_url(page_id) + + return unless uri + + avatar_resource = LocalResource.new(uri) facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) end @@ -98,7 +94,6 @@ class Api::V1::CallbacksController < ApplicationController raise end pic_url = response.base_uri.to_s - Rails.logger.info(pic_url) rescue StandardError => e pic_url = nil end diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index cc179a75d..6c44114be 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -1,11 +1,5 @@ class Api::V1::ConversationsController < Api::BaseController - before_action :set_conversation, except: [:index, :get_messages] - - # TODO: move this to public controller - skip_before_action :authenticate_user!, only: [:get_messages] - skip_before_action :set_current_user, only: [:get_messages] - skip_before_action :check_subscription, only: [:get_messages] - skip_around_action :handle_with_exception, only: [:get_messages] + before_action :set_conversation, except: [:index] def index result = conversation_finder.perform @@ -27,11 +21,6 @@ class Api::V1::ConversationsController < Api::BaseController head :ok end - def get_messages - @conversation = Conversation.find(params[:id]) - @messages = messages_finder.perform - end - private def parsed_last_seen_at diff --git a/app/controllers/api/v1/inbox_members_controller.rb b/app/controllers/api/v1/inbox_members_controller.rb index ed47f409a..982ad00ba 100644 --- a/app/controllers/api/v1/inbox_members_controller.rb +++ b/app/controllers/api/v1/inbox_members_controller.rb @@ -4,17 +4,11 @@ class Api::V1::InboxMembersController < Api::BaseController def create # update also done via same action - if @inbox - begin - update_agents_list - head :ok - rescue StandardError => e - Rails.logger.debug "Rescued: #{e.inspect}" - render_could_not_create_error('Could not add agents to inbox') - end - else - render_not_found_error('Agents or inbox not found') - end + update_agents_list + head :ok + rescue StandardError => e + Rails.logger.debug "Rescued: #{e.inspect}" + render_could_not_create_error('Could not add agents to inbox') end def show diff --git a/app/controllers/api/v1/inboxes_controller.rb b/app/controllers/api/v1/inboxes_controller.rb index 00b6d0c1f..e9005a0c7 100644 --- a/app/controllers/api/v1/inboxes_controller.rb +++ b/app/controllers/api/v1/inboxes_controller.rb @@ -1,6 +1,6 @@ class Api::V1::InboxesController < Api::BaseController before_action :check_authorization - before_action :fetch_inbox, only: [:destroy] + before_action :fetch_inbox, only: [:destroy, :update] def index @inboxes = policy_scope(current_account.inboxes) @@ -11,6 +11,10 @@ class Api::V1::InboxesController < Api::BaseController head :ok end + def update + @inbox.update(inbox_update_params) + end + private def fetch_inbox @@ -20,4 +24,8 @@ class Api::V1::InboxesController < Api::BaseController def check_authorization authorize(Inbox) end + + def inbox_update_params + params.require(:inbox).permit(:enable_auto_assignment) + end end diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 72e432f74..9a0bbfc17 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -1,5 +1,5 @@ class Api::V1::ProfilesController < Api::BaseController - before_action :fetch_user + before_action :set_user def show render json: @user @@ -7,12 +7,11 @@ class Api::V1::ProfilesController < Api::BaseController def update @user.update!(profile_params) - render json: @user end private - def fetch_user + def set_user @user = current_user end diff --git a/app/controllers/api/v1/subscriptions_controller.rb b/app/controllers/api/v1/subscriptions_controller.rb index 4ddd5a161..92e4f7f13 100644 --- a/app/controllers/api/v1/subscriptions_controller.rb +++ b/app/controllers/api/v1/subscriptions_controller.rb @@ -1,6 +1,8 @@ -class Api::V1::SubscriptionsController < ApplicationController +class Api::V1::SubscriptionsController < Api::BaseController skip_before_action :check_subscription + before_action :check_billing_enabled + def index render json: current_account.subscription_data end diff --git a/app/controllers/api/v1/user/notification_settings_controller.rb b/app/controllers/api/v1/user/notification_settings_controller.rb new file mode 100644 index 000000000..78c5dc720 --- /dev/null +++ b/app/controllers/api/v1/user/notification_settings_controller.rb @@ -0,0 +1,29 @@ +class Api::V1::User::NotificationSettingsController < Api::BaseController + before_action :set_user, :load_notification_setting + + def show; end + + def update + update_flags + @notification_setting.save! + render action: 'show' + end + + private + + def set_user + @user = current_user + end + + def load_notification_setting + @notification_setting = @user.notification_settings.find_by(account_id: current_account.id) + end + + def notification_setting_params + params.require(:notification_settings).permit(selected_email_flags: []) + end + + def update_flags + @notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags] + end +end diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb index 10bcfc25a..d15b414c1 100644 --- a/app/controllers/api/v1/webhooks_controller.rb +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -4,6 +4,7 @@ class Api::V1::WebhooksController < ApplicationController skip_before_action :check_subscription before_action :login_from_basic_auth, only: [:chargebee] + before_action :check_billing_enabled, only: [:chargebee] def chargebee chargebee_consumer.consume head :ok @@ -13,7 +14,7 @@ class Api::V1::WebhooksController < ApplicationController end def twitter_crc - render json: { response_token: "sha256=#{$twitter.generate_crc(params[:crc_token])}" } + render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" } end def twitter_events @@ -26,6 +27,12 @@ class Api::V1::WebhooksController < ApplicationController private + def twitter_client + Twitty::Facade.new do |config| + config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil) + end + end + def login_from_basic_auth authenticate_or_request_with_http_basic do |username, password| username == ENV['CHARGEBEE_WEBHOOK_USERNAME'] && password == ENV['CHARGEBEE_WEBHOOK_PASSWORD'] diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index a6087bb8c..6e1eae9fe 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -31,9 +31,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController def message_params { account_id: conversation.account_id, + contact_id: @contact.id, + content: permitted_params[:message][:content], inbox_id: conversation.inbox_id, - message_type: :incoming, - content: permitted_params[:message][:content] + message_type: :incoming } end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 27e627a21..f38c45c63 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -67,7 +67,7 @@ class ApplicationController < ActionController::Base def check_subscription # This block is left over from the initial version of chatwoot # We might reuse this later in the hosted version of chatwoot. - return unless ENV['BILLING_ENABLED'] + return if !ENV['BILLING_ENABLED'] || !current_user if current_subscription.trial? && current_subscription.expiry < Date.current render json: { error: 'Trial Expired' }, status: :trial_expired diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index 8ef7f2d11..3a6614074 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -2,4 +2,8 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle # Prevent session parameter from being passed # Unpermitted parameter: session wrap_parameters format: [] + + def render_create_success + render 'devise/auth.json' + end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index e700f2570..d811b859e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,13 +1,9 @@ require 'rest-client' require 'telegram/bot' + class HomeController < ApplicationController skip_before_action :verify_authenticity_token, only: [:telegram] skip_before_action :authenticate_user!, only: [:telegram], raise: false skip_before_action :set_current_user skip_before_action :check_subscription - def index; end - - def status - head :ok - end end diff --git a/app/controllers/swagger_controller.rb b/app/controllers/swagger_controller.rb new file mode 100644 index 000000000..c5f8c0f5b --- /dev/null +++ b/app/controllers/swagger_controller.rb @@ -0,0 +1,18 @@ +class SwaggerController < ApplicationController + def respond + if Rails.env.development? || Rails.env.test? + render inline: File.read(Rails.root.join('swagger', derived_path)) + else + head 404 + end + end + + private + + def derived_path + params[:path] ||= 'index.html' + path = params[:path] + path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s) + path + end +end diff --git a/app/controllers/twitter/authorizations_controller.rb b/app/controllers/twitter/authorizations_controller.rb new file mode 100644 index 000000000..69145ff84 --- /dev/null +++ b/app/controllers/twitter/authorizations_controller.rb @@ -0,0 +1,30 @@ +class Twitter::AuthorizationsController < Twitter::BaseController + def create + @response = twitter_client.request_oauth_token(url: twitter_callback_url) + + if @response.status == '200' + ::Redis::Alfred.setex(oauth_token, account.id) + redirect_to oauth_authorize_endpoint(oauth_token) + else + redirect_to app_new_twitter_inbox_url + end + end + + private + + def oauth_token + parsed_body['oauth_token'] + end + + def user + @user ||= User.find_by(id: params[:user_id]) + end + + def account + @account ||= user.account + end + + def oauth_authorize_endpoint(oauth_token) + "#{twitter_api_base_url}/oauth/authorize?oauth_token=#{oauth_token}" + end +end diff --git a/app/controllers/twitter/base_controller.rb b/app/controllers/twitter/base_controller.rb new file mode 100644 index 000000000..353c1159c --- /dev/null +++ b/app/controllers/twitter/base_controller.rb @@ -0,0 +1,24 @@ +class Twitter::BaseController < ApplicationController + private + + def parsed_body + @parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body) + end + + def host + ENV.fetch('FRONTEND_URL', '') + end + + def twitter_client + Twitty::Facade.new do |config| + config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil) + config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil) + config.base_url = twitter_api_base_url + config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '') + end + end + + def twitter_api_base_url + 'https://api.twitter.com' + end +end diff --git a/app/controllers/twitter/callbacks_controller.rb b/app/controllers/twitter/callbacks_controller.rb new file mode 100644 index 000000000..876720f26 --- /dev/null +++ b/app/controllers/twitter/callbacks_controller.rb @@ -0,0 +1,51 @@ +class Twitter::CallbacksController < Twitter::BaseController + def show + @response = twitter_client.access_token( + oauth_token: permitted_params[:oauth_token], + oauth_verifier: permitted_params[:oauth_verifier] + ) + if @response.status == '200' + inbox = build_inbox + ::Redis::Alfred.delete(permitted_params[:oauth_token]) + ::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform + redirect_to app_twitter_inbox_agents_url(inbox_id: inbox.id) + else + redirect_to app_new_twitter_inbox_url + end + end + + private + + def parsed_body + @parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body) + end + + def account_id + ::Redis::Alfred.get(permitted_params[:oauth_token]) + end + + def account + @account ||= Account.find_by!(id: account_id) + end + + def build_inbox + ActiveRecord::Base.transaction do + twitter_profile = account.twitter_profiles.create( + twitter_access_token: parsed_body['oauth_token'], + twitter_access_token_secret: parsed_body['oauth_token_secret'], + profile_id: parsed_body['user_id'], + name: parsed_body['screen_name'] + ) + account.inboxes.create( + name: parsed_body['screen_name'], + channel: twitter_profile + ) + rescue StandardError => e + Rails.logger e + end + end + + def permitted_params + params.permit(:oauth_token, :oauth_verifier) + end +end diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 60b522831..7b7cb4f52 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -4,9 +4,7 @@ class WidgetsController < ActionController::Base before_action :set_contact before_action :build_contact - def index - render - end + def index; end private diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index 97da6de93..dff0c9d92 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -5,7 +5,7 @@ class AsyncDispatcher < BaseDispatcher end def listeners - listeners = [ReportingListener.instance] + listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance] listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED'] listeners end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index f4e114bf4..fcc0daf98 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -1,23 +1,18 @@ class ConversationFinder attr_reader :current_user, :current_account, :params - ASSIGNEE_TYPES = { me: 0, unassigned: 1, all: 2 }.freeze - - ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert - ASSIGNEE_TYPES_BY_ID.default = :me - DEFAULT_STATUS = 'open'.freeze # assumptions # inbox_id if not given, take from all conversations, else specific to inbox - # assignee_type if not given, take 'me' + # assignee_type if not given, take 'all' # conversation_status if not given, take 'open' # response of this class will be of type # {conversations: [array of conversations], count: {open: count, resolved: count}} # params - # assignee_type_id, inbox_id, :status + # assignee_type, inbox_id, :status def initialize(current_user, params) @current_user = current_user @@ -62,7 +57,7 @@ class ConversationFinder end def set_assignee_type - @assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]] + @assignee_type = params[:assignee_type] end def find_all_conversations @@ -72,12 +67,10 @@ class ConversationFinder end def filter_by_assignee_type - if @assignee_type_id == ASSIGNEE_TYPES[:me] + if @assignee_type == 'me' @conversations = @conversations.assigned_to(current_user) - elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned] + elsif @assignee_type == 'unassigned' @conversations = @conversations.unassigned - elsif @assignee_type_id == ASSIGNEE_TYPES[:all] - @conversations end @conversations end diff --git a/app/helpers/api/v1/webhooks_helper.rb b/app/helpers/api/v1/webhooks_helper.rb deleted file mode 100644 index 160b78c7c..000000000 --- a/app/helpers/api/v1/webhooks_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Api::V1::WebhooksHelper -end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 692b92a65..acf177d7c 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -18,8 +18,7 @@ export default { }, mounted() { - this.$store.dispatch('set_user'); - this.$store.dispatch('validityCheck'); + this.$store.dispatch('setUser'); }, }; diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 031d02b14..f247089cd 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -1,29 +1,10 @@ /* eslint no-console: 0 */ /* global axios */ /* eslint no-undef: "error" */ -/* eslint-env browser */ -/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ -import moment from 'moment'; import Cookies from 'js-cookie'; import endPoints from './endPoints'; -import { frontendURL } from '../helper/URLHelper'; - -const setAuthCredentials = response => { - const expiryDate = moment.unix(response.headers.expiry); - Cookies.set('auth_data', response.headers, { - expires: expiryDate.diff(moment(), 'days'), - }); - Cookies.set('user', response.data.data, { - expires: expiryDate.diff(moment(), 'days'), - }); -}; - -const clearCookiesOnLogout = () => { - Cookies.remove('auth_data'); - Cookies.remove('user'); - window.location = frontendURL('login'); -}; +import { setAuthCredentials, clearCookiesOnLogout } from '../store/utils/api'; export default { login(creds) { @@ -60,20 +41,7 @@ export default { }, validityCheck() { const urlData = endPoints('validityCheck'); - const fetchPromise = new Promise((resolve, reject) => { - axios - .get(urlData.url) - .then(response => { - resolve(response); - }) - .catch(error => { - if (error.response.status === 401) { - clearCookiesOnLogout(); - } - reject(error); - }); - }); - return fetchPromise; + return axios.get(urlData.url); }, logout() { const urlData = endPoints('logout'); @@ -136,13 +104,7 @@ export default { password, }) .then(response => { - const expiryDate = moment.unix(response.headers.expiry); - Cookies.set('auth_data', response.headers, { - expires: expiryDate.diff(moment(), 'days'), - }); - Cookies.set('user', response.data.data, { - expires: expiryDate.diff(moment(), 'days'), - }); + setAuthCredentials(response); resolve(response); }) .catch(error => { @@ -155,4 +117,22 @@ export default { const urlData = endPoints('resetPassword'); return axios.post(urlData.url, { email }); }, + + profileUpdate({ name, email, password, password_confirmation, avatar }) { + const formData = new FormData(); + if (name) { + formData.append('profile[name]', name); + } + if (email) { + formData.append('profile[email]', email); + } + if (password && password_confirmation) { + formData.append('profile[password]', password); + formData.append('profile[password_confirmation]', password_confirmation); + } + if (avatar) { + formData.append('profile[avatar]', avatar); + } + return axios.put(endPoints('profileUpdate').url, formData); + }, }; diff --git a/app/javascript/dashboard/api/conversations.js b/app/javascript/dashboard/api/conversations.js index 02d312388..fd36f8db3 100644 --- a/app/javascript/dashboard/api/conversations.js +++ b/app/javascript/dashboard/api/conversations.js @@ -10,8 +10,8 @@ class ConversationApi extends ApiClient { return axios.get(`${this.url}/${conversationID}/labels`); } - createLabels(conversationID) { - return axios.get(`${this.url}/${conversationID}/labels`); + updateLabels(conversationID, labels) { + return axios.post(`${this.url}/${conversationID}/labels`, { labels }); } } diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index 2898362cc..10a0608bd 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -10,6 +10,9 @@ const endPoints = { validityCheck: { url: '/auth/validate_token', }, + profileUpdate: { + url: '/api/v1/profile', + }, logout: { url: 'auth/sign_out', }, diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index b444d0e17..6a86cff7f 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -6,12 +6,13 @@ class ConversationApi extends ApiClient { super('conversations'); } - get({ inboxId, status, assigneeType }) { + get({ inboxId, status, assigneeType, page }) { return axios.get(this.url, { params: { inbox_id: inboxId, status, - assignee_type_id: assigneeType, + assignee_type: assigneeType, + page, }, }); } diff --git a/app/javascript/dashboard/api/specs/conversations.spec.js b/app/javascript/dashboard/api/specs/conversations.spec.js index 71cebae3a..2ac815459 100644 --- a/app/javascript/dashboard/api/specs/conversations.spec.js +++ b/app/javascript/dashboard/api/specs/conversations.spec.js @@ -10,6 +10,6 @@ describe('#ConversationApi', () => { expect(conversations).toHaveProperty('update'); expect(conversations).toHaveProperty('delete'); expect(conversations).toHaveProperty('getLabels'); - expect(conversations).toHaveProperty('createLabels'); + expect(conversations).toHaveProperty('updateLabels'); }); }); diff --git a/app/javascript/dashboard/api/specs/userNotificationSettings.spec.js b/app/javascript/dashboard/api/specs/userNotificationSettings.spec.js new file mode 100644 index 000000000..f65f1c3a3 --- /dev/null +++ b/app/javascript/dashboard/api/specs/userNotificationSettings.spec.js @@ -0,0 +1,13 @@ +import userNotificationSettings from '../userNotificationSettings'; +import ApiClient from '../ApiClient'; + +describe('#AgentAPI', () => { + it('creates correct instance', () => { + expect(userNotificationSettings).toBeInstanceOf(ApiClient); + expect(userNotificationSettings).toHaveProperty('get'); + expect(userNotificationSettings).toHaveProperty('show'); + expect(userNotificationSettings).toHaveProperty('create'); + expect(userNotificationSettings).toHaveProperty('update'); + expect(userNotificationSettings).toHaveProperty('delete'); + }); +}); diff --git a/app/javascript/dashboard/api/userNotificationSettings.js b/app/javascript/dashboard/api/userNotificationSettings.js new file mode 100644 index 000000000..15cea8942 --- /dev/null +++ b/app/javascript/dashboard/api/userNotificationSettings.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class UserNotificationSettings extends ApiClient { + constructor() { + super('user/notification_settings'); + } + + update(params) { + return axios.patch(`${this.url}`, params); + } +} + +export default new UserNotificationSettings(); diff --git a/app/javascript/dashboard/api/webhooks.js b/app/javascript/dashboard/api/webhooks.js new file mode 100644 index 000000000..229519dd7 --- /dev/null +++ b/app/javascript/dashboard/api/webhooks.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class WebHooks extends ApiClient { + constructor() { + super('account/webhooks'); + } +} + +export default new WebHooks(); diff --git a/app/javascript/dashboard/assets/audio/ding.mp3 b/app/javascript/dashboard/assets/audio/ding.mp3 deleted file mode 100644 index 1c4921711..000000000 Binary files a/app/javascript/dashboard/assets/audio/ding.mp3 and /dev/null differ diff --git a/app/javascript/dashboard/assets/images/integrations/cable.svg b/app/javascript/dashboard/assets/images/integrations/cable.svg new file mode 100644 index 000000000..2a9f7008d --- /dev/null +++ b/app/javascript/dashboard/assets/images/integrations/cable.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index f918ed97c..a544ae883 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -28,7 +28,7 @@ code { border: 0; - font-family: 'Monaco'; + font-family: 'Monaco', Verdana; font-size: $font-size-mini; &.hljs { diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index 769232229..4779db720 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -41,29 +41,34 @@ // 36. Tooltip // 37. Top Bar -@import "~foundation-sites/scss/util/util"; +@import '~foundation-sites/scss/util/util'; // 1. Global // --------- $global-font-size: 10px; $global-width: 100%; $global-lineheight: 1.5; -$foundation-palette: ( - primary: $color-woot, +$foundation-palette: (primary: $color-woot, secondary: #777, success: #13ce66, warning: #ffc82c, - alert: #ff4949 -); + alert: #ff4949); $light-gray: #c0ccda; $medium-gray: #8492a6; $dark-gray: $color-gray; -$black: #000000; +$black: #000; $white: #fff; $body-background: $white; $body-font-color: $color-body; -$body-font-family: 'Inter', -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", - Roboto, "Helvetica Neue", Arial, sans-serif; +$body-font-family: 'Inter', +-apple-system, +system-ui, +BlinkMacSystemFont, +"Segoe UI", +Roboto, +"Helvetica Neue", +Arial, +sans-serif; $body-antialiased: true; $global-margin: $space-one; $global-padding: $space-one; @@ -79,13 +84,11 @@ $print-transparent-backgrounds: true; // 2. Breakpoints // -------------- -$breakpoints: ( - small: 0, +$breakpoints: (small: 0, medium: 640px, large: 1024px, xlarge: 1200px, - xxlarge: 1440px -); + xxlarge: 1440px); $print-breakpoint: large; $breakpoint-classes: (small medium large); @@ -94,10 +97,8 @@ $breakpoint-classes: (small medium large); $grid-row-width: $global-width; $grid-column-count: 12; -$grid-column-gutter: ( - small: $zero, - medium: $zero -); +$grid-column-gutter: (small: $zero, + medium: $zero); $grid-column-align-edge: true; $block-grid-max: 8; @@ -105,54 +106,24 @@ $block-grid-max: 8; // ------------------ $header-font-family: $body-font-family; -$header-font-weight: $global-weight-normal; +$header-font-weight: $font-weight-medium; $header-font-style: normal; $font-family-monospace: $body-font-family; $header-color: $color-heading; $header-lineheight: 1.4; $header-margin-bottom: 0.5rem; -$header-styles: ( - small: ( - "h1": ( - "font-size": 24 - ), - "h2": ( - "font-size": 20 - ), - "h3": ( - "font-size": 19 - ), - "h4": ( - "font-size": 18 - ), - "h5": ( - "font-size": 17 - ), - "h6": ( - "font-size": 16 - ) - ), - medium: ( - "h1": ( - "font-size": 48 - ), - "h2": ( - "font-size": 40 - ), - "h3": ( - "font-size": 31 - ), - "h4": ( - "font-size": 25 - ), - "h5": ( - "font-size": 20 - ), - "h6": ( - "font-size": 16 - ) - ) -); +$header-styles: (small: ("h1": ("font-size": 24), + "h2": ("font-size": 20), + "h3": ("font-size": 19), + "h4": ("font-size": 18), + "h5": ("font-size": 17), + "h6": ("font-size": 16)), + medium: ("h1": ("font-size": 48), + "h2": ("font-size": 40), + "h3": ("font-size": 31), + "h4": ("font-size": 25), + "h5": ("font-size": 20), + "h6": ("font-size": 16))); $header-text-rendering: optimizeLegibility; $small-font-size: 80%; $header-small-font-color: $medium-gray; @@ -186,7 +157,7 @@ $blockquote-padding: rem-calc(9 20 0 19); $blockquote-border: 1px solid $medium-gray; $cite-font-size: rem-calc(13); $cite-color: $dark-gray; -$cite-pseudo-content: "\2014 \0020"; +$cite-pseudo-content: '\2014 \0020'; $keystroke-font: $font-family-monospace; $keystroke-color: $black; $keystroke-background: $light-gray; @@ -272,24 +243,23 @@ $button-background-hover: scale-color($button-background, $lightness: -15%); $button-color: $white; $button-color-alt: $white; $button-radius: $global-radius; -$button-sizes: ( - tiny: $font-size-micro, +$button-sizes: (tiny: $font-size-micro, small: $font-size-mini, default: $font-size-default, - large: $font-size-large -); + large: $font-size-large); $button-palette: $foundation-palette; $button-opacity-disabled: 0.25; $button-background-hover-lightness: -20%; $button-hollow-hover-lightness: -50%; -$button-transition: background-color 0.25s ease-out, color 0.25s ease-out; +$button-transition: background-color 0.25s ease-out, +color 0.25s ease-out; // 12. Button Group // ---------------- $buttongroup-margin: 1rem; $buttongroup-spacing: 1px; -$buttongroup-child-selector: ".button"; +$buttongroup-child-selector: '.button'; $buttongroup-expand-max: 6; $buttongroup-radius-on-each: true; @@ -322,18 +292,12 @@ $card-margin: $global-margin; // ---------------- $closebutton-position: right top; -$closebutton-offset-horizontal: ( - small: 0.66rem, - medium: 1rem -); -$closebutton-offset-vertical: ( - small: 0.33em, - medium: 0.5rem -); -$closebutton-size: ( - small: 1.5em, - medium: 2em -); +$closebutton-offset-horizontal: (small: 0.66rem, + medium: 1rem); +$closebutton-offset-vertical: (small: 0.33em, + medium: 0.5rem); +$closebutton-size: (small: 1.5em, + medium: 2em); $closebutton-lineheight: 1; $closebutton-color: $dark-gray; $closebutton-color-hover: $black; @@ -356,11 +320,9 @@ $dropdown-border: 1px solid $medium-gray; $dropdown-font-size: 1rem; $dropdown-width: 300px; $dropdown-radius: $global-radius; -$dropdown-sizes: ( - tiny: 100px, +$dropdown-sizes: (tiny: 100px, small: 200px, - large: 400px -); + large: 400px); // 18. Dropdown Menu // ----------------- @@ -455,12 +417,10 @@ $meter-fill-bad: $alert-color; // 24. Off-canvas // -------------- -$offcanvas-sizes: ( - small: 23rem, +$offcanvas-sizes: (small: 23rem, medium: 23rem, ); -$offcanvas-vertical-sizes: ( - small: 23rem, +$offcanvas-vertical-sizes: (small: 23rem, medium: 23rem, ); $offcanvas-background: $light-gray; @@ -472,7 +432,7 @@ $offcanvas-transition-length: 0.5s; $offcanvas-transition-timing: ease; $offcanvas-fixed-reveal: true; $offcanvas-exit-background: rgba($white, 0.25); -$maincontent-class: "off-canvas-content"; +$maincontent-class: 'off-canvas-content'; // 25. Orbit // --------- @@ -520,10 +480,8 @@ $progress-radius: $global-radius; // -------------------- $responsive-embed-margin-bottom: rem-calc(16); -$responsive-embed-ratios: ( - default: 4 by 3, - widescreen: 16 by 9 -); +$responsive-embed-ratios: (default: 4 by 3, + widescreen: 16 by 9); // 29. Reveal // ---------- @@ -576,10 +534,8 @@ $table-border: 1px solid transparent; $table-padding: rem-calc(8 10 10); $table-hover-scale: 2%; $table-row-hover: darken($table-background, $table-hover-scale); -$table-row-stripe-hover: darken( - $table-background, - $table-color-scale + $table-hover-scale -); +$table-row-stripe-hover: darken($table-background, + $table-color-scale + $table-hover-scale); $table-is-striped: false; $table-striped-background: smart-scale($table-background, $table-color-scale); $table-stripe: even; diff --git a/app/javascript/dashboard/assets/scss/_layout.scss b/app/javascript/dashboard/assets/scss/_layout.scss index 4b928b888..cdb81c76a 100644 --- a/app/javascript/dashboard/assets/scss/_layout.scss +++ b/app/javascript/dashboard/assets/scss/_layout.scss @@ -35,15 +35,17 @@ body { flex-direction: column; @include margin($zero); @include padding($space-normal); - overflow-y: scroll; + overflow-y: auto; } .content-box { - overflow: scroll; + overflow: auto; @include padding($space-normal); } .back-button { + @include flex; + align-items: center; color: $color-woot; font-size: $font-size-default; font-weight: $font-weight-normal; diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss index c0cf98494..e4efee5ca 100644 --- a/app/javascript/dashboard/assets/scss/_mixins.scss +++ b/app/javascript/dashboard/assets/scss/_mixins.scss @@ -1,3 +1,8 @@ +@import '~widget/assets/scss/mixins'; + +$elegant-shadow-color: rgba(49, 49, 93, 0.15); +$spinner-before-border-color: rgba(255, 255, 255, 0.7); + //borders @mixin border-nil() { border-color: transparent; @@ -77,8 +82,8 @@ &:active, &:hover, &:focus { - box-shadow: none; border-color: transparent; + box-shadow: none; } } @@ -117,7 +122,6 @@ // full height @mixin full-height() { height: 100%; - // COmmenting because unneccessary scroll is apprearing on some pages eg: settings/agents / inboxes } @mixin round-corner() { @@ -125,21 +129,20 @@ } @mixin scroll-on-hover() { - transition: all .4s $ease-in-out-cubic; overflow: hidden; &:hover { - overflow-y: scroll; + overflow-y: auto; } } @mixin horizontal-scroll() { - overflow-y: scroll; + overflow-y: auto; } @mixin elegent-shadow() { - box-shadow: 0 10px 25px 0 rgba(49,49,93,0.15); + box-shadow: 0 10px 25px 0 $elegant-shadow-color; } @mixin elegant-card() { @@ -154,20 +157,20 @@ } } - &:before { - content: ''; + &::before { + animation: spinner .9s linear infinite; + border: 2px solid $spinner-before-border-color; + border-radius: 50%; + border-top-color: lighten($color-woot, 10%); box-sizing: border-box; + content: ''; + height: $space-medium; + left: 50%; + margin-left: -$space-one; + margin-top: -$space-one; position: absolute; top: 50%; - left: 50%; width: $space-medium; - height: $space-medium; - margin-top: -$space-one; - margin-left: -$space-one; - border-radius: 50%; - border: 2px solid rgba(255, 255, 255, 0.7); - border-top-color: lighten($color-woot, 10%); - animation: spinner .9s linear infinite; } } @@ -181,41 +184,41 @@ // .element{ // @include arrow(top, #000, 50px); // } -@mixin arrow($direction, $color, $size){ - display: block; - height: 0; - width: 0; - content: ''; +@mixin arrow($direction, $color, $size) { + display: block; + height: 0; + width: 0; + content: ''; - @if $direction == 'top' { - border-left: $size solid transparent; - border-right: $size solid transparent; - border-bottom: $size solid $color; - } @else if $direction == 'right' { - border-top: $size solid transparent; - border-bottom: $size solid transparent; - border-left: $size solid $color; - } @else if $direction == 'bottom' { - border-top: $size solid $color; - border-right: $size solid transparent; - border-left: $size solid transparent; - } @else if $direction == 'left' { - border-top: $size solid transparent; - border-right: $size solid $color; - border-bottom: $size solid transparent; - } @else if $direction == 'top-left' { - border-top: $size solid $color; - border-right: $size solid transparent; - } @else if $direction == 'top-right' { - border-top: $size solid $color; - border-left: $size solid transparent; - } @else if $direction == 'bottom-left' { - border-bottom: $size solid $color; - border-right: $size solid transparent; - } @else if $direction == 'bottom-right' { - border-bottom: $size solid $color; - border-left: $size solid transparent; - } + @if $direction == 'top' { + border-bottom: $size solid $color; + border-left: $size solid transparent; + border-right: $size solid transparent; + } @else if $direction == 'right' { + border-bottom: $size solid transparent; + border-left: $size solid $color; + border-top: $size solid transparent; + } @else if $direction == 'bottom' { + border-left: $size solid transparent; + border-right: $size solid transparent; + border-top: $size solid $color; + } @else if $direction == 'left' { + border-bottom: $size solid transparent; + border-right: $size solid $color; + border-top: $size solid transparent; + } @else if $direction == 'top-left' { + border-right: $size solid transparent; + border-top: $size solid $color; + } @else if $direction == 'top-right' { + border-left: $size solid transparent; + border-top: $size solid $color; + } @else if $direction == 'bottom-left' { + border-bottom: $size solid $color; + border-right: $size solid transparent; + } @else if $direction == 'bottom-right' { + border-bottom: $size solid $color; + border-left: $size solid transparent; + } } @mixin text-ellipsis { diff --git a/app/javascript/dashboard/assets/scss/_variables.scss b/app/javascript/dashboard/assets/scss/_variables.scss index c9b53d05b..b75d3bf77 100644 --- a/app/javascript/dashboard/assets/scss/_variables.scss +++ b/app/javascript/dashboard/assets/scss/_variables.scss @@ -12,7 +12,7 @@ $font-size-mega: 3.4rem; $font-size-giga: 4.0rem; // spaces -$zero: 0rem; +$zero: 0; $space-micro: 0.2rem; $space-smaller: 0.4rem; $space-small: 0.8rem; @@ -42,16 +42,27 @@ $woot-logo-padding: $space-large $space-two; // Colors $color-woot: #1f93ff; -$color-gray: #6E6F73; -$color-light-gray: #999A9B; -$color-border: #E0E6ED; +$color-gray: #6e6f73; +$color-light-gray: #999a9b; +$color-border: #e0e6ed; $color-border-light: #f0f4f5; -$color-background: #EFF2F7; -$color-background-light: #F9FAFC; -$color-white: #FFF; -$color-body: #3C4858; -$color-heading: #1F2D3D; -$color-extra-light-blue: #F5F7F9; +$color-background: #eff2f7; +$color-background-light: #f9fafc; +$color-white: #fff; +$color-body: #3c4858; +$color-heading: #1f2d3d; +$color-extra-light-blue: #f5f7f9; + +$primary-color: $color-woot; +$secondary-color: #ff5216; +$success-color: #13ce66; +$warning-color: #ffc82c; +$alert-color: #ff4949; + +// Color-palettes + +$color-primary-light: #c7e3ff; +$color-primary-dark: darken($color-woot, 20%); // Thumbnail $thumbnail-radius: 4rem; @@ -81,3 +92,6 @@ $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !d // Ionicons $ionicons-font-path: '~ionicons/fonts'; + +// Transitions +$transition-ease-in: all 0.250s ease-in; diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index c85012e9f..d86e61271 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -25,6 +25,7 @@ @import 'views/settings/inbox'; @import 'views/settings/channel'; +@import 'views/settings/integrations'; @import 'views/signup'; @import 'plugins/multiselect'; diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 5318e3447..6d522dc9b 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -1,32 +1,24 @@ +@mixin label-multiselect-hover { + &::after { + color: $color-primary-dark; + } + + &:hover { + background: $color-background; + + &::after { + color: $color-woot; + } + } +} + .multiselect { margin-bottom: $space-normal; min-height: 38px; - > .multiselect__tags { - @include margin(0); - border: 1px solid $color-border; - min-height: 44px; - padding-top: $zero; - - .multiselect__placeholder { - padding-top: $space-small; - } - - .multiselect__tag { - margin-top: $space-one; - } - - .multiselect__input { - @include ghost-input; - @include padding($zero); - - margin-bottom: $zero; - } - - .multiselect__single { - @include padding($space-one); - - margin-bottom: 0; + &.multiselect--active { + >.multiselect__tags { + border-color: $color-woot; } } @@ -41,4 +33,93 @@ top: 60%; } } + + .multiselect__content .multiselect__option { + font-size: $font-size-small; + font-weight: $font-weight-normal; + + &.multiselect__option--highlight { + font-weight: $font-weight-medium; + } + } +} + +.multiselect>.multiselect__tags { + @include margin(0); + border: 1px solid $color-border; + min-height: 44px; + padding-top: $zero; + + .multiselect__tags-wrap { + display: inline-block; + line-height: 1; + margin-top: $space-smaller; + } + + .multiselect__placeholder { + color: $color-gray; + font-weight: $font-weight-normal; + padding-top: $space-small; + } + + .multiselect__tag { + $vertical-space: $space-smaller + $space-micro; + background: $color-background; + color: $color-heading; + margin-top: $space-smaller; + padding: $vertical-space $space-medium $vertical-space $space-one; + } + + .multiselect__tag-icon { + @include label-multiselect-hover; + line-height: $space-medium + $space-micro; + } + + .multiselect__input { + @include ghost-input; + @include padding($zero); + font-size: $font-size-small; + + margin-bottom: $zero; + } + + .multiselect__single { + @include padding($space-one); + + margin-bottom: 0; + } +} + +.sidebar-labels-wrap { + + &.has-edited, + &:hover { + .multiselect { + cursor: pointer; + } + + .multiselect>.multiselect__tags { + border-color: $color-border; + } + + .multiselect>.multiselect__select { + visibility: visible; + } + } + + .multiselect { + margin-top: $space-small; + + >.multiselect__select { + visibility: hidden; + } + + >.multiselect__tags { + border-color: transparent; + } + + &.multiselect--active>.multiselect__tags { + border-color: $color-woot; + } + } } diff --git a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss index b6865213e..0059816d8 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss @@ -212,7 +212,7 @@ .code { max-height: $space-mega; - overflow: scroll; + overflow: auto; white-space: nowrap; @include padding($space-one); background: $color-background; diff --git a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss new file mode 100644 index 000000000..183fa9a23 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss @@ -0,0 +1,37 @@ +.integrations-wrap { + .integration { + background: $color-white; + border: 2px solid $color-border; + border-radius: $space-slab; + padding: $space-normal; + + .integration--image { + display: flex; + margin-right: $space-normal; + width: 8rem; + + img { + max-width: 8rem; + padding: $space-small; + } + } + + .integration--title { + font-size: $font-size-large; + } + + .integration--description { + padding-right: $space-medium; + } + + .button-wrap { + @include flex; + @include flex-align(center, middle); + margin-bottom: 0; + } + } +} + +.help-wrap { + padding-left: $space-large; +} diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index d60ac27f9..5d3486aec 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -24,9 +24,18 @@ } } - > .icon { + >.icon { font-size: $font-size-default; } + + &.tiny { + font-size: $font-size-mini; + padding: $space-small $space-slab; + } + + &.round { + border-radius: $space-larger; + } } .button--fixed-right-top { diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss index 004a8236e..f4a037886 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss @@ -5,6 +5,7 @@ @include flex; @include flex-align($x: justify, $y: middle); @include border-normal-bottom; + // Resolve Button .button { @include margin(0); @@ -44,6 +45,7 @@ .user--name { @include margin(0); font-size: $font-size-medium; + text-transform: capitalize; } .user--profile__meta { @@ -64,7 +66,7 @@ } .button.resolve--button { - > .icon { + >.icon { padding-right: $space-small; font-size: $font-size-default; } diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss index a4f632085..649b3b31b 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss @@ -2,8 +2,8 @@ @include flex; @include flex-shrink; @include padding($space-normal $zero $zero $space-normal); - position: relative; cursor: pointer; + position: relative; &.active { background: $color-background; @@ -18,63 +18,63 @@ .conversation--user { font-size: $font-size-small; margin-bottom: $zero; + text-transform: capitalize; .label { - position: relative; - top: $space-micro; left: $space-micro; max-width: $space-jumbo; overflow: hidden; + position: relative; text-overflow: ellipsis; + top: $space-micro; white-space: nowrap; } } .conversation--message { - height: $space-medium; - margin: $zero; - font-size: $font-size-small; - line-height: $space-medium; - font-weight: $font-weight-light; - text-overflow: ellipsis; - overflow: hidden; color: $color-body; - width: 27rem; - white-space: nowrap; + font-size: $font-size-small; + font-weight: $font-weight-normal; + height: $space-medium; + line-height: $space-medium; + margin: $zero; max-width: 96%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 27rem; } .conversation--meta { + @include flex; display: block; + flex-direction: column; position: absolute; right: $space-normal; top: $space-normal; - @include flex; - flex-direction: column; .unread { $unread-size: $space-two - $space-micro; - display: none; - height: $unread-size; - min-width: $unread-size; - background: darken($success-color, 3%); - text-align: center; - padding: 0 $space-smaller; - line-height: $unread-size; - color: $color-white; - font-weight: $font-weight-medium; - font-size: $font-size-mini; - margin-left: auto; @include round-corner; + background: darken($success-color, 3%); + color: $color-white; + display: none; + font-size: $font-size-mini; + font-weight: $font-weight-medium; + height: $unread-size; + line-height: $unread-size; + margin-left: auto; margin-top: $space-smaller; + min-width: $unread-size; + padding: 0 $space-smaller; + text-align: center; } .timestamp { - font-size: $font-size-mini; color: $dark-gray; - line-height: $space-normal; - font-weight: $font-weight-normal; font-size: $font-size-micro; + font-weight: $font-weight-normal; + line-height: $space-normal; margin-left: auto; } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 5e5cd87fa..cf625423b 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -1,7 +1,108 @@ +@mixin bubble-with-tyes { + @include padding($space-smaller $space-one); + @include margin($zero); + background: $color-primary-light; + border-radius: $space-small; + color: $color-heading; + font-size: $font-size-small; + position: relative; + + .icon { + bottom: $space-smaller; + position: absolute; + right: $space-small; + } + + .message-text__wrap { + position: relative; + } + + .message-text { + &::after { + content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0'; + display: inline; + } + } + + .audio { + .time { + margin-top: -$space-two; + } + } + + .image { + @include flex; + align-items: flex-end; + justify-content: center; + text-align: center; + + img { + @include padding($space-small); + max-height: 30rem; + max-width: 20rem; + } + + .time { + margin-left: -$space-large; + white-space: nowrap; + } + + .modal-image { + max-height: 80%; + max-width: 80%; + } + } + + .map { + @include flex; + flex-direction: column; + text-align: right; + + img { + @include padding($space-small); + max-height: 30rem; + max-width: 20rem; + } + + .time { + @include padding($space-small); + margin-left: -$space-smaller; + margin-top: -$space-two; + white-space: nowrap; + } + + .locname { + font-weight: $font-weight-medium; + padding: $space-smaller; + } + } +} + .conversations-sidebar { @include flex; flex-direction: column; + .load-more-conversations { + color: $color-woot; + cursor: pointer; + font-size: $font-size-small; + padding: $space-normal; + + &:hover { + background: $color-background; + } + } + + .end-of-list-text { + font-style: italic; + padding: $space-normal; + } + + .conversations-list { + @include flex-weight(1); + @include scroll-on-hover; + } + .chat-list__top { @include flex; @include padding($space-normal $zero $space-small $zero); @@ -28,10 +129,7 @@ } } - .conversations-list { - @include flex-weight(1); - @include scroll-on-hover; - } + .content-box { text-align: center; @@ -47,16 +145,19 @@ @include background-gray; @include margin(0); @include border-normal-left; + .current-chat { @include flex; @include full-height; - flex-direction: column; @include flex-align(center, middle); + flex-direction: column; + div { @include flex; @include full-height; - flex-direction: column; @include flex-align(center, middle); + flex-direction: column; + img { @include margin($space-normal); width: 10rem; @@ -73,21 +174,22 @@ .conv-empty-state { @include flex; @include full-height; - flex-direction: column; @include flex-align(center, middle); + flex-direction: column; } } .conversation-panel { @include flex; @include flex-weight(1); - flex-direction: column; @include margin($zero); + flex-direction: column; // Firefox flexbox fix height: 100%; - overflow-y: scroll; + margin-bottom: $space-small; + overflow-y: auto; - > li { + >li { @include flex; @include flex-shrink; @include margin($zero $zero $space-smaller); @@ -114,6 +216,7 @@ } .bubble { + @include bubble-with-tyes; max-width: 50rem; text-align: left; word-wrap: break-word; @@ -147,7 +250,7 @@ @include flex-align(right, null); .wrap { - margin-right: $space-small; + margin-right: $space-normal; text-align: right; } @@ -205,6 +308,7 @@ @include padding($space-smaller $space-normal); @include flex-align($x: center, $y: null); background: lighten($warning-color, 32%); + border: 1px solid lighten($warning-color, 26%); border-radius: $space-smaller; font-size: $font-size-small; @@ -237,87 +341,6 @@ } } - .bubble { - @include padding($space-smaller $space-one); - @include margin($zero); - background: #c7e3ff; - border-radius: $space-small; - box-shadow: 0 .5px .5px rgba(0, 0, 0, .05); - color: $color-heading; - font-size: $font-size-small; - position: relative; - - .icon { - bottom: $space-smaller; - position: absolute; - right: $space-small; - } - - .message-text__wrap { - position: relative; - } - - .message-text { - &::after { - content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0'; - display: inline; - } - } - - .audio { - .time { - margin-top: -$space-two; - } - } - - .image { - @include flex; - justify-content: center; - align-items: flex-end; - text-align: center; - - img { - @include padding($space-small); - max-height: 30rem; - max-width: 20rem; - } - - .time { - margin-left: -$space-large; - white-space: nowrap; - } - - .modal-image { - max-height: 80%; - max-width: 80%; - } - } - - .map { - @include flex; - flex-direction: column; - text-align: right; - - img { - @include padding($space-small); - max-height: 30rem; - max-width: 20rem; - } - - .time { - @include padding($space-small); - margin-left: -$space-smaller; - margin-top: -$space-two; - white-space: nowrap; - } - - .locname { - font-weight: $font-weight-medium; - padding: $space-smaller; - } - } - } - .time { bottom: -$space-micro; color: $color-gray; diff --git a/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss b/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss index 8fd11ec4d..7a4e2f2ec 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss @@ -23,7 +23,7 @@ @include padding($space-small); box-sizing: border-box; height: 180px; - overflow-y: scroll; + overflow-y: auto; .emoji { border-radius: 4px; diff --git a/app/javascript/dashboard/assets/scss/widgets/_login.scss b/app/javascript/dashboard/assets/scss/widgets/_login.scss index 608823d39..8720c6b57 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_login.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_login.scss @@ -5,7 +5,7 @@ // Outside login wrapper .login { @include full-height; - overflow-y: scroll; + overflow-y: auto; padding-top: $space-larger * 1.2; .login__hero { diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index 5a2cc7b3d..641400b1e 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -40,7 +40,7 @@ background-color: $color-white; border-radius: $space-small; max-height: 100%; - overflow: scroll; + overflow: auto; position: relative; width: 60rem; diff --git a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss index 9330bce24..15dde2966 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss @@ -1,10 +1,10 @@ .reply-box { + @include elegant-card; + border-bottom: 0; margin: $space-normal; margin-top: 0; - border-bottom: 0; - @include elegant-card; - transition: height 2s $ease-in-out-cubic; max-height: $space-jumbo * 2; + transition: height 2s $ease-in-out-cubic; .reply-box__top { @include flex; @@ -12,26 +12,25 @@ @include padding($space-one $space-normal); @include background-white; @include margin(0); - position: relative; border-top-left-radius: $space-small; border-top-right-radius: $space-small; + position: relative; .canned { @include elegant-card; - z-index: 100; - position: absolute; - background: #fff; - width: 24rem; - left: 0; - border-top: $space-small solid $color-white; + background: $color-white; border-bottom: $space-small solid $color-white; - max-height: 14rem; - overflow: scroll; + border-top: $space-small solid $color-white; + left: 0; - .active { - a { - background: $color-woot; - } + max-height: 14rem; + overflow: auto; + position: absolute; + width: 24rem; + z-index: 100; + + .active a { + background: $color-woot; } } @@ -43,30 +42,30 @@ &.is-private { background: lighten($warning-color, 38%); - > input { + >input { background: lighten($warning-color, 38%); } } - > .icon { - font-size: $font-size-medium; + >.icon { color: $medium-gray; - margin-right: $space-small; cursor: pointer; + font-size: $font-size-medium; + margin-right: $space-small; &.active { color: $color-woot; } } - > textarea { + >textarea { @include ghost-input(); @include margin(0); - resize: none; background: transparent; // Override min-height : 50px in foundation // min-height: 1rem; + resize: none; } } @@ -80,48 +79,47 @@ .tabs { border: 0; - padding: 0; flex: 1; + padding: 0; .tabs-title { margin: 0; - transition: background .2s $ease-in-out-cubic; - transition: color .2s $ease-in-out-cubic; + transition: all .2s $ease-in-out-cubic; + transition-property: color, background; a { + font-weight: $font-weight-medium; padding: $space-one $space-two; } - &:first-child { - border-bottom-left-radius: $space-small; + &.is-private.is-active { + background: lighten($warning-color, 38%); - &.is-active { - @include border-light-right; - border-left: 0; - - a { - border-bottom-left-radius: $space-small; - } + a { + border-bottom-color: darken($warning-color, 15%); + color: darken($warning-color, 15%); } } + } - &.is-private { - &.is-active { - background: lighten($warning-color, 38%); + .tabs-title:first-child { + border-bottom-left-radius: $space-small; - a { - border-bottom-color: darken($warning-color, 15%); - color: darken($warning-color, 15%); - } + &.is-active { + @include border-light-right; + border-left: 0; + + a { + border-bottom-left-radius: $space-small; } } } .is-active { @include background-white; - margin-top: -1px; @include border-light-left; @include border-light-right; + margin-top: -1px; } .message-length { @@ -138,11 +136,11 @@ } .send-button { - height: 3.6rem; border-bottom-right-radius: $space-small; - padding-top: $space-small; - padding-right: $space-two; + height: 3.6rem; padding-left: $space-two; + padding-right: $space-two; + padding-top: $space-small; .icon { margin-left: $space-small; diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss index 9f0ac0bd2..ef5e1dea9 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss @@ -1,7 +1,7 @@ .side-menu { i { - min-width: $space-two; margin-right: $space-smaller; + min-width: $space-two; } } @@ -27,6 +27,26 @@ border-radius: $space-smaller; color: $color-gray; font-size: $font-size-default; + font-weight: $font-weight-medium; + } + + .active a { + color: $color-woot; + } + } + + .nested { + a { + font-size: $font-size-small; + margin-bottom: $space-micro; + margin-top: $space-micro; + + >.inbox-icon { + display: inline-block; + margin-right: $space-micro; + min-width: $space-normal; + text-align: center; + } } } } @@ -36,17 +56,17 @@ @include flex; @include space-between-column; @include padding($space-one $space-normal $space-one $space-one); - flex-direction: column; @include border-normal-top; + flex-direction: column; position: relative; .dropdown-pane { @include elegant-card; @include border-light; + display: block; left: 18%; top: -110%; visibility: visible; - display: block; width: 80%; z-index: 999; @@ -79,23 +99,23 @@ font-size: $font-size-medium; margin-top: $space-medium; - > span { + >span { margin-left: auto; } } } -.menu-title + ul > li > a { +.menu-title+ul>li>a { @include padding($space-micro null); color: $medium-gray; line-height: $global-lineheight; } .current-user { - align-items: center; @include flex; - flex-direction: row; + align-items: center; cursor: pointer; + flex-direction: row; .current-user--data { @include flex; @@ -105,7 +125,7 @@ font-size: $font-size-small; font-weight: $font-weight-medium; line-height: 1; - margin-bottom: $zero; + margin-bottom: $space-smaller; margin-left: $space-one; margin-top: $space-micro; } @@ -132,7 +152,7 @@ display: none; margin-right: $space-normal; - @media screen and (max-width: 1200px){ + @media screen and (max-width: 1200px) { display: block; } } @@ -141,7 +161,7 @@ display: block; margin-right: $space-normal; - @media screen and (max-width: 1200px){ + @media screen and (max-width: 1200px) { display: none; } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index bf16258bf..0e76ffde9 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -13,6 +13,7 @@ .tabs-title { a { font-size: $font-size-default; + font-weight: $font-weight-medium; padding-bottom: $space-slab; padding-top: $space-slab; } diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 6f69857f7..83e1766b5 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -3,40 +3,52 @@

- {{ inbox.name || pageTitle }} + {{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }}

- +
-

+

{{ $t('CHAT_LIST.LIST.404') }}

-
- -
- - +
- + +
+ +
+ +
+ {{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }} +
+ +

+ {{ $t('CHAT_LIST.EOF') }} +

+
@@ -59,11 +71,11 @@ export default { ChatFilter, }, mixins: [timeMixin, conversationMixin], - props: ['conversationInbox', 'pageTitle'], + props: ['conversationInbox'], data() { return { - activeAssigneeTab: 0, - activeStatus: 0, + activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME, + activeStatus: wootConstants.STATUS_TYPE.OPEN, }; }, computed: { @@ -78,66 +90,69 @@ export default { convStats: 'getConvTabStats', }), assigneeTabItems() { - return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map((item, index) => ({ - id: index, + return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({ + key: item.KEY, name: item.NAME, - count: this.convStats[item.KEY] || 0, + count: this.convStats[item.COUNT_KEY] || 0, })); }, inbox() { return this.$store.getters['inboxes/getInbox'](this.activeInbox); }, - getToggleStatus() { - if (this.toggleType) { - return 'Open'; - } - return 'Resolved'; + currentPage() { + return this.$store.getters['conversationPage/getCurrentPage']( + this.activeAssigneeTab + ); + }, + hasCurrentPageEndReached() { + return this.$store.getters['conversationPage/getHasEndReached']( + this.activeAssigneeTab + ); + }, + }, + watch: { + conversationInbox() { + this.resetAndFetchData(); }, }, mounted() { - this.$watch('$store.state.route', () => { - if (this.$store.state.route.name !== 'inbox_conversation') { - this.$store.dispatch('emptyAllConversations'); - this.fetchData(); - } - }); - - this.$store.dispatch('emptyAllConversations'); - this.fetchData(); + this.$store.dispatch('setChatFilter', this.activeStatus); + this.resetAndFetchData(); this.$store.dispatch('agents/get'); }, methods: { - fetchData() { - if (this.chatLists.length === 0) { - this.fetchConversations(); - } + resetAndFetchData() { + this.$store.dispatch('conversationPage/reset'); + this.$store.dispatch('emptyAllConversations'); + this.fetchConversations(); }, fetchConversations() { this.$store.dispatch('fetchAllConversations', { inboxId: this.conversationInbox ? this.conversationInbox : undefined, assigneeType: this.activeAssigneeTab, - status: this.activeStatus ? 'resolved' : 'open', + status: this.activeStatus, + page: this.currentPage + 1, }); }, - getDataForTab(index) { - if (this.activeAssigneeTab !== index) { - this.activeAssigneeTab = index; - this.fetchConversations(); + updateAssigneeTab(selectedTab) { + if (this.activeAssigneeTab !== selectedTab) { + this.activeAssigneeTab = selectedTab; + if (!this.currentPage) { + this.fetchConversations(); + } } }, - getDataForStatusTab(index) { + updateStatusType(index) { if (this.activeStatus !== index) { this.activeStatus = index; - this.fetchConversations(); + this.resetAndFetchData(); } }, getChatsForTab() { let copyList = []; - if (this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.MINE) { + if (this.activeAssigneeTab === 'me') { copyList = this.mineChatsList.slice(); - } else if ( - this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.UNASSIGNED - ) { + } else if (this.activeAssigneeTab === 'unassigned') { copyList = this.unAssignedChatsList.slice(); } else { copyList = this.allChatList.slice(); diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue index b3179a641..b7d9489fb 100644 --- a/app/javascript/dashboard/components/Modal.vue +++ b/app/javascript/dashboard/components/Modal.vue @@ -1,6 +1,11 @@