diff --git a/.env.example b/.env.example index 7f40616b5..824a96285 100644 --- a/.env.example +++ b/.env.example @@ -35,7 +35,7 @@ REDIS_SENTINELS= REDIS_SENTINEL_MASTER_NAME= # By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels -# Use the following environment variable to customize passwords for sentinels. +# Use the following environment variable to customize passwords for sentinels. # Use empty string if sentinels are configured with out passwords # REDIS_SENTINEL_PASSWORD= @@ -45,7 +45,7 @@ REDIS_SENTINEL_MASTER_NAME= # REDIS_OPENSSL_VERIFY_MODE=none # Postgres Database config variables -# You can leave POSTGRES_DATABASE blank. The default name of +# You can leave POSTGRES_DATABASE blank. The default name of # the database in the production environment is chatwoot_production # POSTGRES_DATABASE= POSTGRES_HOST=postgres @@ -213,3 +213,12 @@ STRIPE_WEBHOOK_SECRET= # Set to true if you want to upload files to cloud storage using the signed url # Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true. DIRECT_UPLOADS_ENABLED= + +#MS OAUTH creds +AZURE_APP_ID= +AZURE_APP_SECRET= + +## Advanced configurations +## Change these values to fine tune performance +# control the concurrency setting of sidekiq +# SIDEKIQ_CONCURRENCY=10 diff --git a/Gemfile b/Gemfile index 77720f414..1d9c50cb6 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,8 @@ gem 'json_schemer' gem 'rack-attack' # a utility tool for streaming, flexible and safe downloading of remote files gem 'down', '~> 5.0' +# authentication type to fetch and send mail over oauth2.0 +gem 'gmail_xoauth' ##-- for active storage --## gem 'aws-sdk-s3', require: false @@ -160,6 +162,8 @@ group :test do gem 'database_cleaner' # mock http calls gem 'webmock' + # test profiling + gem 'test-prof' end group :development, :test do @@ -186,3 +190,5 @@ group :development, :test do gem 'spring' gem 'spring-watcher-listen' end +# worked with microsoft refresh token +gem 'omniauth-oauth2' diff --git a/Gemfile.lock b/Gemfile.lock index e96ae1b9e..4b805b109 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -249,6 +249,8 @@ GEM gli (2.21.0) globalid (1.0.0) activesupport (>= 5.0) + gmail_xoauth (0.4.2) + oauth (>= 0.3.6) google-apis-core (0.7.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) @@ -333,8 +335,8 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) http-form_data (2.3.0) - httparty (0.20.0) - mime-types (~> 3.0) + httparty (0.21.0) + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) i18n (1.11.0) @@ -437,6 +439,20 @@ GEM nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) oauth (0.5.10) + oauth2 (2.0.9) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + omniauth (2.1.0) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) orm_adapter (0.5.0) os (1.1.4) parallel (1.22.1) @@ -465,6 +481,8 @@ GEM rack (>= 1.0, < 3) rack-cors (1.1.1) rack (>= 2.0.0) + rack-protection (3.0.5) + rack rack-proxy (0.7.2) rack rack-test (2.0.2) @@ -620,6 +638,9 @@ GEM gli hashie websocket-driver + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) spring (2.1.1) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) @@ -635,6 +656,7 @@ GEM statsd-ruby (1.5.0) stripe (6.5.0) telephone_number (1.4.16) + test-prof (1.0.11) thor (1.2.1) tilt (2.0.10) time_diff (0.3.0) @@ -663,6 +685,7 @@ GEM valid_email2 (4.0.3) activemodel (>= 3.2) mail (~> 2.5) + version_gem (1.1.1) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.0) @@ -735,6 +758,7 @@ DEPENDENCIES flag_shih_tzu foreman geocoder + gmail_xoauth google-cloud-dialogflow google-cloud-storage groupdate @@ -756,6 +780,7 @@ DEPENDENCIES maxminddb mock_redis newrelic_rpm + omniauth-oauth2 pg pg_search procore-sift @@ -791,6 +816,7 @@ DEPENDENCIES squasher stripe telephone_number + test-prof time_diff twilio-ruby (~> 5.66) twitty diff --git a/Procfile.dev b/Procfile.dev index f7c4e6a67..94371cbae 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,3 +1,4 @@ backend: bin/rails s -p 3000 frontend: bin/webpack-dev-server -worker: bundle exec sidekiq -C config/sidekiq.yml +# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695 +worker: dotenv bundle exec sidekiq -C config/sidekiq.yml diff --git a/Procfile.test b/Procfile.test index 760852e80..32792c245 100644 --- a/Procfile.test +++ b/Procfile.test @@ -1,3 +1,3 @@ backend: RAILS_ENV=test bin/rails s -p 5050 frontend: bin/webpack-dev-server -worker: RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml +worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml diff --git a/README.md b/README.md index cf417447a..ac11d649d 100644 --- a/README.md +++ b/README.md @@ -119,4 +119,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri -*Chatwoot* © 2017-2022, Chatwoot Inc - Released under the MIT License. +*Chatwoot* © 2017-2023, Chatwoot Inc - Released under the MIT License. diff --git a/app/controllers/api/v1/accounts/integrations/dyte_controller.rb b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb new file mode 100644 index 000000000..c5f795d34 --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb @@ -0,0 +1,48 @@ +class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseController + before_action :fetch_conversation, only: [:create_a_meeting] + before_action :fetch_message, only: [:add_participant_to_meeting] + before_action :authorize_request + + def create_a_meeting + render_response(dyte_processor_service.create_a_meeting(Current.user)) + end + + def add_participant_to_meeting + if @message.content_type != 'integrations' + return render json: { + error: I18n.t('errors.dyte.invalid_message_type') + }, status: :unprocessable_entity + end + + render_response( + dyte_processor_service.add_participant_to_meeting(@message.content_attributes['data']['meeting_id'], Current.user) + ) + end + + private + + def authorize_request + authorize @conversation.inbox, :show? + end + + def render_response(response) + render json: response, status: response[:error].blank? ? :ok : :unprocessable_entity + end + + def dyte_processor_service + Integrations::Dyte::ProcessorService.new(account: Current.account, conversation: @conversation) + end + + def permitted_params + params.permit(:conversation_id, :message_id) + end + + def fetch_conversation + @conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id]) + end + + def fetch_message + @message = Current.account.messages.find(permitted_params[:message_id]) + @conversation = @message.conversation + end +end diff --git a/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb b/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb new file mode 100644 index 000000000..49a4bdcb0 --- /dev/null +++ b/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb @@ -0,0 +1,27 @@ +class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController + include MicrosoftConcern + before_action :check_authorization + + def create + email = params[:authorization][:email] + redirect_url = microsoft_client.auth_code.authorize_url( + { + redirect_uri: "#{base_url}/microsoft/callback", + scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid', + prompt: 'consent' + } + ) + if redirect_url + ::Redis::Alfred.setex(email, Current.account.id, 5.minutes) + render json: { success: true, url: redirect_url } + else + render json: { success: false }, status: :unprocessable_entity + end + end + + private + + def check_authorization + raise Pundit::NotAuthorizedError unless Current.account_user.administrator? + end +end diff --git a/app/controllers/api/v1/widget/integrations/dyte_controller.rb b/app/controllers/api/v1/widget/integrations/dyte_controller.rb new file mode 100644 index 000000000..0661b4a3c --- /dev/null +++ b/app/controllers/api/v1/widget/integrations/dyte_controller.rb @@ -0,0 +1,36 @@ +class Api::V1::Widget::Integrations::DyteController < Api::V1::Widget::BaseController + before_action :set_message + + def add_participant_to_meeting + if @message.content_type != 'integrations' + return render json: { + error: I18n.t('errors.dyte.invalid_message_type') + }, status: :unprocessable_entity + end + + response = dyte_processor_service.add_participant_to_meeting( + @message.content_attributes['data']['meeting_id'], + @conversation.contact + ) + render_response(response) + end + + private + + def render_response(response) + render json: response, status: response[:error].blank? ? :ok : :unprocessable_entity + end + + def dyte_processor_service + Integrations::Dyte::ProcessorService.new(account: @web_widget.inbox.account, conversation: @conversation) + end + + def set_message + @message = @web_widget.inbox.messages.find(permitted_params[:message_id]) + @conversation = @message.conversation + end + + def permitted_params + params.permit(:website_token, :message_id) + end +end diff --git a/app/controllers/concerns/microsoft_concern.rb b/app/controllers/concerns/microsoft_concern.rb new file mode 100644 index 000000000..3aa3e4e81 --- /dev/null +++ b/app/controllers/concerns/microsoft_concern.rb @@ -0,0 +1,22 @@ +module MicrosoftConcern + extend ActiveSupport::Concern + + def microsoft_client + ::OAuth2::Client.new(ENV.fetch('AZURE_APP_ID', nil), ENV.fetch('AZURE_APP_SECRET', nil), + { + site: 'https://login.microsoftonline.com', + authorize_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + token_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + }) + end + + private + + def parsed_body + @parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body) + end + + def base_url + ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 30b78772a..25a981df6 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -55,7 +55,8 @@ class DashboardController < ActionController::Base ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), FACEBOOK_API_VERSION: 'v14.0', - IS_ENTERPRISE: ChatwootApp.enterprise? + IS_ENTERPRISE: ChatwootApp.enterprise?, + AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', '') } end end diff --git a/app/controllers/microsoft/callbacks_controller.rb b/app/controllers/microsoft/callbacks_controller.rb new file mode 100644 index 000000000..5b765f001 --- /dev/null +++ b/app/controllers/microsoft/callbacks_controller.rb @@ -0,0 +1,72 @@ +class Microsoft::CallbacksController < ApplicationController + include MicrosoftConcern + + def show + @response = microsoft_client.auth_code.get_token( + oauth_code, + redirect_uri: "#{base_url}/microsoft/callback" + ) + + inbox = find_or_create_inbox + ::Redis::Alfred.delete(users_data['email']) + redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id) + rescue StandardError => e + ChatwootExceptionTracker.new(e).capture_exception + redirect_to '/' + end + + private + + def oauth_code + params[:code] + end + + def users_data + decoded_token = JWT.decode parsed_body[:id_token], nil, false + decoded_token[0] + end + + def parsed_body + @parsed_body ||= @response.response.parsed + end + + def account_id + ::Redis::Alfred.get(users_data['email']) + end + + def account + @account ||= Account.find(account_id) + end + + def find_or_create_inbox + channel_email = Channel::Email.find_by(email: users_data['email'], account: account) + channel_email ||= create_microsoft_channel_with_inbox + update_microsoft_channel(channel_email) + channel_email.inbox + end + + def create_microsoft_channel_with_inbox + ActiveRecord::Base.transaction do + channel_email = Channel::Email.create!(email: users_data['email'], account: account) + account.inboxes.create!( + account: account, + channel: channel_email, + name: users_data['name'] + ) + channel_email + end + end + + def update_microsoft_channel(channel_email) + channel_email.update!({ + imap_login: users_data['email'], imap_address: 'outlook.office365.com', + imap_port: '993', imap_enabled: true, + provider: 'microsoft', + provider_config: { + access_token: parsed_body['access_token'], + refresh_token: parsed_body['refresh_token'], + expires_on: (Time.current.utc + 1.hour).to_s + } + }) + end +end diff --git a/app/controllers/swagger_controller.rb b/app/controllers/swagger_controller.rb index 23a87b757..697f04ed0 100644 --- a/app/controllers/swagger_controller.rb +++ b/app/controllers/swagger_controller.rb @@ -11,8 +11,8 @@ class SwaggerController < ApplicationController def derived_path params[:path] ||= 'index.html' - path = params[:path] - path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s) + path = Rack::Utils.clean_path_info(params[:path]) + path << ".#{Rack::Utils.clean_path_info(params[:format])}" unless path.ends_with?(params[:format].to_s) path end end diff --git a/app/drops/contact_drop.rb b/app/drops/contact_drop.rb new file mode 100644 index 000000000..7d5a85f0e --- /dev/null +++ b/app/drops/contact_drop.rb @@ -0,0 +1,17 @@ +class ContactDrop < BaseDrop + def email + @obj.try(:email) + end + + def phone_number + @obj.try(:phone_number) + end + + def first_name + @obj.try(:name).try(:split).try(:first) + end + + def last_name + @obj.try(:name).try(:split).try(:last) if @obj.try(:name).try(:split).try(:size) > 1 + end +end diff --git a/app/drops/user_drop.rb b/app/drops/user_drop.rb index f10eee131..0ef91a724 100644 --- a/app/drops/user_drop.rb +++ b/app/drops/user_drop.rb @@ -2,4 +2,12 @@ class UserDrop < BaseDrop def available_name @obj.try(:available_name) end + + def first_name + @obj.try(:name).try(:split).try(:first) + end + + def last_name + @obj.try(:name).try(:split).try(:last) if @obj.try(:name).try(:split).try(:size) > 1 + end end diff --git a/app/javascript/dashboard/api/channel/microsoftClient.js b/app/javascript/dashboard/api/channel/microsoftClient.js new file mode 100644 index 000000000..2d43fb132 --- /dev/null +++ b/app/javascript/dashboard/api/channel/microsoftClient.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class MicrosoftClient extends ApiClient { + constructor() { + super('microsoft', { accountScoped: true }); + } + + generateAuthorization(payload) { + return axios.post(`${this.url}/authorization`, payload); + } +} + +export default new MicrosoftClient(); diff --git a/app/javascript/dashboard/api/integrations/dyte.js b/app/javascript/dashboard/api/integrations/dyte.js new file mode 100644 index 000000000..e22a5893a --- /dev/null +++ b/app/javascript/dashboard/api/integrations/dyte.js @@ -0,0 +1,23 @@ +/* global axios */ + +import ApiClient from '../ApiClient'; + +class DyteAPI extends ApiClient { + constructor() { + super('integrations/dyte', { accountScoped: true }); + } + + createAMeeting(conversationId) { + return axios.post(`${this.url}/create_a_meeting`, { + conversation_id: conversationId, + }); + } + + addParticipantToMeeting(messageId) { + return axios.post(`${this.url}/add_participant_to_meeting`, { + message_id: messageId, + }); + } +} + +export default new DyteAPI(); diff --git a/app/javascript/dashboard/api/specs/integrations/dyte.spec.js b/app/javascript/dashboard/api/specs/integrations/dyte.spec.js new file mode 100644 index 000000000..c89c2106e --- /dev/null +++ b/app/javascript/dashboard/api/specs/integrations/dyte.spec.js @@ -0,0 +1,35 @@ +import DyteAPIClient from '../../integrations/dyte'; +import ApiClient from '../../ApiClient'; +import describeWithAPIMock from '../apiSpecHelper'; + +describe('#accountAPI', () => { + it('creates correct instance', () => { + expect(DyteAPIClient).toBeInstanceOf(ApiClient); + expect(DyteAPIClient).toHaveProperty('createAMeeting'); + expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting'); + }); + + describeWithAPIMock('createAMeeting', context => { + it('creates a valid request', () => { + DyteAPIClient.createAMeeting(1); + expect(context.axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/dyte/create_a_meeting', + { + conversation_id: 1, + } + ); + }); + }); + + describeWithAPIMock('addParticipantToMeeting', context => { + it('creates a valid request', () => { + DyteAPIClient.addParticipantToMeeting(1); + expect(context.axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/dyte/add_participant_to_meeting', + { + message_id: 1, + } + ); + }); + }); +}); diff --git a/app/javascript/dashboard/assets/images/channels/facebook.png b/app/javascript/dashboard/assets/images/channels/facebook.png deleted file mode 100755 index 26333d70c..000000000 Binary files a/app/javascript/dashboard/assets/images/channels/facebook.png and /dev/null differ diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index 414cb2539..28defb4fd 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -75,7 +75,7 @@ Arial, sans-serif; $body-antialiased: true; $global-margin: $space-small; -$global-padding: $space-micro; +$global-padding: $space-small; $global-weight-normal: normal; $global-weight-bold: bold; $global-radius: 0; diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 675771715..6041b286a 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -37,7 +37,6 @@ @include foundation-prototype-sizing; @include foundation-prototype-spacing; - @import 'typography'; @import 'layout'; @import 'animations'; @@ -61,7 +60,6 @@ @import 'widgets/woot-tables'; @import 'views/settings/inbox'; -@import 'views/settings/channel'; @import 'views/settings/integrations'; @import 'plugins/multiselect'; diff --git a/app/javascript/dashboard/assets/scss/views/settings/channel.scss b/app/javascript/dashboard/assets/scss/views/settings/channel.scss deleted file mode 100644 index 5782744d3..000000000 --- a/app/javascript/dashboard/assets/scss/views/settings/channel.scss +++ /dev/null @@ -1,52 +0,0 @@ -$channel-hover-color: rgba(0, 0, 0, 0.1); - -.channels { - margin-top: $space-medium; - - .inactive { - filter: grayscale(100%); - } - - .channel { - @include flex; - @include background-white; - @include border-light; - - cursor: pointer; - flex-direction: column; - margin: -1px; - padding: $space-normal $zero; - transition: all 0.2s ease-in; - - &:last-child { - @include border-light; - } - - &:hover { - border: 1px solid $primary-color; - box-shadow: 0 2px 8px $channel-hover-color; - z-index: 999; - } - - &.disabled { - opacity: 0.6; - } - - img { - margin: $space-normal auto; - width: 50%; - } - - .channel__title { - color: $color-body; - font-size: var(--font-size-default); - text-align: center; - text-transform: capitalize; - } - - p { - color: $medium-gray; - width: 100%; - } - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss index c0d6b1bb6..0ed06c914 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss @@ -42,7 +42,6 @@ $resolve-button-width: 13.2rem; margin-right: var(--space-normal); min-width: 0; - .user--profile__meta { align-items: flex-start; display: flex; @@ -54,13 +53,17 @@ $resolve-button-width: 13.2rem; } } - .header-actions-wrap { align-items: center; display: flex; flex-direction: row; flex-grow: 1; justify-content: flex-end; + margin-top: var(--space-small); + + @include breakpoint(medium up) { + margin-top: 0; + } &.has-open-sidebar { justify-content: flex-end; diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index b67932e6d..d46f5d5f8 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -81,8 +81,12 @@ } .conversations-list { - @include scroll-on-hover; + overflow-y: auto; flex: 1 1; + + @include breakpoint(large up) { + @include scroll-on-hover; + } } .chat-list__top { @@ -324,6 +328,7 @@ var(--space-one); .is-text { + align-items: center; display: inline-flex; text-align: start; diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index 543a60797..c363d4220 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -82,8 +82,8 @@ @include flex-align($x: flex-end, $y: middle); padding: $space-small $zero; - button { - font-size: $font-size-small; + .button { + margin-left: var(--space-small); } &.justify-content-end { diff --git a/app/javascript/dashboard/components/ChannelSelector.vue b/app/javascript/dashboard/components/ChannelSelector.vue new file mode 100644 index 000000000..f64000fb5 --- /dev/null +++ b/app/javascript/dashboard/components/ChannelSelector.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index a957ff78b..7d4a9239e 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -794,30 +794,15 @@ export default { .conversations-list-wrap { flex-shrink: 0; - width: 34rem; + flex-basis: clamp(32rem, 4vw + 34rem, 44rem); overflow: hidden; - @include breakpoint(large up) { - width: 36rem; - } - @include breakpoint(xlarge up) { - width: 35rem; - } - @include breakpoint(xxlarge up) { - width: 38rem; - } - @include breakpoint(xxxlarge up) { - flex-basis: 46rem; - } &.hide { display: none; } &.list--full-width { - width: 100%; - @include breakpoint(xxxlarge up) { - flex-basis: 100%; - } + flex-basis: 100%; } } .filter--actions { diff --git a/app/javascript/dashboard/components/ModalHeader.vue b/app/javascript/dashboard/components/ModalHeader.vue index b60f5d851..18ff7e785 100644 --- a/app/javascript/dashboard/components/ModalHeader.vue +++ b/app/javascript/dashboard/components/ModalHeader.vue @@ -39,6 +39,8 @@ export default { diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index 23388f617..5ebee6b40 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -1,56 +1,15 @@ diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index b6614dcc0..ef3a58979 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -15,29 +15,25 @@ diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index bd67011e5..6922389b0 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -87,6 +87,10 @@ :title="'Whatsapp Templates'" @click="$emit('selectWhatsappTemplate')" /> +
-
- - -
-

- {{ currentContact.name }} - -

-
- - - {{ snoozedDisplayText }} - - - {{ contactPanelToggleText }} - +
+
+ + +
+

+ {{ currentContact.name }} + +

+
+ + + {{ snoozedDisplayText }} + + + {{ contactPanelToggleText }} + +
-
-
- +
+ +
@@ -165,14 +166,27 @@ export default { diff --git a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue index 4613d8357..bf26f3b90 100644 --- a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue +++ b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue @@ -70,7 +70,9 @@ export default { .mention--box { background: var(--white); border-bottom: var(--space-small) solid var(--white); + border-radius: var(--border-radius-normal); border-top: 1px solid var(--color-border); + box-shadow: var(--shadow-medium); left: 0; max-height: 14rem; overflow: auto; diff --git a/app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue b/app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue index 18d8783ee..5d968bfaa 100644 --- a/app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue +++ b/app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue @@ -1,23 +1,14 @@