diff --git a/.env.example b/.env.example index 177cb7fc8..26b1487ae 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,9 @@ FORCE_SSL=false ENABLE_ACCOUNT_SIGNUP=false # Redis config +# specify the configs via single URL or individual variables +# ref: https://www.iana.org/assignments/uri-schemes/prov/redis +# You can also use the following format for the URL: redis://:password@host:port/db_number REDIS_URL=redis://redis:6379 # If you are using docker-compose, set this variable's value to be any string, # which will be the password for the redis service running inside the docker-compose @@ -180,8 +183,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38: ## Sentry # SENTRY_DSN= -## LogRocket -# LOG_ROCKET_PROJECT_ID=xxxxx/some-project # MICROSOFT CLARITY # MS_CLARITY_TOKEN=xxxxxxxxx @@ -251,9 +252,6 @@ AZURE_APP_SECRET= ## OpenAI key # OPENAI_API_KEY= -# Sentiment analysis model file path -SENTIMENT_FILE_PATH= - # Housekeeping/Performance related configurations # Set to true if you want to remove stale contact inboxes # contact_inboxes with no conversation older than 90 days will be removed diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1015fe997..499e5c120 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,7 +1,2 @@ -## All javascript files should be reviewed by pranav before merging -*.js @pranavrajs -*.vue @pranavrajs - - ## All enterprise related files should be reviewed by sojan before merging /enterprise/* @sojan-official diff --git a/.github/workflows/deploy_check.yml b/.github/workflows/deploy_check.yml new file mode 100644 index 000000000..7fda2b1a4 --- /dev/null +++ b/.github/workflows/deploy_check.yml @@ -0,0 +1,45 @@ +## github action to check deployment success +## curl the deployment url and check for 200 status +## deployment url will be of the form chatwoot-pr-.herokuapp.com +name: Deploy Check + +on: + pull_request: + +jobs: + deployment_check: + name: Check Deployment + runs-on: ubuntu-latest + steps: + - name: Install jq + run: sudo apt-get install -y jq + - name: Print Deployment URL + run: echo "https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com" + - name: Check Deployment Status + run: | + max_attempts=10 + attempt=1 + status_code=0 + echo "Waiting for review app to be deployed/redeployed, trying in 10 minutes..." + sleep 600 + while [ $attempt -le $max_attempts ]; do + response=$(curl -s -o /dev/null -w "%{http_code}" https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api) + status_code=$(echo $response | head -n 1) + if [ $status_code -eq 200 ]; then + body=$(curl -s https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api) + if echo "$body" | jq -e '.version and .timestamp and .queue_services == "ok" and .data_services == "ok"' > /dev/null; then + echo "Deployment successful" + exit 0 + else + echo "Deployment status unknown, retrying in 3 minutes..." + sleep 180 + fi + else + echo "Waiting for review app to be ready, retrying in 3 minutes..." + sleep 180 + attempt=$((attempt + 1)) + fi + done + echo "Deployment failed after $max_attempts attempts" + exit 1 + fi diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml index 77b96c48a..c594ff404 100644 --- a/.github/workflows/run_response_bot_spec.yml +++ b/.github/workflows/run_response_bot_spec.yml @@ -73,6 +73,7 @@ jobs: spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \ spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \ spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \ + spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \ --profile=10 \ --format documentation diff --git a/.rubocop.yml b/.rubocop.yml index 5add6e26a..1cdfbc713 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,6 +2,8 @@ require: - rubocop-performance - rubocop-rails - rubocop-rspec + - ./rubocop/use_from_email.rb + - ./rubocop/custom_cop_location.rb Layout/LineLength: Max: 150 @@ -140,6 +142,16 @@ RSpec/MultipleExpectations: RSpec/MultipleMemoizedHelpers: Max: 14 +# custom rules +UseFromEmail: + Enabled: true + Exclude: + - 'app/models/user.rb' + - 'app/models/contact.rb' + +CustomCopLocation: + Enabled: true + AllCops: NewCops: enable Exclude: diff --git a/Gemfile b/Gemfile index a21af8c01..1b2ed90b9 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,8 @@ source 'https://rubygems.org' ruby '3.2.2' ##-- base gems for rails --## -gem 'rack-cors', require: 'rack/cors' -gem 'rails', '~> 7.0.8.0' +gem 'rack-cors', '2.0.0', require: 'rack/cors' +gem 'rails', '~> 7.0.8.1' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', require: false @@ -69,15 +69,15 @@ gem 'webpacker' gem 'barnes' ##--- gems for authentication & authorization ---## -gem 'devise', '>= 4.9.3' +gem 'devise', '>= 4.9.4' gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot' -gem 'devise_token_auth' +gem 'devise_token_auth', '>= 1.2.3' # authorization gem 'jwt' gem 'pundit' # super admin gem 'administrate', '>= 0.20.1' -gem 'administrate-field-active_storage', '>= 1.0.1' +gem 'administrate-field-active_storage', '>= 1.0.3' gem 'administrate-field-belongs_to_search', '>= 0.9.0' ##--- gems for pubsub service ---## @@ -113,10 +113,10 @@ gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'scout_apm', require: false gem 'sentry-rails', '>= 5.14.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.14.0', require: false +gem 'sentry-sidekiq', '>= 5.15.0', require: false ##-- background job processing --## -gem 'sidekiq', '>= 7.2.1' +gem 'sidekiq', '>= 7.2.4' # We want cron jobs gem 'sidekiq-cron', '>= 1.12.0' @@ -165,8 +165,8 @@ gem 'audited', '~> 5.4', '>= 5.4.1' # need for google auth gem 'omniauth', '>= 2.1.2' -gem 'omniauth-google-oauth2' -gem 'omniauth-rails_csrf_protection', '~> 1.0' +gem 'omniauth-google-oauth2', '>= 1.1.2' +gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2' ## Gems for reponse bot # adds cosine similarity to postgres using vector extension @@ -175,9 +175,6 @@ gem 'pgvector' # Convert Website HTML to Markdown gem 'reverse_markdown' -# Sentiment analysis -gem 'informers' - ### Gems required only in specific deployment environments ### ############################################################## @@ -203,7 +200,7 @@ group :development do gem 'rack-mini-profiler', '>= 3.2.0', require: false gem 'stackprof' # Should install the associated chrome extension to view query logs - gem 'meta_request' + gem 'meta_request', '>= 0.8.0' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 52a475fa7..a226a3c3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,70 +33,70 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + actioncable (7.0.8.3) + actionpack (= 7.0.8.3) + activesupport (= 7.0.8.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + actionmailbox (7.0.8.3) + actionpack (= 7.0.8.3) + activejob (= 7.0.8.3) + activerecord (= 7.0.8.3) + activestorage (= 7.0.8.3) + activesupport (= 7.0.8.3) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8) - actionpack (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activesupport (= 7.0.8) + actionmailer (7.0.8.3) + actionpack (= 7.0.8.3) + actionview (= 7.0.8.3) + activejob (= 7.0.8.3) + activesupport (= 7.0.8.3) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8) - actionview (= 7.0.8) - activesupport (= 7.0.8) + actionpack (7.0.8.3) + actionview (= 7.0.8.3) + activesupport (= 7.0.8.3) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8) - actionpack (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + actiontext (7.0.8.3) + actionpack (= 7.0.8.3) + activerecord (= 7.0.8.3) + activestorage (= 7.0.8.3) + activesupport (= 7.0.8.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8) - activesupport (= 7.0.8) + actionview (7.0.8.3) + activesupport (= 7.0.8.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (7.0.8) - activesupport (= 7.0.8) + activejob (7.0.8.3) + activesupport (= 7.0.8.3) globalid (>= 0.3.6) - activemodel (7.0.8) - activesupport (= 7.0.8) - activerecord (7.0.8) - activemodel (= 7.0.8) - activesupport (= 7.0.8) + activemodel (7.0.8.3) + activesupport (= 7.0.8.3) + activerecord (7.0.8.3) + activemodel (= 7.0.8.3) + activesupport (= 7.0.8.3) activerecord-import (1.4.1) activerecord (>= 4.2) - activestorage (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activesupport (= 7.0.8) + activestorage (7.0.8.3) + actionpack (= 7.0.8.3) + activejob (= 7.0.8.3) + activerecord (= 7.0.8.3) + activesupport (= 7.0.8.3) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8) + activesupport (7.0.8.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -113,7 +113,7 @@ GEM kaminari (~> 1.2.2) sassc-rails (~> 2.1) selectize-rails (~> 0.6) - administrate-field-active_storage (1.0.1) + administrate-field-active_storage (1.0.3) administrate (>= 0.2.2) rails (>= 7.0) administrate-field-belongs_to_search (0.9.0) @@ -149,9 +149,9 @@ GEM multi_json (~> 1) statsd-ruby (~> 1.1) base64 (0.1.1) - bcrypt (3.1.19) + bcrypt (3.1.20) + bigdecimal (3.1.8) bindex (0.8.1) - blingfire (0.1.8) bootsnap (1.16.0) msgpack (~> 1.2) brakeman (5.4.1) @@ -194,16 +194,16 @@ GEM irb (>= 1.5.0) reline (>= 0.3.1) declarative (0.0.20) - devise (4.9.3) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise_token_auth (1.2.1) + devise_token_auth (1.2.3) bcrypt (~> 3.0) devise (> 3.5.2, < 5) - rails (>= 4.2.0, < 7.1) + rails (>= 4.2.0, < 7.2) diff-lcs (1.5.0) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) @@ -374,10 +374,6 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - informers (0.2.0) - blingfire (>= 0.1.7) - numo-narray - onnxruntime (>= 0.5.1) io-console (0.6.0) irb (1.7.2) reline (>= 0.3.6) @@ -451,12 +447,12 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) maxminddb (0.1.22) memoist (0.16.2) - meta_request (0.7.4) + meta_request (0.8.2) rack-contrib (>= 1.1, < 3) - railties (>= 3.0.0, < 7.1) + railties (>= 3.0.0, < 8) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) @@ -500,7 +496,6 @@ GEM racc (~> 1.4) nokogiri (1.16.2-x86_64-linux) racc (~> 1.4) - numo-narray (0.9.2.1) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) snaky_hash (~> 2.0) @@ -518,25 +513,17 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-google-oauth2 (1.1.1) + omniauth-google-oauth2 (1.1.2) jwt (>= 2.0) - oauth2 (~> 2.0.6) + oauth2 (~> 2.0) omniauth (~> 2.0) - omniauth-oauth2 (~> 1.8.0) + omniauth-oauth2 (~> 1.8) omniauth-oauth2 (1.8.0) oauth2 (>= 1.4, < 3) omniauth (~> 2.0) - omniauth-rails_csrf_protection (1.0.1) + omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - onnxruntime (0.7.6) - ffi - onnxruntime (0.7.6-arm64-darwin) - ffi - onnxruntime (0.7.6-x86_64-darwin) - ffi - onnxruntime (0.7.6-x86_64-linux) - ffi openssl (3.1.0) orm_adapter (0.5.0) os (1.1.4) @@ -567,7 +554,7 @@ GEM rack (>= 1.0, < 4) rack-contrib (2.4.0) rack (< 4) - rack-cors (2.0.1) + rack-cors (2.0.0) rack (>= 2.0.0) rack-mini-profiler (3.2.0) rack (>= 1.2.0) @@ -578,20 +565,20 @@ GEM rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rails (7.0.8) - actioncable (= 7.0.8) - actionmailbox (= 7.0.8) - actionmailer (= 7.0.8) - actionpack (= 7.0.8) - actiontext (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activemodel (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rails (7.0.8.3) + actioncable (= 7.0.8.3) + actionmailbox (= 7.0.8.3) + actionmailer (= 7.0.8.3) + actionpack (= 7.0.8.3) + actiontext (= 7.0.8.3) + actionview (= 7.0.8.3) + activejob (= 7.0.8.3) + activemodel (= 7.0.8.3) + activerecord (= 7.0.8.3) + activestorage (= 7.0.8.3) + activesupport (= 7.0.8.3) bundler (>= 1.15.0) - railties (= 7.0.8) + railties (= 7.0.8.3) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -599,9 +586,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + railties (7.0.8.3) + actionpack (= 7.0.8.3) + activesupport (= 7.0.8.3) method_source rake (>= 12.2) thor (~> 1.0) @@ -712,18 +699,19 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.14.0) + sentry-rails (5.17.3) railties (>= 5.0) - sentry-ruby (~> 5.14.0) - sentry-ruby (5.14.0) + sentry-ruby (~> 5.17.3) + sentry-ruby (5.17.3) + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.14.0) - sentry-ruby (~> 5.14.0) + sentry-sidekiq (5.17.3) + sentry-ruby (~> 5.17.3) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.2.1) + sidekiq (7.2.4) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) @@ -844,7 +832,7 @@ DEPENDENCIES activerecord-import acts-as-taggable-on administrate (>= 0.20.1) - administrate-field-active_storage (>= 1.0.1) + administrate-field-active_storage (>= 1.0.3) administrate-field-belongs_to_search (>= 0.9.0) annotate attr_extras @@ -865,9 +853,9 @@ DEPENDENCIES database_cleaner ddtrace debug (~> 1.8) - devise (>= 4.9.3) + devise (>= 4.9.4) devise-secure_password! - devise_token_auth + devise_token_auth (>= 1.2.3) dotenv-rails down elastic-apm @@ -890,7 +878,6 @@ DEPENDENCIES hashie html2text! image_processing - informers jbuilder json_refs json_schemer @@ -903,16 +890,16 @@ DEPENDENCIES listen lograge (~> 0.14.0) maxminddb - meta_request + meta_request (>= 0.8.0) mock_redis neighbor net-smtp (~> 0.3.4) newrelic-sidekiq-metrics (>= 1.6.2) newrelic_rpm omniauth (>= 2.1.2) - omniauth-google-oauth2 + omniauth-google-oauth2 (>= 1.1.2) omniauth-oauth2 - omniauth-rails_csrf_protection (~> 1.0) + omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2) pg pg_search pgvector @@ -921,10 +908,10 @@ DEPENDENCIES puma pundit rack-attack (>= 6.7.0) - rack-cors + rack-cors (= 2.0.0) rack-mini-profiler (>= 3.2.0) rack-timeout - rails (~> 7.0.8.0) + rails (~> 7.0.8.1) redis redis-namespace responders (>= 3.1.1) @@ -941,9 +928,9 @@ DEPENDENCIES seed_dump sentry-rails (>= 5.14.0) sentry-ruby - sentry-sidekiq (>= 5.14.0) + sentry-sidekiq (>= 5.15.0) shoulda-matchers - sidekiq (>= 7.2.1) + sidekiq (>= 7.2.4) sidekiq-cron (>= 1.12.0) simplecov (= 0.17.1) slack-ruby-client (~> 2.2.0) diff --git a/Makefile b/Makefile index 16eb80718..7f2680a2d 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,9 @@ db_migrate: db_seed: RAILS_ENV=$(RAILS_ENV) bundle exec rails db:seed +db_reset: + RAILS_ENV=$(RAILS_ENV) bundle exec rails db:reset + db: RAILS_ENV=$(RAILS_ENV) bundle exec rails db:chatwoot_prepare @@ -49,4 +52,4 @@ debug_worker: docker: docker build -t $(APP_NAME) -f ./docker/Dockerfile . -.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker +.PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run debug debug_worker diff --git a/README.md b/README.md index 17d4e7f5e..152660fb7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale Commits-per-month Discord - Huntr uptime response time Artifact HUB diff --git a/app.json b/app.json index a89f2a08a..d50040814 100644 --- a/app.json +++ b/app.json @@ -55,7 +55,7 @@ "plan": "heroku-redis:mini" }, { - "plan": "heroku-postgresql:mini" + "plan": "heroku-postgresql:essential-0" } ], "stack": "heroku-20", @@ -72,6 +72,11 @@ "scripts": { "test": "bundle exec rake test" } + }, + "review": { + "scripts": { + "postdeploy": "bundle exec rails db:seed" + } } } } diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 6b2f37433..a88d3535b 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -59,7 +59,7 @@ class ContactIdentifyAction def existing_email_contact return if params[:email].blank? - @existing_email_contact ||= account.contacts.find_by(email: params[:email]) + @existing_email_contact ||= account.contacts.from_email(params[:email]) end def existing_phone_number_contact diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb index 6ea68821d..54f478920 100644 --- a/app/builders/agent_builder.rb +++ b/app/builders/agent_builder.rb @@ -16,7 +16,6 @@ class AgentBuilder def perform ActiveRecord::Base.transaction do @user = find_or_create_user - send_confirmation_if_required create_account_user end @user @@ -27,18 +26,13 @@ class AgentBuilder # Finds a user by email or creates a new one with a temporary password. # @return [User] the found or created user. def find_or_create_user - user = User.find_by(email: email) + user = User.from_email(email) return user if user temp_password = "1!aA#{SecureRandom.alphanumeric(12)}" User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password) end - # Sends confirmation instructions if the user is persisted and not confirmed. - def send_confirmation_if_required - @user.send_confirmation_instructions if user_needs_confirmation? - end - # Checks if the user needs confirmation. # @return [Boolean] true if the user is persisted and not confirmed, false otherwise. def user_needs_confirmation? diff --git a/app/builders/contact_inbox_with_contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb index d97f64cfe..2f30093db 100644 --- a/app/builders/contact_inbox_with_contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -19,9 +19,9 @@ class ContactInboxWithContactBuilder ActiveRecord::Base.transaction(requires_new: true) do build_contact_with_contact_inbox - update_contact_avatar(@contact) unless @contact.avatar.attached? - @contact_inbox end + update_contact_avatar(@contact) unless @contact.avatar.attached? + @contact_inbox end private @@ -75,7 +75,7 @@ class ContactInboxWithContactBuilder def find_contact_by_email(email) return if email.blank? - account.contacts.find_by(email: email.downcase) + account.contacts.from_email(email) end def find_contact_by_phone_number(phone_number) diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index fec298bce..2c55922f6 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -53,7 +53,23 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder end def conversation - @conversation ||= Conversation.find_by(conversation_params) || build_conversation + @conversation ||= set_conversation_based_on_inbox_config + end + + def set_conversation_based_on_inbox_config + if @inbox.lock_to_single_conversation + Conversation.where(conversation_params).order(created_at: :desc).first || build_conversation + else + find_or_build_for_multiple_conversations + end + end + + def find_or_build_for_multiple_conversations + # If lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved + last_conversation = Conversation.where(conversation_params).where.not(status: :resolved).order(created_at: :desc).first + return build_conversation if last_conversation.nil? + + last_conversation end def build_conversation diff --git a/app/builders/messages/instagram/message_builder.rb b/app/builders/messages/instagram/message_builder.rb index e9debb767..7f1b9cab2 100644 --- a/app/builders/messages/instagram/message_builder.rb +++ b/app/builders/messages/instagram/message_builder.rb @@ -69,9 +69,28 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder end def conversation - @conversation ||= Conversation.where(conversation_params).find_by( - "additional_attributes ->> 'type' = 'instagram_direct_message'" - ) || build_conversation + @conversation ||= set_conversation_based_on_inbox_config + end + + def instagram_direct_message_conversation + Conversation.where(conversation_params) + .where("additional_attributes ->> 'type' = 'instagram_direct_message'") + end + + def set_conversation_based_on_inbox_config + if @inbox.lock_to_single_conversation + instagram_direct_message_conversation.order(created_at: :desc).first || build_conversation + else + find_or_build_for_multiple_conversations + end + end + + def find_or_build_for_multiple_conversations + last_conversation = instagram_direct_message_conversation.where.not(status: :resolved).order(created_at: :desc).first + + return build_conversation if last_conversation.nil? + + last_conversation end def message_content diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 4886ad886..fb986d335 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -78,6 +78,8 @@ class V2::ReportBuilder avg_first_response_time avg_resolution_time reply_time resolutions_count + bot_resolutions_count + bot_handoffs_count reply_time].include?(params[:metric]) end diff --git a/app/builders/v2/reports/bot_metrics_builder.rb b/app/builders/v2/reports/bot_metrics_builder.rb new file mode 100644 index 000000000..c46daf978 --- /dev/null +++ b/app/builders/v2/reports/bot_metrics_builder.rb @@ -0,0 +1,54 @@ +class V2::Reports::BotMetricsBuilder + include DateRangeHelper + attr_reader :account, :params + + def initialize(account, params) + @account = account + @params = params + end + + def metrics + { + conversation_count: bot_conversations.count, + message_count: bot_messages.count, + resolution_rate: bot_resolution_rate.to_i, + handoff_rate: bot_handoff_rate.to_i + } + end + + private + + def bot_activated_inbox_ids + @bot_activated_inbox_ids ||= account.inboxes.filter(&:active_bot?).map(&:id) + end + + def bot_conversations + @bot_conversations ||= account.conversations.where(inbox_id: bot_activated_inbox_ids).where(created_at: range) + end + + def bot_messages + @bot_messages ||= account.messages.outgoing.where(conversation_id: bot_conversations.ids).where(created_at: range) + end + + def bot_resolutions_count + account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved, + created_at: range).distinct.count + end + + def bot_handoffs_count + account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff, + created_at: range).distinct.count + end + + def bot_resolution_rate + return 0 if bot_conversations.count.zero? + + bot_resolutions_count.to_f / bot_conversations.count * 100 + end + + def bot_handoff_rate + return 0 if bot_conversations.count.zero? + + bot_handoffs_count.to_f / bot_conversations.count * 100 + end +end diff --git a/app/builders/v2/reports/conversations/base_report_builder.rb b/app/builders/v2/reports/conversations/base_report_builder.rb new file mode 100644 index 000000000..a7961b0d6 --- /dev/null +++ b/app/builders/v2/reports/conversations/base_report_builder.rb @@ -0,0 +1,30 @@ +class V2::Reports::Conversations::BaseReportBuilder + pattr_initialize :account, :params + + private + + AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze + COUNT_METRICS = %w[ + conversations_count + incoming_messages_count + outgoing_messages_count + resolutions_count + bot_resolutions_count + bot_handoffs_count + ].freeze + + def builder_class(metric) + case metric + when *AVG_METRICS + V2::Reports::Timeseries::AverageReportBuilder + when *COUNT_METRICS + V2::Reports::Timeseries::CountReportBuilder + end + end + + def log_invalid_metric + Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}" + + {} + end +end diff --git a/app/builders/v2/reports/conversations/metric_builder.rb b/app/builders/v2/reports/conversations/metric_builder.rb new file mode 100644 index 000000000..6635fb186 --- /dev/null +++ b/app/builders/v2/reports/conversations/metric_builder.rb @@ -0,0 +1,30 @@ +class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder + def summary + { + conversations_count: count('conversations_count'), + incoming_messages_count: count('incoming_messages_count'), + outgoing_messages_count: count('outgoing_messages_count'), + avg_first_response_time: count('avg_first_response_time'), + avg_resolution_time: count('avg_resolution_time'), + resolutions_count: count('resolutions_count'), + reply_time: count('reply_time') + } + end + + def bot_summary + { + bot_resolutions_count: count('bot_resolutions_count'), + bot_handoffs_count: count('bot_handoffs_count') + } + end + + private + + def count(metric) + builder_class(metric).new(account, builder_params(metric)).aggregate_value + end + + def builder_params(metric) + params.merge({ metric: metric }) + end +end diff --git a/app/builders/v2/reports/conversations/report_builder.rb b/app/builders/v2/reports/conversations/report_builder.rb new file mode 100644 index 000000000..8f992d4c6 --- /dev/null +++ b/app/builders/v2/reports/conversations/report_builder.rb @@ -0,0 +1,21 @@ +class V2::Reports::Conversations::ReportBuilder < V2::Reports::Conversations::BaseReportBuilder + def timeseries + perform_action(:timeseries) + end + + def aggregate_value + perform_action(:aggregate_value) + end + + private + + def perform_action(method_name) + return builder.new(account, params).public_send(method_name) if builder.present? + + log_invalid_metric + end + + def builder + builder_class(params[:metric]) + end +end diff --git a/app/builders/v2/reports/timeseries/base_timeseries_builder.rb b/app/builders/v2/reports/timeseries/base_timeseries_builder.rb index c2ee5676d..50699417d 100644 --- a/app/builders/v2/reports/timeseries/base_timeseries_builder.rb +++ b/app/builders/v2/reports/timeseries/base_timeseries_builder.rb @@ -1,11 +1,12 @@ class V2::Reports::Timeseries::BaseTimeseriesBuilder + include TimezoneHelper include DateRangeHelper DEFAULT_GROUP_BY = 'day'.freeze pattr_initialize :account, :params def scope - case params[:type] + case params[:type].to_sym when :account account when :inbox @@ -36,14 +37,10 @@ class V2::Reports::Timeseries::BaseTimeseriesBuilder end def group_by - @group_by ||= params[:group_by] || DEFAULT_GROUP_BY + @group_by ||= %w[day week month year hour].include?(params[:group_by]) ? params[:group_by] : DEFAULT_GROUP_BY end def timezone - @timezone ||= ActiveSupport::TimeZone[timezone_offset]&.name - end - - def timezone_offset - @timezone_offset ||= params.fetch(:timezone_offset, 0).to_f + @timezone ||= timezone_name_from_offset(params[:timezone_offset]) end end diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 221c96b85..4eff10127 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -19,7 +19,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController account: Current.account ) - builder.perform + @agent = builder.perform end def update @@ -49,6 +49,11 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}" end end + + # This endpoint is used to bulk create agents during onboarding + # onboarding_step key in present in Current account custom attributes, since this is a one time operation + Current.account.custom_attributes.delete('onboarding_step') + Current.account.save! head :ok end diff --git a/app/controllers/api/v1/accounts/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/bulk_actions_controller.rb index 832f5a49b..34db47861 100644 --- a/app/controllers/api/v1/accounts/bulk_actions_controller.rb +++ b/app/controllers/api/v1/accounts/bulk_actions_controller.rb @@ -21,6 +21,6 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll end def permitted_params - params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) + params.permit(:type, :snoozed_until, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) end end diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index d2d51baef..ebf8e49dd 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -2,13 +2,9 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: before_action :authorize_request def create - ActiveRecord::Base.transaction do - authenticate_twilio - build_inbox - setup_webhooks if @twilio_channel.sms? - rescue StandardError => e - render_could_not_create_error(e.message) - end + process_create + rescue StandardError => e + render_could_not_create_error(e.message) end private @@ -17,6 +13,14 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: authorize ::Inbox end + def process_create + ActiveRecord::Base.transaction do + authenticate_twilio + build_inbox + setup_webhooks if @twilio_channel.sms? + end + end + def authenticate_twilio client = if permitted_params[:api_key_sid].present? Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid]) diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index f424f3b66..250c64a86 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -46,7 +46,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def export column_names = params['column_names'] - Account::ContactsExportJob.perform_later(Current.account.id, column_names) + filter_params = { :payload => params.permit!['payload'], :label => params.permit!['label'] } + Account::ContactsExportJob.perform_later(Current.account.id, Current.user.id, column_names, filter_params) head :ok, message: I18n.t('errors.contacts.export.success') end @@ -61,10 +62,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def show; end def filter - result = ::Contacts::FilterService.new(params.permit!, current_user).perform + result = ::Contacts::FilterService.new(Current.account, Current.user, params.permit!).perform contacts = result[:contacts] @contacts_count = result[:count] @contacts = fetch_contacts(contacts) + rescue CustomExceptions::CustomFilter::InvalidAttribute, + CustomExceptions::CustomFilter::InvalidOperator, + CustomExceptions::CustomFilter::InvalidValue => e + render_could_not_create_error(e.message) end def contactable_inboxes @@ -148,7 +153,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController end def permitted_params - params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {}) + params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {}) end def contact_custom_attributes diff --git a/app/controllers/api/v1/accounts/conversations/draft_messages_controller.rb b/app/controllers/api/v1/accounts/conversations/draft_messages_controller.rb new file mode 100644 index 000000000..86b7ce5bb --- /dev/null +++ b/app/controllers/api/v1/accounts/conversations/draft_messages_controller.rb @@ -0,0 +1,28 @@ +class Api::V1::Accounts::Conversations::DraftMessagesController < Api::V1::Accounts::Conversations::BaseController + def show + render json: { has_draft: false } and return unless Redis::Alfred.exists?(draft_redis_key) + + draft_message = Redis::Alfred.get(draft_redis_key) + render json: { has_draft: true, message: draft_message } + end + + def update + Redis::Alfred.set(draft_redis_key, draft_message_params) + head :ok + end + + def destroy + Redis::Alfred.delete(draft_redis_key) + head :ok + end + + private + + def draft_redis_key + format(Redis::Alfred::CONVERSATION_DRAFT_MESSAGE, id: @conversation.id) + end + + def draft_message_params + params.dig(:draft_message, :message) || '' + end +end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 281ff95de..2aedf1928 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -36,10 +36,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end end + def update + @conversation.update!(permitted_update_params) + end + def filter result = ::Conversations::FilterService.new(params.permit!, current_user).perform @conversations = result[:conversations] @conversations_count = result[:count] + rescue CustomExceptions::CustomFilter::InvalidAttribute, + CustomExceptions::CustomFilter::InvalidOperator, + CustomExceptions::CustomFilter::InvalidValue => e + render_could_not_create_error(e.message) end def mute @@ -110,6 +118,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro private + def permitted_update_params + # TODO: Move the other conversation attributes to this method and remove specific endpoints for each attribute + params.permit(:priority) + end + def update_last_seen_on_conversation(last_seen_at, update_assignee) # rubocop:disable Rails/SkipsModelValidations @conversation.update_column(:agent_last_seen_at, last_seen_at) @@ -176,3 +189,5 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro @conversation.assignee_id? && Current.user == @conversation.assignee end end + +Api::V1::Accounts::ConversationsController.prepend_mod_with('Api::V1::Accounts::ConversationsController') diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb new file mode 100644 index 000000000..9d5d76d75 --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb @@ -0,0 +1,93 @@ +class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController + before_action :fetch_conversation, only: [:link_issue, :linked_issues] + + def teams + teams = linear_processor_service.teams + if teams[:error] + render json: { error: teams[:error] }, status: :unprocessable_entity + else + render json: teams[:data], status: :ok + end + end + + def team_entities + team_id = permitted_params[:team_id] + team_entities = linear_processor_service.team_entities(team_id) + if team_entities[:error] + render json: { error: team_entities[:error] }, status: :unprocessable_entity + else + render json: team_entities[:data], status: :ok + end + end + + def create_issue + issue = linear_processor_service.create_issue(permitted_params) + if issue[:error] + render json: { error: issue[:error] }, status: :unprocessable_entity + else + render json: issue[:data], status: :ok + end + end + + def link_issue + issue_id = permitted_params[:issue_id] + title = permitted_params[:title] + issue = linear_processor_service.link_issue(conversation_link, issue_id, title) + if issue[:error] + render json: { error: issue[:error] }, status: :unprocessable_entity + else + render json: issue[:data], status: :ok + end + end + + def unlink_issue + link_id = permitted_params[:link_id] + issue = linear_processor_service.unlink_issue(link_id) + + if issue[:error] + render json: { error: issue[:error] }, status: :unprocessable_entity + else + render json: issue[:data], status: :ok + end + end + + def linked_issues + issues = linear_processor_service.linked_issues(conversation_link) + + if issues[:error] + render json: { error: issues[:error] }, status: :unprocessable_entity + else + render json: issues[:data], status: :ok + end + end + + def search_issue + render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return + + term = params[:q] + issues = linear_processor_service.search_issue(term) + if issues[:error] + render json: { error: issues[:error] }, status: :unprocessable_entity + else + render json: issues[:data], status: :ok + end + end + + private + + def conversation_link + "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/conversations/#{@conversation.display_id}" + end + + def fetch_conversation + @conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id]) + end + + def linear_processor_service + Integrations::Linear::ProcessorService.new(account: Current.account) + end + + def permitted_params + params.permit(:team_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: []) + end +end diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index 0eeff5695..52035ce64 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -7,8 +7,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro before_action :set_current_page, only: [:index] def index + @notifications = notification_finder.notifications @unread_count = notification_finder.unread_count - @notifications = notification_finder.perform @count = notification_finder.count end @@ -54,7 +54,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro end def snooze - @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until] + updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil) + @notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until] render json: @notification end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 481594ee4..c0dac6d9e 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -46,6 +46,7 @@ class Api::V1::AccountsController < Api::BaseController def update @account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration)) @account.custom_attributes.merge!(custom_attributes_params) + @account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update' @account.save! end diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 1d1ba8a7e..7e6d84bd2 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -33,10 +33,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def transcript - if permitted_params[:email].present? && conversation.present? + if conversation.present? && conversation.contact.present? && conversation.contact.email.present? ConversationReplyMailer.with(account: conversation.account).conversation_transcript( conversation, - permitted_params[:email] + conversation.contact.email )&.deliver_later end head :ok diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index d24669952..ed5be5518 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -5,7 +5,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController before_action :check_authorization def index - builder = V2::ConversationReportBuilder.new(Current.account, report_params) + builder = V2::Reports::Conversations::ReportBuilder.new(Current.account, report_params) data = builder.timeseries render json: data end @@ -125,7 +125,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController end def build_summary(method) - builder = V2::ConversationMetricBuilder + builder = V2::Reports::Conversations::MetricBuilder current_summary = builder.new(Current.account, current_summary_params).send(method) previous_summary = builder.new(Current.account, previous_summary_params).send(method) current_summary.merge(previous: previous_summary) diff --git a/app/controllers/api/v2/accounts_controller.rb b/app/controllers/api/v2/accounts_controller.rb index ea19727ad..45faca3d0 100644 --- a/app/controllers/api/v2/accounts_controller.rb +++ b/app/controllers/api/v2/accounts_controller.rb @@ -22,6 +22,7 @@ class Api::V2::AccountsController < Api::BaseController ).perform fetch_account_and_user_info + update_account_info if @account.present? if @user send_auth_headers(@user) @@ -33,6 +34,18 @@ class Api::V2::AccountsController < Api::BaseController private + def account_attributes + { + custom_attributes: @account.custom_attributes.merge({ 'onboarding_step' => 'profile_update' }) + } + end + + def update_account_info + @account.update!( + account_attributes + ) + end + def fetch_account_and_user_info; end def fetch_account diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d2960a699..2f389049d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,3 +25,4 @@ class ApplicationController < ActionController::Base } end end +ApplicationController.include_mod_with('Concerns::ApplicationControllerConcern') diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index cd760f7ce..2ee9f9854 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -1,6 +1,6 @@ module AccessTokenAuthHelper BOT_ACCESSIBLE_ENDPOINTS = { - 'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create], + 'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update], 'api/v1/accounts/conversations/messages' => ['create'], 'api/v1/accounts/conversations/assignments' => ['create'] }.freeze diff --git a/app/controllers/concerns/domain_helper.rb b/app/controllers/concerns/domain_helper.rb new file mode 100644 index 000000000..1b7d8f187 --- /dev/null +++ b/app/controllers/concerns/domain_helper.rb @@ -0,0 +1,5 @@ +module DomainHelper + def self.chatwoot_domain?(domain = request.host) + [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain) + end +end diff --git a/app/controllers/concerns/microsoft_concern.rb b/app/controllers/concerns/microsoft_concern.rb index 3aa3e4e81..0f37bd03f 100644 --- a/app/controllers/concerns/microsoft_concern.rb +++ b/app/controllers/concerns/microsoft_concern.rb @@ -2,7 +2,10 @@ module MicrosoftConcern extend ActiveSupport::Concern def microsoft_client - ::OAuth2::Client.new(ENV.fetch('AZURE_APP_ID', nil), ENV.fetch('AZURE_APP_SECRET', nil), + app_id = GlobalConfigService.load('AZURE_APP_ID', nil) + app_secret = GlobalConfigService.load('AZURE_APP_SECRET', nil) + + ::OAuth2::Client.new(app_id, app_secret, { site: 'https://login.microsoftonline.com', authorize_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', diff --git a/app/controllers/concerns/switch_locale.rb b/app/controllers/concerns/switch_locale.rb index 744a70da9..3013ff3cc 100644 --- a/app/controllers/concerns/switch_locale.rb +++ b/app/controllers/concerns/switch_locale.rb @@ -6,6 +6,7 @@ module SwitchLocale def switch_locale(&) # priority is for locale set in query string (mostly for widget/from js sdk) locale ||= locale_from_params + locale ||= locale_from_custom_domain # if locale is not set in account, let's use DEFAULT_LOCALE env variable locale ||= locale_from_env_variable set_locale(locale, &) @@ -16,6 +17,20 @@ module SwitchLocale set_locale(locale, &) end + # If the request is coming from a custom domain, it should be for a helpcenter portal + # We will use the portal locale in such cases + def locale_from_custom_domain(&) + return if params[:locale] + + domain = request.host + return if DomainHelper.chatwoot_domain?(domain) + + @portal = Portal.find_by(custom_domain: domain) + return unless @portal + + @portal.default_locale + end + def set_locale(locale, &) # if locale is empty, use default_locale locale ||= I18n.default_locale diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 9e59758ea..521d027cc 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -18,6 +18,7 @@ class DashboardController < ActionController::Base 'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL', 'TERMS_URL', + 'BRAND_URL', 'BRAND_NAME', 'PRIVACY_URL', 'DISPLAY_MANIFEST', 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD', @@ -55,9 +56,9 @@ class DashboardController < ActionController::Base VAPID_PUBLIC_KEY: VapidService.public_key, ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), - FACEBOOK_API_VERSION: 'v14.0', + FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), IS_ENTERPRISE: ChatwootApp.enterprise?, - AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''), + AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), GIT_SHA: GIT_HASH } end diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb index 26a9d4555..17dd32086 100644 --- a/app/controllers/devise_overrides/passwords_controller.rb +++ b/app/controllers/devise_overrides/passwords_controller.rb @@ -5,7 +5,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController skip_before_action :authenticate_user!, raise: false def create - @user = User.find_by(email: params[:email]) + @user = User.from_email(params[:email]) if @user @user.send_reset_password_instructions build_response(I18n.t('messages.reset_password_success'), 200) diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index 2659aeebf..fc7b12767 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -4,6 +4,10 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController wrap_parameters format: [] before_action :process_sso_auth_token, only: [:create] + def new + redirect_to login_page_url(error: 'access-denied') + end + def create # Authenticate user via the temporary sso auth token if params[:sso_auth_token].present? && @resource.present? @@ -21,6 +25,12 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController private + def login_page_url(error: nil) + frontend_url = ENV.fetch('FRONTEND_URL', nil) + + "#{frontend_url}/app/login?error=#{error}" + end + def authenticate_resource_with_sso_token @token = @resource.create_token @resource.save! @@ -33,7 +43,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController def process_sso_auth_token return if params[:email].blank? - user = User.find_by(email: params[:email]) + user = User.from_email(params[:email]) @resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token]) end end diff --git a/app/controllers/microsoft_controller.rb b/app/controllers/microsoft_controller.rb index 07e58d4db..e6a12dafa 100644 --- a/app/controllers/microsoft_controller.rb +++ b/app/controllers/microsoft_controller.rb @@ -12,6 +12,6 @@ class MicrosoftController < ApplicationController end def microsoft_indentity - @identity_json = ENV.fetch('AZURE_APP_ID', nil) + @identity_json = GlobalConfigService.load('AZURE_APP_ID', nil) end end diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index 03ed55754..453e475b0 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -8,7 +8,7 @@ class Platform::Api::V1::UsersController < PlatformController def show; end def create - @resource = (User.find_by(email: user_params[:email]) || User.new(user_params)) + @resource = (User.from_email(user_params[:email]) || User.new(user_params)) @resource.skip_confirmation! @resource.save! @platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource) diff --git a/app/controllers/public/api/v1/inboxes/conversations_controller.rb b/app/controllers/public/api/v1/inboxes/conversations_controller.rb index 671863bdc..4e3b5dca9 100644 --- a/app/controllers/public/api/v1/inboxes/conversations_controller.rb +++ b/app/controllers/public/api/v1/inboxes/conversations_controller.rb @@ -1,15 +1,31 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController include Events::Types - before_action :set_conversation, only: [:toggle_typing, :update_last_seen] + before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status] def index @conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations end + def show; end + def create @conversation = create_conversation end + def toggle_status + # Check if the conversation is already resolved to prevent redundant operations + return if @conversation.resolved? + + # Assign the conversation's contact as the resolver + # This step attributes the resolution action to the contact involved in the conversation + # If this assignment is not made, the system implicitly becomes the resolver by default + Current.contact = @conversation.contact + + # Update the conversation's status to 'resolved' to reflect its closure + @conversation.status = :resolved + @conversation.save! + end + def toggle_typing case params[:typing_status] when 'on' @@ -30,7 +46,11 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox private def set_conversation - @conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id]) + @conversation = if @contact_inbox.hmac_verified? + @contact_inbox.contact.conversations.find_by!(display_id: params[:id]) + else + @contact_inbox.conversations.find_by!(display_id: params[:id]) + end end def create_conversation diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 4e967cfbb..46ecf19a7 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -7,7 +7,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def index @articles = @portal.articles - @articles = @articles.search(list_params) if list_params.present? + search_articles order_by_sort_param @articles.page(list_params[:page]) if list_params[:page].present? end @@ -16,6 +16,10 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B private + def search_articles + @articles = @articles.search(list_params) if list_params.present? + end + def order_by_sort_param @articles = if list_params[:sort].present? && list_params[:sort] == 'views' @articles.order_by_views @@ -51,3 +55,5 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B ChatwootMarkdownRenderer.new(content).render_article end end + +Public::Api::V1::Portals::ArticlesController.prepend_mod_with('Public::Api::V1::Portals::ArticlesController') diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index 4d3cc56b8..f6c10f7c4 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -47,7 +47,7 @@ class Public::Api::V1::Portals::BaseController < PublicController @locale = if article.category.present? article.category.locale else - 'en' + article.portal.default_locale end I18n.with_locale(@locale, &) diff --git a/app/controllers/public/api/v1/portals_controller.rb b/app/controllers/public/api/v1/portals_controller.rb index fe51ed161..e8fb867cb 100644 --- a/app/controllers/public/api/v1/portals_controller.rb +++ b/app/controllers/public/api/v1/portals_controller.rb @@ -6,6 +6,12 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl def show; end + def sitemap + @help_center_url = @portal.custom_domain || ChatwootApp.help_center_root + # if help_center_url does not contain a protocol, prepend it with https + @help_center_url = "https://#{@help_center_url}" unless @help_center_url.include?('://') + end + private def portal diff --git a/app/controllers/public_controller.rb b/app/controllers/public_controller.rb index 0c3f52ff6..3b83a2210 100644 --- a/app/controllers/public_controller.rb +++ b/app/controllers/public_controller.rb @@ -8,8 +8,7 @@ class PublicController < ActionController::Base def ensure_custom_domain_request domain = request.host - - return if [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain) + return if DomainHelper.chatwoot_domain?(domain) @portal = ::Portal.find_by(custom_domain: domain) return if @portal.present? diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index a31d01675..9d9494fc4 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -34,7 +34,9 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController def allowed_configs @allowed_configs = case @config when 'facebook' - %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + when 'microsoft' + %w[AZURE_APP_ID AZURE_APP_SECRET] when 'email' ['MAILER_INBOUND_EMAIL_DOMAIN'] else diff --git a/app/controllers/super_admin/application_controller.rb b/app/controllers/super_admin/application_controller.rb index 3b98a6e21..775fb34fc 100644 --- a/app/controllers/super_admin/application_controller.rb +++ b/app/controllers/super_admin/application_controller.rb @@ -5,6 +5,10 @@ # If you want to add pagination or other controller-level concerns, # you're free to overwrite the RESTful controller actions. class SuperAdmin::ApplicationController < Administrate::ApplicationController + include ActionView::Helpers::TagHelper + include ActionView::Context + + helper_method :render_vue_component # authenticiation done via devise : SuperAdmin Model before_action :authenticate_super_admin! @@ -23,6 +27,17 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController private + def render_vue_component(component_name, props = {}) + html_options = { + id: 'app', + data: { + component_name: component_name, + props: props.to_json + } + } + content_tag(:div, '', html_options) + end + def invalid_action_perfomed # rubocop:disable Rails/I18nLocaleTexts flash[:error] = 'Invalid action performed' diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index f2f238cd4..b582bc5dd 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -10,14 +10,15 @@ class AsyncDispatcher < BaseDispatcher def listeners [ + AutomationRuleListener.instance, CampaignListener.instance, CsatSurveyListener.instance, HookListener.instance, InstallationWebhookListener.instance, NotificationListener.instance, + ParticipationListener.instance, ReportingEventListener.instance, - WebhookListener.instance, - AutomationRuleListener.instance + WebhookListener.instance ] end end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 0cc5e52d8..44592c201 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -163,12 +163,23 @@ class ConversationFinder params[:page] || 1 end - def conversations - @conversations = @conversations.includes( + def conversations_base_query + @conversations.includes( :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox ) + end + + def conversations + @conversations = conversations_base_query sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc'] - @conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i) + @conversations = @conversations.send(sort_by, sort_order) + + if params[:updated_within].present? + @conversations.where('conversations.updated_at > ?', Time.zone.now - params[:updated_within].to_i.seconds) + else + @conversations.page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i) + end end end +ConversationFinder.prepend_mod_with('ConversationFinder') diff --git a/app/finders/notification_finder.rb b/app/finders/notification_finder.rb index 7e5789372..ccfe470a0 100644 --- a/app/finders/notification_finder.rb +++ b/app/finders/notification_finder.rb @@ -10,8 +10,8 @@ class NotificationFinder set_up end - def perform - notifications + def notifications + @notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order) end def unread_count @@ -26,27 +26,31 @@ class NotificationFinder def set_up find_all_notifications - filter_by_read_status - filter_by_status + filter_snoozed_notifications + fitler_read_notifications end def find_all_notifications @notifications = current_user.notifications.where(account_id: @current_account.id) end - def filter_by_status - @notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed' + def filter_snoozed_notifications + @notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed') end - def filter_by_read_status - @notifications = @notifications.where.not(read_at: nil) if params[:type] == 'read' + def fitler_read_notifications + @notifications = @notifications.where(read_at: nil) unless type_included?('read') + end + + def type_included?(type) + (params[:includes] || []).include?(type) end def current_page params[:page] || 1 end - def notifications - @notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: params[:sort_order] || :desc) + def sort_order + params[:sort_order] || :desc end end diff --git a/app/helpers/api/v2/accounts/reports_helper.rb b/app/helpers/api/v2/accounts/reports_helper.rb index 74c7af43e..4d5a6f115 100644 --- a/app/helpers/api/v2/accounts/reports_helper.rb +++ b/app/helpers/api/v2/accounts/reports_helper.rb @@ -1,7 +1,7 @@ module Api::V2::Accounts::ReportsHelper def generate_agents_report Current.account.users.map do |agent| - agent_report = generate_report({ type: :agent, id: agent.id }) + agent_report = report_builder({ type: :agent, id: agent.id }).summary [agent.name] + generate_readable_report_metrics(agent_report) end end @@ -15,7 +15,7 @@ module Api::V2::Accounts::ReportsHelper def generate_teams_report Current.account.teams.map do |team| - team_report = generate_report({ type: :team, id: team.id }) + team_report = report_builder({ type: :team, id: team.id }).summary [team.name] + generate_readable_report_metrics(team_report) end end @@ -27,7 +27,7 @@ module Api::V2::Accounts::ReportsHelper end end - def generate_report(report_params) + def report_builder(report_params) V2::ReportBuilder.new( Current.account, report_params.merge( @@ -37,7 +37,11 @@ module Api::V2::Accounts::ReportsHelper business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours]) } ) - ).short_summary + ) + end + + def generate_report(report_params) + report_builder(report_params).short_summary end private @@ -46,7 +50,9 @@ module Api::V2::Accounts::ReportsHelper [ report_metric[:conversations_count], Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format, - Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format + Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format, + Reports::TimeFormatPresenter.new(report_metric[:reply_time]).format, + report_metric[:resolutions_count] ] end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b52b2300e..76d0bacc8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,4 +2,11 @@ module ApplicationHelper def available_locales_with_name LANGUAGES_CONFIG.map { |_key, val| val.slice(:name, :iso_639_1_code) } end + + def feature_help_urls + features = YAML.safe_load(Rails.root.join('config/features.yml').read).freeze + features.each_with_object({}) do |feature, hash| + hash[feature['name']] = feature['help_url'] if feature['help_url'] + end + end end diff --git a/app/helpers/contact_helper.rb b/app/helpers/contact_helper.rb new file mode 100644 index 000000000..2bd86937f --- /dev/null +++ b/app/helpers/contact_helper.rb @@ -0,0 +1,83 @@ +module ContactHelper + def parse_name(full_name) + # If the input is nil or not a string, return a hash with all values set to nil + return default_name_hash if invalid_name?(full_name) + + # If the input is a number, return a hash with the number as the first name + return numeric_name_hash(full_name) if valid_number?(full_name) + + full_name = full_name.squish + + # If full name consists of only one word, consider it as the first name + return single_word_name_hash(full_name) if single_word?(full_name) + + parts = split_name(full_name) + parts = handle_conjunction(parts) + build_name_hash(parts) + end + + private + + def default_name_hash + { first_name: nil, last_name: nil, middle_name: nil, prefix: nil, suffix: nil } + end + + def invalid_name?(full_name) + !full_name.is_a?(String) || full_name.empty? + end + + def numeric_name_hash(full_name) + { first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil } + end + + def valid_number?(full_name) + full_name.gsub(/\s+/, '').match?(/\A\+?\d+\z/) + end + + def single_word_name_hash(full_name) + { first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil } + end + + def single_word?(full_name) + full_name.split.size == 1 + end + + def split_name(full_name) + full_name.split + end + + def handle_conjunction(parts) + conjunctions = ['and', '&'] + parts.each_index do |i| + next unless conjunctions.include?(parts[i]) && i.positive? + + parts[i - 1] = [parts[i - 1], parts[i + 1]].join(' ') + parts.delete_at(i) + parts.delete_at(i) + end + parts + end + + def build_name_hash(parts) + suffix = parts.pop if parts.last.match?(/(\w+\.|[IVXLM]+|[A-Z]+)$/) + last_name = parts.pop + prefix = parts.shift if parts.first.match?(/^\w+\./) + first_name = parts.shift + middle_name = parts.join(' ') + + hash = { + first_name: first_name, + last_name: last_name, + prefix: prefix, + middle_name: middle_name, + suffix: suffix + } + + # Reverse name if "," was used in Last, First notation. + if hash[:first_name] =~ /,$/ + hash[:first_name] = hash[:last_name] + hash[:last_name] = Regexp.last_match.pre_match + end + hash + end +end diff --git a/app/helpers/filter_helper.rb b/app/helpers/filter_helper.rb new file mode 100644 index 000000000..bce2de5ea --- /dev/null +++ b/app/helpers/filter_helper.rb @@ -0,0 +1,84 @@ +module FilterHelper + def build_condition_query(model_filters, query_hash, current_index) + current_filter = model_filters[query_hash['attribute_key']] + + # Throw InvalidOperator Error if the attribute is a standard attribute + # and the operator is not allowed in the config + if current_filter.present? && current_filter['filter_operators'].exclude?(query_hash[:filter_operator]) + raise CustomExceptions::CustomFilter::InvalidOperator.new( + attribute_name: query_hash['attribute_key'], + allowed_keys: current_filter['filter_operators'] + ) + end + + # Every other filter expects a value to be present + if %w[is_present is_not_present].exclude?(query_hash[:filter_operator]) && query_hash['values'].blank? + raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: query_hash['attribute_key']) + end + + condition_query = build_condition_query_string(current_filter, query_hash, current_index) + # The query becomes empty only when it doesn't match to any supported + # standard attribute or custom attribute defined in the account. + if condition_query.empty? + raise CustomExceptions::CustomFilter::InvalidAttribute.new(key: query_hash['attribute_key'], + allowed_keys: model_filters.keys) + end + + condition_query + end + + def build_condition_query_string(current_filter, query_hash, current_index) + filter_operator_value = filter_operation(query_hash, current_index) + + return handle_nil_filter(query_hash, current_index) if current_filter.nil? + + case current_filter['attribute_type'] + when 'additional_attributes' + handle_additional_attributes(query_hash, filter_operator_value, current_filter['data_type']) + else + handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) + end + end + + def handle_nil_filter(query_hash, current_index) + attribute_type = "#{filter_config[:entity].downcase}_attribute" + custom_attribute_query(query_hash, attribute_type, current_index) + end + + def handle_additional_attributes(query_hash, filter_operator_value, data_type) + if data_type == 'text_case_insensitive' + "LOWER(#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}') " \ + "#{filter_operator_value} #{query_hash[:query_operator]}" + else + "#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}' " \ + "#{filter_operator_value} #{query_hash[:query_operator]} " + end + end + + def handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) + case current_filter['data_type'] + when 'date' + date_filter(current_filter, query_hash, filter_operator_value) + when 'labels' + tag_filter_query(query_hash, current_index) + when 'text_case_insensitive' + text_case_insensitive_filter(query_hash, filter_operator_value) + else + default_filter(query_hash, filter_operator_value) + end + end + + def date_filter(current_filter, query_hash, filter_operator_value) + "(#{filter_config[:table_name]}.#{query_hash[:attribute_key]})::#{current_filter['data_type']} " \ + "#{filter_operator_value}#{current_filter['data_type']} #{query_hash[:query_operator]}" + end + + def text_case_insensitive_filter(query_hash, filter_operator_value) + "LOWER(#{filter_config[:table_name]}.#{query_hash[:attribute_key]}) " \ + "#{filter_operator_value} #{query_hash[:query_operator]}" + end + + def default_filter(query_hash, filter_operator_value) + "#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}" + end +end diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb index 2b0f43b3f..99f3fd36b 100644 --- a/app/helpers/report_helper.rb +++ b/app/helpers/report_helper.rb @@ -32,6 +32,14 @@ module ReportHelper (get_grouped_values resolutions).count end + def bot_resolutions_count + (get_grouped_values bot_resolutions).count + end + + def bot_handoffs_count + (get_grouped_values bot_handoffs).count + end + def conversations scope.conversations.where(account_id: account.id, created_at: range) end @@ -49,6 +57,16 @@ module ReportHelper conversations: { status: :resolved }, created_at: range).distinct end + def bot_resolutions + scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved, + conversations: { status: :resolved }, created_at: range).distinct + end + + def bot_handoffs + scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff, + created_at: range).distinct + end + def avg_first_response_time grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response', account_id: account.id)) return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] diff --git a/app/helpers/timezone_helper.rb b/app/helpers/timezone_helper.rb new file mode 100644 index 000000000..b016cc9d9 --- /dev/null +++ b/app/helpers/timezone_helper.rb @@ -0,0 +1,19 @@ +module TimezoneHelper + # ActiveSupport TimeZone is not aware of the current time, so ActiveSupport::Timezone[offset] + # would return the timezone without considering day light savings. To get the correct timezone, + # this method uses zone.now.utc_offset for comparison as referenced in the issues below + # + # https://github.com/rails/rails/pull/22243 + # https://github.com/rails/rails/issues/21501 + # https://github.com/rails/rails/issues/7297 + def timezone_name_from_offset(offset) + return 'UTC' if offset.blank? + + offset_in_seconds = offset.to_f * 3600 + matching_zone = ActiveSupport::TimeZone.all.find do |zone| + zone.now.utc_offset == offset_in_seconds + end + + return matching_zone.name if matching_zone + end +end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 49fe325f6..481c671ce 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -8,8 +8,8 @@ > @@ -38,6 +38,7 @@ import vueActionCable from './helper/actionCable'; import WootSnackbarBox from './components/SnackbarContainer.vue'; import rtlMixin from 'shared/mixins/rtlMixin'; import { setColorTheme } from './helper/themeHelper'; +import { isOnOnboardingView } from 'v3/helpers/RouteHelper'; import { registerSubscription, verifyServiceWorkerExistence, @@ -79,6 +80,9 @@ export default { const { accounts = [] } = this.currentUser || {}; return accounts.length > 0; }, + hideOnOnboardingView() { + return !isOnOnboardingView(this.$route); + }, }, watch: { diff --git a/app/javascript/dashboard/api/agents.js b/app/javascript/dashboard/api/agents.js index 7cc5e6d0c..cfc6b36ff 100644 --- a/app/javascript/dashboard/api/agents.js +++ b/app/javascript/dashboard/api/agents.js @@ -1,9 +1,17 @@ +/* global axios */ + import ApiClient from './ApiClient'; class Agents extends ApiClient { constructor() { super('agents', { accountScoped: true }); } + + bulkInvite({ emails }) { + return axios.post(`${this.url}/bulk_create`, { + emails, + }); + } } export default new Agents(); diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 61312dd24..85c4bba1d 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -77,8 +77,8 @@ class ContactAPI extends ApiClient { return axios.delete(`${this.url}/${contactId}/avatar`); } - exportContacts() { - return axios.get(`${this.url}/export`); + exportContacts(queryPayload) { + return axios.post(`${this.url}/export`, queryPayload); } } diff --git a/app/javascript/dashboard/api/notifications.js b/app/javascript/dashboard/api/notifications.js index 65fc937b7..65284623f 100644 --- a/app/javascript/dashboard/api/notifications.js +++ b/app/javascript/dashboard/api/notifications.js @@ -7,12 +7,13 @@ class NotificationsAPI extends ApiClient { } get({ page, status, type, sortOrder }) { + const includesFilter = [status, type].filter(value => !!value); + return axios.get(this.url, { params: { page, - status, - type, sort_order: sortOrder, + includes: includesFilter, }, }); } diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 6188778fa..c87dfc82b 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -84,6 +84,24 @@ class ReportsAPI extends ApiClient { params: { since, until, business_hours: businessHours }, }); } + + getBotMetrics({ from, to } = {}) { + return axios.get(`${this.url}/bot_metrics`, { + params: { since: from, until: to }, + }); + } + + getBotSummary({ from, to, groupBy, businessHours } = {}) { + return axios.get(`${this.url}/bot_summary`, { + params: { + since: from, + until: to, + type: 'account', + group_by: groupBy, + business_hours: businessHours, + }, + }); + } } export default new ReportsAPI(); diff --git a/app/javascript/dashboard/api/sla.js b/app/javascript/dashboard/api/sla.js new file mode 100644 index 000000000..8480b1846 --- /dev/null +++ b/app/javascript/dashboard/api/sla.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class SlaAPI extends ApiClient { + constructor() { + super('sla_policies', { accountScoped: true }); + } +} + +export default new SlaAPI(); diff --git a/app/javascript/dashboard/api/slaReports.js b/app/javascript/dashboard/api/slaReports.js new file mode 100644 index 000000000..fedc988b2 --- /dev/null +++ b/app/javascript/dashboard/api/slaReports.js @@ -0,0 +1,78 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class SLAReportsAPI extends ApiClient { + constructor() { + super('applied_slas', { accountScoped: true }); + } + + get({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + label_list, + page, + } = {}) { + return axios.get(this.url, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + label_list, + page, + }, + }); + } + + download({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + sla_policy_id, + label_list, + } = {}) { + return axios.get(`${this.url}/download`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + team_id, + label_list, + sla_policy_id, + }, + }); + } + + getMetrics({ + from, + to, + assigned_agent_id, + inbox_id, + team_id, + label_list, + sla_policy_id, + } = {}) { + return axios.get(`${this.url}/metrics`, { + params: { + since: from, + until: to, + assigned_agent_id, + inbox_id, + label_list, + team_id, + sla_policy_id, + }, + }); + } +} + +export default new SLAReportsAPI(); diff --git a/app/javascript/dashboard/api/specs/agents.spec.js b/app/javascript/dashboard/api/specs/agents.spec.js index 7cf1bdd0e..20dd36688 100644 --- a/app/javascript/dashboard/api/specs/agents.spec.js +++ b/app/javascript/dashboard/api/specs/agents.spec.js @@ -10,4 +10,29 @@ describe('#AgentAPI', () => { expect(agents).toHaveProperty('update'); expect(agents).toHaveProperty('delete'); }); + + describe('API calls', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#bulkInvite', () => { + agents.bulkInvite({ emails: ['hello@hi.com'] }); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/agents/bulk_create', + { + emails: ['hello@hi.com'], + } + ); + }); + }); }); diff --git a/app/javascript/dashboard/api/specs/notifications.spec.js b/app/javascript/dashboard/api/specs/notifications.spec.js index bc06eaa2b..fe748fe19 100644 --- a/app/javascript/dashboard/api/specs/notifications.spec.js +++ b/app/javascript/dashboard/api/specs/notifications.spec.js @@ -27,20 +27,36 @@ describe('#NotificationAPI', () => { window.axios = originalAxios; }); - it('#get', () => { - notificationsAPI.get({ - page: 1, - status: 'read', - type: 'Conversation', - sortOrder: 'desc', - }); - expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', { - params: { + describe('#get', () => { + it('generates the API call if both params are available', () => { + notificationsAPI.get({ page: 1, - status: 'read', - type: 'Conversation', - sort_order: 'desc', - }, + status: 'snoozed', + type: 'read', + sortOrder: 'desc', + }); + expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', { + params: { + page: 1, + sort_order: 'desc', + includes: ['snoozed', 'read'], + }, + }); + }); + + it('generates the API call if one of the params are available', () => { + notificationsAPI.get({ + page: 1, + type: 'read', + sortOrder: 'desc', + }); + expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', { + params: { + page: 1, + sort_order: 'desc', + includes: ['read'], + }, + }); }); }); diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index 7822dad8f..05d4a152c 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -111,6 +111,40 @@ describe('#Reports API', () => { }); }); + it('#getBotMetrics', () => { + reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_metrics', + { + params: { + since: 1621103400, + until: 1621621800, + }, + } + ); + }); + + it('#getBotSummary', () => { + reportsAPI.getBotSummary({ + from: 1621103400, + to: 1621621800, + groupBy: 'date', + businessHours: true, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/bot_summary', + { + params: { + since: 1621103400, + until: 1621621800, + type: 'account', + group_by: 'date', + business_hours: true, + }, + } + ); + }); + it('#getConversationMetric', () => { reportsAPI.getConversationMetric('account'); expect(axiosMock.get).toHaveBeenCalledWith( diff --git a/app/javascript/dashboard/api/specs/slaReports.spec.js b/app/javascript/dashboard/api/specs/slaReports.spec.js new file mode 100644 index 000000000..f540b6acc --- /dev/null +++ b/app/javascript/dashboard/api/specs/slaReports.spec.js @@ -0,0 +1,104 @@ +import SLAReportsAPI from '../slaReports'; +import ApiClient from '../ApiClient'; + +describe('#SLAReports API', () => { + it('creates correct instance', () => { + expect(SLAReportsAPI).toBeInstanceOf(ApiClient); + expect(SLAReportsAPI.apiVersion).toBe('/api/v1'); + expect(SLAReportsAPI).toHaveProperty('get'); + expect(SLAReportsAPI).toHaveProperty('getMetrics'); + }); + + describe('API calls', () => { + const originalAxios = window.axios; + const axiosMock = { + post: jest.fn(() => Promise.resolve()), + get: jest.fn(() => Promise.resolve()), + patch: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#get', () => { + SLAReportsAPI.get({ + page: 1, + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }); + expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/applied_slas', { + params: { + page: 1, + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }, + }); + }); + it('#getMetrics', () => { + SLAReportsAPI.getMetrics({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/applied_slas/metrics', + { + params: { + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }, + } + ); + }); + it('#download', () => { + SLAReportsAPI.download({ + from: 1622485800, + to: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/applied_slas/download', + { + params: { + since: 1622485800, + until: 1623695400, + assigned_agent_id: 1, + inbox_id: 1, + team_id: 1, + sla_policy_id: 1, + label_list: ['label1'], + }, + } + ); + }); + }); +}); diff --git a/app/javascript/dashboard/assets/scss/_date-picker.scss b/app/javascript/dashboard/assets/scss/_date-picker.scss index 2132d5fd5..60c24b421 100644 --- a/app/javascript/dashboard/assets/scss/_date-picker.scss +++ b/app/javascript/dashboard/assets/scss/_date-picker.scss @@ -1,6 +1,13 @@ @import '~vue2-datepicker/scss/index'; .date-picker { + // To be removed one SLA reports date picker is created + &.small { + .mx-input { + @apply h-8 text-sm; + } + } + &.no-margin { .mx-input { @apply mb-0; diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss deleted file mode 100644 index f7d10e633..000000000 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ /dev/null @@ -1,58 +0,0 @@ -.button { - font-family: $body-font-family; - font-weight: $font-weight-medium; - - &.round { - border-radius: 1000px; - } -} - -select { - height: 2.5rem; -} - -.card { - margin-bottom: var(--space-small); - padding: var(--space-normal); -} - -code { - border: 0; - font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', - '"Liberation Mono"', '"Courier New"', 'monospace'; - font-size: $font-size-mini; - - &.hljs { - background: $color-background; - border-radius: var(--border-radius-large); - padding: $space-two; - @apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100; - } -} - -.text-truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.text-capitalize { - text-transform: capitalize; -} - -.cursor-pointer { - cursor: pointer; -} - -// remove when grid gutters are fixed -.columns.with-right-space { - padding-right: var(--space-normal); -} - -.badge { - border-radius: var(--border-radius-normal); -} - -.padding-right-small { - padding-right: var(--space-one); -} diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss deleted file mode 100644 index 3fd834cdd..000000000 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ /dev/null @@ -1,623 +0,0 @@ -// Foundation for Sites Settings -// ----------------------------- -// -// Table of Contents: -// -// 1. Global -// 2. Breakpoints -// 3. The Grid -// 4. Base Typography -// 5. Typography Helpers -// 6. Abide -// 7. Accordion -// 8. Accordion Menu -// 9. Badge -// 10. Breadcrumbs -// 11. Button -// 12. Button Group -// 13. Callout -// 14. Card -// 15. Close Button -// 16. Drilldown -// 17. Dropdown -// 18. Dropdown Menu -// 19. Forms -// 20. Label -// 21. Media Object -// 22. Menu -// 23. Meter -// 24. Off-canvas -// 25. Orbit -// 26. Pagination -// 27. Progress Bar -// 28. Responsive Embed -// 29. Reveal -// 30. Slider -// 31. Switch -// 32. Table -// 33. Tabs -// 34. Thumbnail -// 35. Title Bar -// 36. Tooltip -// 37. Top Bar - -@import '~foundation-sites/scss/util/util'; -// 1. Global -// --------- - -// Disable contrast warnings in Foundation. -$contrast-warnings: false; - -$global-font-size: 16px; -$global-width: 100%; -$global-lineheight: 1.5; -$foundation-palette: (primary: $color-woot, - secondary: #5d7592, - success: #44ce4b, - warning: #ffc532, - alert: #ff382d); -$light-gray: #c0ccda; -$medium-gray: #8492a6; -$dark-gray: $color-gray; -$black: #000; -$white: #fff; -$body-background: $white; -$body-font-color: $color-body; -$body-font-family: 'PlusJakarta', --apple-system, -system-ui, -BlinkMacSystemFont, -"Segoe UI", -Roboto, -"Helvetica Neue", -Tahoma, -Arial, -sans-serif; -$body-antialiased: true; -$global-margin: $space-small; -$global-padding: $space-small; -$global-weight-normal: normal; -$global-weight-bold: bold; -$global-radius: 0; -$global-text-direction: ltr; -$global-flexbox: false; -$print-transparent-backgrounds: true; - -@include add-foundation-colors; - -// 2. Breakpoints -// -------------- - -$breakpoints: (small: 0, - medium: 640px, - large: 1024px, - xlarge: 1200px, - xxlarge: 1400px, - xxxlarge: 1600px, -); -$print-breakpoint: large; -$breakpoint-classes: (small medium large); - -// 3. The Grid -// ----------- - -$grid-row-width: $global-width; -$grid-column-count: 12; -$grid-column-gutter: (small: $zero, - medium: $zero); -$grid-column-align-edge: true; -$block-grid-max: 8; - -// 4. Base Typography -// ------------------ - -$header-font-family: $body-font-family; -$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.3125rem; -$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; -$paragraph-lineheight: 1.65; -$paragraph-margin-bottom: var(--space-small); -$paragraph-text-rendering: optimizeLegibility; -$code-color: $black; -$code-font-family: $font-family-monospace; -$code-font-weight: $global-weight-normal; -$code-background: $light-gray; -$code-border: 1px solid $medium-gray; -$code-padding: rem-calc(2 5 1); -$anchor-color: $primary-color; -$anchor-color-hover: scale-color($anchor-color, $lightness: -14%); -$anchor-text-decoration: none; -$anchor-text-decoration-hover: none; -$hr-width: $global-width; -$hr-border: 1px solid $medium-gray; -$hr-margin: rem-calc(20) auto; -$list-lineheight: $paragraph-lineheight; -$list-margin-bottom: $paragraph-margin-bottom; -$list-style-type: disc; -$list-style-position: outside; -$list-side-margin: 0.78125rem; -$list-nested-side-margin: 0.78125rem; -$defnlist-margin-bottom: 0.6875rem; -$defnlist-term-weight: $global-weight-bold; -$defnlist-term-margin-bottom: 0.1875rem; -$blockquote-color: $dark-gray; -$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'; -$keystroke-font: $font-family-monospace; -$keystroke-color: $black; -$keystroke-background: $light-gray; -$keystroke-padding: rem-calc(2 4 0); -$keystroke-radius: $global-radius; -$abbr-underline: 1px dotted $black; - -// 5. Typography Helpers -// --------------------- - -$lead-font-size: $global-font-size * 1.25; -$lead-lineheight: 1.6; -$subheader-lineheight: 1.4; -$subheader-color: $dark-gray; -$subheader-font-weight: $global-weight-normal; -$subheader-margin-top: 0.125rem; -$subheader-margin-bottom: 0.3125rem; -$stat-font-size: 1.5625rem; - -// 6. Abide -// -------- - -$abide-inputs: true; -$abide-labels: true; -$input-background-invalid: get-color(alert); -$form-label-color-invalid: get-color(alert); -$input-error-color: get-color(alert); -$input-error-font-size: rem-calc(12); -$input-error-font-weight: $global-weight-bold; - -// 7. Accordion -// ------------ - -$accordion-background: $white; -$accordion-plusminus: true; -$accordion-title-font-size: rem-calc(12); -$accordion-item-color: $primary-color; -$accordion-item-background-hover: $light-gray; -$accordion-item-padding: 0.78125rem 0.625rem; -$accordion-content-background: $white; -$accordion-content-border: 1px solid $light-gray; -$accordion-content-color: $body-font-color; -$accordion-content-padding: 0.625rem; - -// 8. Accordion Menu -// ----------------- - -$accordionmenu-arrows: true; -$accordionmenu-arrow-color: $primary-color; -$accordionmenu-arrow-size: 6px; - -// 9. Badge -// -------- - -$badge-background: $primary-color; -$badge-color: $white; -$badge-color-alt: $black; -$badge-palette: $foundation-palette; -$badge-padding: var(--space-smaller); -$badge-minwidth: 2.1em; -$badge-font-size: var(--font-size-nano); - -// 10. Breadcrumbs -// --------------- - -$breadcrumbs-margin: 0 0 $global-margin 0; -$breadcrumbs-item-font-size: rem-calc(11); -$breadcrumbs-item-color: $primary-color; -$breadcrumbs-item-color-current: $black; -$breadcrumbs-item-color-disabled: $medium-gray; -$breadcrumbs-item-margin: 0.46875rem; -$breadcrumbs-item-uppercase: true; -$breadcrumbs-item-slash: true; - -// 11. Button -// ---------- - -$button-padding: var(--space-smaller) 1em; -$button-margin: 0 0 $global-margin 0; -$button-fill: solid; -$button-background: $primary-color; -$button-background-hover: scale-color($button-background, $lightness: -15%); -$button-color: $white; -$button-color-alt: $white; -$button-radius: var(--border-radius-normal); -$button-sizes: (tiny: var(--font-size-micro), - small: var(--font-size-mini), - default: var(--font-size-small), - large: var(--font-size-medium)); -$button-palette: $foundation-palette; -$button-opacity-disabled: 0.4; -$button-background-hover-lightness: -20%; -$button-hollow-hover-lightness: -50%; -$button-transition: background-color 0.25s ease-out, -color 0.25s ease-out; - -// 12. Button Group -// ---------------- - -$buttongroup-margin: 0; -$buttongroup-spacing: 0; -$buttongroup-child-selector: '.button'; -$buttongroup-expand-max: 6; -$buttongroup-radius-on-each: false; - -// 13. Callout -// ----------- - -$callout-background: $white; -$callout-background-fade: 85%; -$callout-border: 1px solid rgba($black, 0.25); -$callout-margin: 0 0 0.625rem 0; -$callout-padding: 0.625rem; -$callout-font-color: $body-font-color; -$callout-font-color-alt: $body-background; -$callout-radius: $global-radius; -$callout-link-tint: 30%; - -// 14. Card -// -------- - -$card-background: $white; -$card-font-color: $body-font-color; -$card-divider-background: $light-gray; -$card-border: 1px solid var(--color-border); -$card-shadow: var(--shadow-small); -$card-border-radius: var(--border-radius-normal); -$card-padding: var(--space-small); -$card-margin: $global-margin; - -// 15. Close Button -// ---------------- - -$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-lineheight: 1; -$closebutton-color: $dark-gray; -$closebutton-color-hover: $black; - -// 16. Drilldown -// ------------- - -$drilldown-transition: transform 0.15s linear; -$drilldown-arrows: true; -$drilldown-arrow-color: $primary-color; -$drilldown-arrow-size: 6px; -$drilldown-background: $white; - -// 17. Dropdown -// ------------ - -$dropdown-padding: 0.625rem; -$dropdown-background: $body-background; -$dropdown-border: 1px solid $medium-gray; -$dropdown-font-size: 0.625rem; -$dropdown-width: 300px; -$dropdown-radius: $global-radius; -$dropdown-sizes: (tiny: 100px, - small: 200px, - large: 400px); - -// 18. Dropdown Menu -// ----------------- - -$dropdownmenu-arrows: true; -$dropdownmenu-arrow-color: $anchor-color; -$dropdownmenu-arrow-size: 6px; -$dropdownmenu-min-width: 200px; -$dropdownmenu-background: $white; -$dropdownmenu-border: 1px solid $medium-gray; - -// 19. Forms -// --------- - -$fieldset-border: 1px solid $light-gray; -$fieldset-padding: $space-two; -$fieldset-margin: $space-one $zero; -$legend-padding: rem-calc(0 3); -$form-spacing: $space-normal; -$helptext-color: $color-body; -$helptext-font-size: $font-size-small; -$helptext-font-style: italic; -$input-prefix-color: $color-body; -$input-prefix-background: var(--b-100); -$input-prefix-border: 1px solid $color-border; -$input-prefix-padding: 0.625rem; -$form-label-color: $color-body; -$form-label-font-size: rem-calc(14); -$form-label-font-weight: $font-weight-medium; -$form-label-line-height: 1.8; -$select-background: $white; -$select-triangle-color: $dark-gray; -$select-radius: var(--border-radius-normal); -$input-color: $color-body; -$input-placeholder-color: $light-gray; -$input-font-family: inherit; -$input-font-size: $font-size-default; -$input-font-weight: $global-weight-normal; -$input-background: $white; -$input-background-focus: $white; -$input-background-disabled: $light-gray; -$input-border: 1px solid var(--s-200); -$input-border-focus: 1px solid lighten($primary-color, 15%); -$input-shadow: 0; -$input-shadow-focus: 0; -$input-cursor-disabled: not-allowed; -$input-transition: border-color 0.25s ease-in-out; -$input-number-spinners: true; -$input-radius: var(--border-radius-normal); -$form-button-radius: var(--border-radius-normal); - -// 20. Label -// --------- - -$label-background: $white; -$label-color: $black; -$label-color-alt: $black; -$label-palette: $foundation-palette; -$label-font-size: $font-size-mini; -$label-padding: $space-smaller $space-small; -$label-radius: var(--border-radius-small); - -// 21. Media Object -// ---------------- - -$mediaobject-margin-bottom: $global-margin; -$mediaobject-section-padding: $global-padding; -$mediaobject-image-width-stacked: 100%; - -// 22. Menu -// -------- - -$menu-margin: 0; -$menu-margin-nested: $space-medium; -$menu-item-padding: $space-slab; -$menu-item-color-active: $white; -$menu-item-background-active: $color-background; -$menu-icon-spacing: 0.15625rem; -$menu-item-background-hover: $light-gray; -$menu-border: $light-gray; - -// 23. Meter -// --------- - -$meter-height: 0.625rem; -$meter-radius: $global-radius; -$meter-background: $medium-gray; -$meter-fill-good: $success-color; -$meter-fill-medium: $warning-color; -$meter-fill-bad: $alert-color; - -// 24. Off-canvas -// -------------- - -$offcanvas-sizes: (small: 14.375, - medium: 14.375, -); -$offcanvas-vertical-sizes: (small: 14.375, - medium: 14.375, -); -$offcanvas-background: $light-gray; -$offcanvas-shadow: 0 0 10px rgba($black, 0.7); -$offcanvas-push-zindex: 1; -$offcanvas-overlap-zindex: 10; -$offcanvas-reveal-zindex: 1; -$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'; - -// 25. Orbit -// --------- - -$orbit-bullet-background: $medium-gray; -$orbit-bullet-background-active: $dark-gray; -$orbit-bullet-diameter: 0.75rem; -$orbit-bullet-margin: 0.0625rem; -$orbit-bullet-margin-top: 0.5rem; -$orbit-bullet-margin-bottom: 0.5rem; -$orbit-caption-background: rgba($black, 0.5); -$orbit-caption-padding: 0.625rem; -$orbit-control-background-hover: rgba($black, 0.5); -$orbit-control-padding: 0.625rem; -$orbit-control-zindex: 10; - -// 26. Pagination -// -------------- - -$pagination-font-size: rem-calc(14); -$pagination-margin-bottom: $global-margin; -$pagination-item-color: $black; -$pagination-item-padding: rem-calc(3 10); -$pagination-item-spacing: rem-calc(1); -$pagination-radius: $global-radius; -$pagination-item-background-hover: $light-gray; -$pagination-item-background-current: $primary-color; -$pagination-item-color-current: $white; -$pagination-item-color-disabled: $medium-gray; -$pagination-ellipsis-color: $black; -$pagination-mobile-items: false; -$pagination-mobile-current-item: false; -$pagination-arrows: true; - -// 27. Progress Bar -// ---------------- - -$progress-height: 0.625rem; -$progress-background: $medium-gray; -$progress-margin-bottom: $global-margin; -$progress-meter-background: $primary-color; -$progress-radius: $global-radius; - -// 28. Responsive Embed -// -------------------- - -$responsive-embed-margin-bottom: rem-calc(16); -$responsive-embed-ratios: (default: 4 by 3, - widescreen: 16 by 9); - -// 29. Reveal -// ---------- - -$reveal-background: $white; -$reveal-width: 600px; -$reveal-max-width: $global-width; -$reveal-padding: $global-padding; -$reveal-border: 1px solid $medium-gray; -$reveal-radius: $global-radius; -$reveal-zindex: 1005; -$reveal-overlay-background: rgba($black, 0.45); - -// 30. Slider -// ---------- - -$slider-width-vertical: 0.3125rem; -$slider-transition: all 0.2s ease-in-out; -$slider-height: 0.3125rem; -$slider-background: $light-gray; -$slider-fill-background: $medium-gray; -$slider-handle-height: 0.875rem; -$slider-handle-width: 0.875rem; -$slider-handle-background: $primary-color; -$slider-opacity-disabled: 0.25; -$slider-radius: $global-radius; - -// 31. Switch -// ---------- - -$switch-background: $light-gray; -$switch-background-active: $primary-color; -$switch-height: $space-two; -$switch-height-tiny: $space-slab; -$switch-height-small: $space-normal; -$switch-height-large: $space-large; -$switch-radius: $space-large; -$switch-margin: $global-margin; -$switch-paddle-background: $white; -$switch-paddle-offset: $space-micro; -$switch-paddle-radius: $space-large; -$switch-paddle-transition: all 0.15s ease-out; - -// 32. Table -// --------- - -$table-background: transparent; -$table-color-scale: 5%; -$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-is-striped: false; -$table-striped-background: smart-scale($table-background, $table-color-scale); -$table-stripe: even; -$table-head-background: smart-scale($table-background, $table-color-scale / 2); -$table-head-row-hover: darken($table-head-background, $table-hover-scale); -$table-foot-background: smart-scale($table-background, $table-color-scale); -$table-foot-row-hover: darken($table-foot-background, $table-hover-scale); -$table-head-font-color: $body-font-color; -$table-foot-font-color: $body-font-color; -$show-header-for-stacked: false; - -// 33. Tabs -// -------- - -$tab-margin: 0; - -$tab-background: transparent; -$tab-background-active: transparent; -$tab-item-font-size: $font-size-small; -$tab-item-background-hover: transparent; -$tab-item-padding: $space-one $zero; -$tab-color: $primary-color; -$tab-active-color: $primary-color; -$tab-expand-max: 6; -$tab-content-background: transparent; -$tab-content-border: transparent; -$tab-content-color: foreground($tab-background, $primary-color); -$tab-content-padding: 0.625rem; - -// 34. Thumbnail -// ------------- - -$thumbnail-border: solid 4px $white; -$thumbnail-margin-bottom: $global-margin; -$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); -$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); -$thumbnail-transition: box-shadow 200ms ease-out; -$thumbnail-radius: $global-radius; - -// 35. Title Bar -// ------------- - -$titlebar-background: $black; -$titlebar-color: $white; -$titlebar-padding: 0.3125rem; -$titlebar-text-font-weight: bold; -$titlebar-icon-color: $white; -$titlebar-icon-color-hover: $medium-gray; -$titlebar-icon-spacing: 0.15625rem; - -// 36. Tooltip -// ----------- - -$has-tip-font-weight: $global-weight-bold; -$has-tip-border-bottom: dotted 1px $dark-gray; -$tooltip-background-color: $black; -$tooltip-color: $white; -$tooltip-padding: 0.46875rem; -$tooltip-font-size: $font-size-mini; -$tooltip-pip-width: 0.46875rem; -$tooltip-pip-height: $tooltip-pip-width * 0.866; -$tooltip-radius: $global-radius; - -// 37. Top Bar -// ----------- - -$topbar-padding: 0.3125; -$topbar-background: $light-gray; -$topbar-submenu-background: $topbar-background; -$topbar-title-spacing: 0.3125 0.625rem 0.3125 0; -$topbar-input-width: 200px; -$topbar-unstack-breakpoint: medium; - - -// Internal variable that contains the flex justifying options -$-zf-flex-justify: -zf-flex-justify($global-text-direction); - -$menu-items-padding: $space-one; -$xy-grid: false; diff --git a/app/javascript/dashboard/assets/scss/_helper-classes.scss b/app/javascript/dashboard/assets/scss/_helper-classes.scss index 0b400c4e9..48ee1918b 100644 --- a/app/javascript/dashboard/assets/scss/_helper-classes.scss +++ b/app/javascript/dashboard/assets/scss/_helper-classes.scss @@ -1,64 +1,22 @@ -.bg-light { - @apply bg-slate-25 dark:bg-slate-800; -} - -.flex-center { - @include flex-align(center, middle); - display: flex; -} - -.bottom-space-fix { - margin-bottom: auto; -} - -.full-height { - @include full-height(); -} - +// loader class .spinner { @include color-spinner(); - display: inline-block; - height: $space-medium; - padding: $zero $space-medium; - position: relative; - vertical-align: middle; - width: $space-medium; + @apply inline-block h-6 py-0 px-6 relative align-middle w-6; &.message { @include normal-shadow; - background: $color-white; - border-radius: $space-large; - left: 0; - margin: $space-slab auto; - padding: $space-normal; - top: 0; + @apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0; &::before { - margin-left: -$space-slab; - margin-top: -$space-slab; + @apply -ml-3 -mt-3; } } &.small { - height: $space-normal; - width: $space-normal; + @apply h-4 w-4; &::before { - height: $space-normal; - margin-top: -$space-small; - width: $space-normal; + @apply h-4 -mt-2 w-4; } } } - -.justify-space-between { - justify-content: space-between; -} - -.w-full { - width: 100%; -} - -.h-full { - height: 100%; -} diff --git a/app/javascript/dashboard/assets/scss/_layout.scss b/app/javascript/dashboard/assets/scss/_layout.scss index 594a14d95..b9c8a9bf1 100644 --- a/app/javascript/dashboard/assets/scss/_layout.scss +++ b/app/javascript/dashboard/assets/scss/_layout.scss @@ -1,5 +1,19 @@ +// scss-lint:disable SpaceAfterPropertyColon +@import 'shared/assets/fonts/inter'; +// Inter, html, body { + font-family: + 'PlusJakarta', + -apple-system, + system-ui, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Tahoma, + Arial, + sans-serif !important; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; height: 100%; diff --git a/app/javascript/dashboard/assets/scss/_rtl.scss b/app/javascript/dashboard/assets/scss/_rtl.scss index 6f16a2af4..36679fc60 100644 --- a/app/javascript/dashboard/assets/scss/_rtl.scss +++ b/app/javascript/dashboard/assets/scss/_rtl.scss @@ -131,37 +131,12 @@ } } - .search-header--wrap { - .search--input { - text-align: right; - } - - .layout-switch__container { - transform: rotate(180deg); - } - } - // Basic filter dropdown .basic-filter { left: 0; right: unset; } - // Card label - .label-container { - .label { - margin-left: var(--space-smaller); - margin-right: 0; - } - } - - // Secondary sidebar toggle button - .toggle-sidebar { - margin-left: 0; - margin-right: var(--space-minus-small); - transform: rotate(180deg); - } - // Bulk actions .bulk-action__container { .triangle { @@ -202,22 +177,6 @@ } } - // Notification panel - .notification-wrap { - left: 0; - right: var(--space-jumbo); - - .action-button { - margin-left: var(--space-small); - margin-right: 0; - } - - .notification-content--wrap { - margin-left: 0; - margin-right: var(--space-small); - } - } - // Help center .article-container .row--article-block { td:last-child { @@ -324,10 +283,6 @@ // Other changes - .account-selector--wrap { - direction: initial; - } - .colorpicker--chrome { direction: initial; } @@ -347,9 +302,4 @@ .contact--form .input-group { direction: initial; } - - // scss-lint:disable QualifyingElement - .dropdown-menu--header > span.title { - text-align: right; - } } diff --git a/app/javascript/dashboard/assets/scss/_typography.scss b/app/javascript/dashboard/assets/scss/_typography.scss deleted file mode 100644 index ee35a9cf0..000000000 --- a/app/javascript/dashboard/assets/scss/_typography.scss +++ /dev/null @@ -1,33 +0,0 @@ -.page-title { - font-size: $font-size-big; -} - -.page-sub-title { - font-size: $font-size-large; - word-wrap: break-word; -} - -.block-title { - font-size: $font-size-medium; -} - -.sub-block-title { - font-size: $font-size-default; -} - -.text-block-title { - font-size: $font-size-small; -} - -.text-muted { - color: var(--s-300); -} - -a { - font-size: $font-size-small; -} - -p { - font-size: $font-size-small; - word-spacing: .12em; -} diff --git a/app/javascript/dashboard/assets/scss/_utility-helpers.scss b/app/javascript/dashboard/assets/scss/_utility-helpers.scss deleted file mode 100644 index 99f0fe3eb..000000000 --- a/app/javascript/dashboard/assets/scss/_utility-helpers.scss +++ /dev/null @@ -1,73 +0,0 @@ -.margin-bottom-small { - margin-bottom: var(--space-small); -} - -.margin-right-smaller { - margin-right: var(--space-smaller); -} - -.margin-left-minus-slab { - margin-left: var(--space-minus-slab); -} - -.margin-right-minus-slab { - margin-right: var(--space-minus-slab); -} - -.fs-small { - font-size: var(--font-size-small); -} - -.fs-default { - font-size: var(--font-size-default); -} - -.fw-medium { - font-weight: var(--font-weight-medium); -} - -.p-normal { - padding: var(--space-normal); -} - -.overflow-scroll { - overflow: scroll; -} - -.overflow-auto { - overflow: auto; -} - -.overflow-hidden { - overflow: hidden; -} - -.border-right { - @apply border-r border-slate-50 dark:border-slate-700; -} - -.border-left { - border-left: 1px solid var(--color-border); -} - -.text-ellipsis { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.flex-between { - align-items: center; - display: flex; - justify-content: space-between; -} - -.flex-end { - display: flex; - justify-content: end; -} - -.flex-align-center { - align-items: center; - display: flex; -} diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 2773ced6f..4b33d5b10 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -15,63 +15,372 @@ @import 'variables'; @import 'mixins'; -@import 'foundation-settings'; @import 'helper-classes'; @import 'formulate'; @import 'date-picker'; -@import 'foundation-sites/scss/foundation'; - -@include foundation-everything($flex: true); - -@include foundation-prototype-text-utilities; -@include foundation-prototype-text-transformation; -@include foundation-prototype-text-decoration; -@include foundation-prototype-font-styling; -@include foundation-prototype-list-style-type; -@include foundation-prototype-rounded; -@include foundation-prototype-bordered; -@include foundation-prototype-shadow; -@include foundation-prototype-separator; -@include foundation-prototype-overflow; -@include foundation-prototype-display; -@include foundation-prototype-position; -@include foundation-prototype-border-box; -@include foundation-prototype-border-none; -@include foundation-prototype-sizing; -@include foundation-prototype-spacing; - -@import 'typography'; @import 'layout'; @import 'animations'; -@import 'foundation-custom'; @import 'rtl'; +@import 'widgets/base'; @import 'widgets/buttons'; -@import 'widgets/conv-header'; -@import 'widgets/conversation-card'; @import 'widgets/conversation-view'; -@import 'widgets/forms'; -@import 'widgets/login'; -@import 'widgets/modal'; -@import 'widgets/reply-box'; -@import 'widgets/report'; -@import 'widgets/search-box'; -@import 'widgets/sidemenu'; -@import 'widgets/snackbar'; -@import 'widgets/states'; -@import 'widgets/status-bar'; @import 'widgets/tabs'; @import 'widgets/woot-tables'; -@import 'views/settings/inbox'; -@import 'views/settings/integrations'; - @import 'plugins/multiselect'; @import 'plugins/dropdown'; @import '~shared/assets/stylesheets/ionicons'; -@import 'utility-helpers'; .tooltip { @apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900; } + +.hide { + @apply hidden; +} + +@layer base { + // scss-lint:disable PropertySortOrder + :root { + --color-amber-25: 254 253 251; + --color-amber-50: 255 249 237; + --color-amber-75: 255 243 208; + --color-amber-100: 255 236 183; + --color-amber-200: 255 224 161; + --color-amber-300: 245 208 140; + --color-amber-400: 228 187 120; + --color-amber-500: 214 163 92; + --color-amber-600: 214 163 92; + --color-amber-700: 255 186 26; + --color-amber-800: 145 89 48; + --color-amber-900: 79 52 34; + + --color-ash-100: 235 235 239; + --color-ash-200: 228 228 233; + --color-ash-25: 252 252 253; + --color-ash-300: 221 221 227; + --color-ash-400: 211 212 219; + --color-ash-50: 249 249 251; + --color-ash-500: 185 187 198; + --color-ash-600: 139 141 152; + --color-ash-700: 126 128 138; + --color-ash-75: 242 242 245; + --color-ash-800: 96 100 108; + --color-ash-900: 28 32 36; + + --color-primary-25: 251 253 255; + --color-primary-50: 245 249 255; + --color-primary-75: 233 243 255; + --color-primary-100: 218 236 255; + --color-primary-200: 201 226 255; + --color-primary-300: 181 213 255; + --color-primary-400: 155 195 252; + --color-primary-500: 117 171 247; + --color-primary-600: 39 129 246; + --color-primary-700: 16 115 233; + --color-primary-800: 8 109 224; + --color-primary-900: 11 50 101; + + --color-ruby-100: 255 220 225; + --color-ruby-200: 255 206 214; + --color-ruby-25: 255 252 253; + --color-ruby-300: 248 191 200; + --color-ruby-400: 239 172 184; + --color-ruby-50: 255 247 248; + --color-ruby-500: 229 146 163; + --color-ruby-600: 229 70 102; + --color-ruby-700: 220 59 93; + --color-ruby-75: 254 234 237; + --color-ruby-800: 202 36 77; + --color-ruby-900: 100 23 43; + + --color-teal-100: 224 248 243; + --color-teal-200: 204 243 234; + --color-teal-25: 250 254 253; + --color-teal-300: 184 234 224; + --color-teal-400: 161 222 210; + --color-teal-50: 243 251 249; + --color-teal-500: 83 185 171; + --color-teal-600: 18 165 148; + --color-teal-700: 13 155 138; + --color-teal-75: 236 249 255; + --color-teal-800: 0 133 115; + --color-teal-900: 13 61 56; + + --color-green-25: 251 254 252; + --color-green-50: 244 251 246; + --color-green-75: 230 246 235; + --color-green-100: 214 241 223; + --color-green-200: 196 232 209; + --color-green-300: 173 221 192; + --color-green-400: 142 206 170; + --color-green-500: 91 185 139; + --color-green-600: 48 164 108; + --color-green-700: 43 154 102; + --color-green-800: 33 131 88; + --color-green-900: 25 59 45; + + --color-mint-25: 249 254 253; + --color-mint-50: 242 251 249; + --color-mint-75: 221 249 242; + --color-mint-100: 200 244 233; + --color-mint-200: 179 236 222; + --color-mint-300: 156 224 208; + --color-mint-400: 126 207 189; + --color-mint-500: 76 187 165; + --color-mint-600: 134 234 212; + --color-mint-700: 125 224 203; + --color-mint-800: 2 120 100; + --color-mint-900: 22 67 60; + + --color-sky-25: 249 254 255; + --color-sky-50: 241 250 253; + --color-sky-75: 225 246 253; + --color-sky-100: 209 240 250; + --color-sky-200: 190 231 245; + --color-sky-300: 169 218 237; + --color-sky-400: 141 202 227; + --color-sky-500: 96 179 215; + --color-sky-600: 124 226 254; + --color-sky-700: 116 218 248; + --color-sky-800: 0 116 158; + --color-sky-900: 29 62 86; + + --color-indigo-25: 253 253 254; + --color-indigo-50: 247 249 255; + --color-indigo-75: 237 242 254; + --color-indigo-100: 225 233 255; + --color-indigo-200: 210 222 255; + --color-indigo-300: 193 208 255; + --color-indigo-400: 171 189 249; + --color-indigo-500: 141 164 239; + --color-indigo-600: 62 99 221; + --color-indigo-700: 51 88 212; + --color-indigo-800: 58 91 199; + --color-indigo-900: 31 45 92; + + --color-iris-25: 253 253 255; + --color-iris-50: 248 248 255; + --color-iris-75: 240 241 254; + --color-iris-100: 230 231 255; + --color-iris-200: 218 220 255; + --color-iris-300: 203 205 255; + --color-iris-400: 184 186 248; + --color-iris-500: 155 158 240; + --color-iris-600: 91 91 214; + --color-iris-700: 81 81 205; + --color-iris-800: 87 83 198; + --color-iris-900: 39 41 98; + + --color-violet-25: 253 252 254; + --color-violet-50: 250 248 255; + --color-violet-75: 244 240 254; + --color-violet-100: 235 228 255; + --color-violet-200: 225 217 255; + --color-violet-300: 212 202 254; + --color-violet-400: 194 181 245; + --color-violet-500: 170 153 236; + --color-violet-600: 110 86 207; + --color-violet-700: 101 77 196; + --color-violet-800: 101 80 185; + --color-violet-900: 47 38 95; + + --color-pink-25: 255 252 254; + --color-pink-50: 254 247 251; + --color-pink-75: 254 233 245; + --color-pink-100: 251 220 239; + --color-pink-200: 246 206 231; + --color-pink-300: 239 191 221; + --color-pink-400: 231 172 208; + --color-pink-500: 221 147 194; + --color-pink-600: 214 64 159; + --color-pink-700: 207 56 151; + --color-pink-800: 194 41 138; + --color-pink-900: 101 18 73; + + --color-orange-25: 254 252 251; + --color-orange-50: 255 247 237; + --color-orange-75: 255 239 214; + --color-orange-100: 255 223 181; + --color-orange-200: 255 209 154; + --color-orange-300: 255 193 130; + --color-orange-400: 245 174 115; + --color-orange-500: 236 148 85; + --color-orange-600: 247 107 21; + --color-orange-700: 239 95 0; + --color-orange-800: 204 78 0; + --color-orange-900: 88 45 29; + } + // scss-lint:disable QualifyingElement + body.dark { + --color-amber-25: 31 19 0; + --color-amber-50: 37 24 4; + --color-amber-75: 48 32 11; + --color-amber-100: 57 39 15; + --color-amber-200: 67 46 18; + --color-amber-300: 83 57 22; + --color-amber-400: 111 77 29; + --color-amber-500: 169 118 42; + --color-amber-600: 169 118 42; + --color-amber-700: 255 203 71; + --color-amber-800: 255 204 77; + --color-amber-900: 255 231 179; + + --color-ash-100: 46 48 53; + --color-ash-200: 53 55 60; + --color-ash-25: 24 24 26; + --color-ash-300: 60 63 68; + --color-ash-400: 70 75 80; + --color-ash-50: 27 27 31; + --color-ash-500: 90 97 101; + --color-ash-600: 105 110 119; + --color-ash-700: 120 127 133; + --color-ash-75: 39 40 45; + --color-ash-800: 173 177 184; + --color-ash-900: 237 238 240; + + --color-primary-25: 10 17 28; + --color-primary-50: 15 24 38; + --color-primary-75: 15 39 72; + --color-primary-100: 10 49 99; + --color-primary-200: 18 61 117; + --color-primary-300: 29 74 134; + --color-primary-400: 40 89 156; + --color-primary-500: 48 106 186; + --color-primary-600: 39 129 246; + --color-primary-700: 21 116 231; + --color-primary-800: 126 182 255; + --color-primary-900: 205 227 255; + + --color-ruby-100: 78 19 37; + --color-ruby-200: 94 26 46; + --color-ruby-25: 25 17 19; + --color-ruby-300: 111 37 57; + --color-ruby-400: 136 52 71; + --color-ruby-50: 30 21 23; + --color-ruby-500: 179 68 90; + --color-ruby-600: 229 70 102; + --color-ruby-700: 236 90 114; + --color-ruby-75: 58 20 30; + --color-ruby-800: 255 148 157; + --color-ruby-900: 254 210 225; + + --color-teal-100: 2 59 55; + --color-teal-200: 8 72 67; + --color-teal-25: 13 21 20; + --color-teal-300: 28 105 97; + --color-teal-400: 28 105 97; + --color-teal-50: 17 28 27; + --color-teal-500: 32 126 115; + --color-teal-600: 41 163 131; + --color-teal-700: 14 179 158; + --color-teal-75: 13 45 42; + --color-teal-800: 11 216 182; + --color-teal-900: 173 240 221; + + --color-green-25: 14 21 18; + --color-green-50: 18 27 23; + --color-green-75: 19 45 33; + --color-green-100: 17 59 41; + --color-green-200: 23 73 51; + --color-green-300: 32 87 62; + --color-green-400: 40 104 74; + --color-green-500: 47 124 87; + --color-green-600: 48 164 108; + --color-green-700: 51 176 116; + --color-green-800: 61 214 140; + --color-green-900: 177 241 203; + + --color-mint-25: 14 21 21; + --color-mint-50: 15 27 27; + --color-mint-75: 9 44 43; + --color-mint-100: 0 58 56; + --color-mint-200: 0 71 68; + --color-mint-300: 16 86 80; + --color-mint-400: 30 104 95; + --color-mint-500: 39 127 112; + --color-mint-600: 134 234 212; + --color-mint-700: 168 245 229; + --color-mint-800: 88 213 186; + --color-mint-900: 196 245 225; + + --color-sky-25: 14 21 21; + --color-sky-50: 15 27 27; + --color-sky-75: 9 44 43; + --color-sky-100: 0 58 56; + --color-sky-200: 0 71 68; + --color-sky-300: 16 86 80; + --color-sky-400: 30 104 95; + --color-sky-500: 39 127 112; + --color-sky-600: 134 234 212; + --color-sky-700: 168 245 229; + --color-sky-800: 88 213 186; + --color-sky-900: 196 245 225; + + --color-indigo-25: 17 19 31; + --color-indigo-50: 20 23 38; + --color-indigo-75: 24 36 73; + --color-indigo-100: 29 46 98; + --color-indigo-200: 37 57 116; + --color-indigo-300: 48 67 132; + --color-indigo-400: 58 79 151; + --color-indigo-500: 67 93 177; + --color-indigo-600: 62 99 221; + --color-indigo-700: 84 114 228; + --color-indigo-800: 158 177 255; + --color-indigo-900: 214 225 255; + + --color-iris-25: 19 19 30; + --color-iris-50: 23 22 37; + --color-iris-75: 32 34 72; + --color-iris-100: 38 42 101; + --color-iris-200: 48 51 116; + --color-iris-300: 61 62 130; + --color-iris-400: 74 74 149; + --color-iris-500: 89 88 177; + --color-iris-600: 91 91 214; + --color-iris-700: 110 106 222; + --color-iris-800: 177 169 255; + --color-iris-900: 224 223 254; + + --color-violet-25: 20 18 31; + --color-violet-50: 27 21 37; + --color-violet-75: 41 31 67; + --color-violet-100: 51 37 91; + --color-violet-200: 60 46 105; + --color-violet-300: 71 56 118; + --color-violet-400: 86 70 139; + --color-violet-500: 105 88 173; + --color-violet-600: 110 86 207; + --color-violet-700: 125 102 217; + --color-violet-800: 186 167 255; + --color-violet-900: 226 221 254; + + --color-pink-25: 25 17 23; + --color-pink-50: 33 18 29; + --color-pink-75: 55 23 47; + --color-pink-100: 75 20 61; + --color-pink-200: 89 28 71; + --color-pink-300: 105 41 85; + --color-pink-400: 131 56 105; + --color-pink-500: 168 72 133; + --color-pink-600: 214 64 159; + --color-pink-700: 222 81 168; + --color-pink-800: 255 141 204; + --color-pink-900: 253 209 234; + --color-orange-25: 23 18 14; + --color-orange-50: 30 22 15; + --color-orange-75: 51 30 11; + --color-orange-100: 70 33 0; + --color-orange-200: 86 40 0; + --color-orange-300: 102 53 12; + --color-orange-400: 126 69 29; + --color-orange-500: 163 88 41; + --color-orange-600: 247 107 21; + --color-orange-700: 255 128 31; + --color-orange-800: 255 160 87; + --color-orange-900: 255 224 194; + } +} diff --git a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss index 100662394..e08099ae6 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss @@ -1,46 +1,7 @@ .dropdown-pane { - @include elegant-card; - @include border-light; - box-sizing: content-box; - padding: var(--space-small); - width: fit-content; - z-index: var(--z-index-very-high); + @apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999]; &.dropdown-pane--open { - @apply bg-white dark:bg-slate-800; - display: block; - visibility: visible; - } - - &.dropdowm--bottom { - &::before { - @include arrow(top, var(--color-border-light), 14px); - position: absolute; - right: 6px; - top: -14px; - } - - &::after { - @include arrow(top, $color-white, var(--space-slab)); - position: absolute; - right: var(--space-small); - top: -12px; - } - } - - &.dropdowm--top { - &::before { - @include arrow(bottom, var(--color-border-light), 14px); - bottom: -14px; - position: absolute; - right: 6px; - } - - &::after { - @include arrow(bottom, $color-white, var(--space-slab)); - bottom: -12px; - position: absolute; - right: var(--space-small); - } + @apply bg-white absolute dark:bg-slate-800 block visible; } } diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 0fddae3d1..99b006045 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -97,6 +97,10 @@ .multiselect__tags { @apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0; + + input { + @apply border-0 border-none; + } } .multiselect__tags-wrap { @@ -149,6 +153,28 @@ } .multiselect-wrap--small { + // To be removed one SLA reports date picker is created + &.tiny { + .multiselect.no-margin { + @apply min-h-[32px]; + } + + .multiselect__select { + @apply min-h-[32px] h-8; + + &::before { + @apply top-[60%]; + } + } + + .multiselect__tags { + @apply min-h-[32px] max-h-[32px]; + + .multiselect__single { + @apply pt-1 pb-1; + } + } + } .multiselect__tags, .multiselect__input, @@ -180,7 +206,6 @@ .multiselect--disabled .multiselect__select { @apply bg-transparent; } - } .multiselect-wrap--medium { diff --git a/app/javascript/dashboard/assets/scss/storybook.scss b/app/javascript/dashboard/assets/scss/storybook.scss index 457560aa9..fa2455918 100644 --- a/app/javascript/dashboard/assets/scss/storybook.scss +++ b/app/javascript/dashboard/assets/scss/storybook.scss @@ -13,20 +13,14 @@ @import '~shared/assets/stylesheets/ionicons'; @import 'mixins'; -@import 'foundation-settings'; @import 'helper-classes'; -@import 'foundation-sites/scss/foundation'; - -@include foundation-prototype-spacing; -@include foundation-everything($flex: true); @import 'typography'; @import 'layout'; @import 'animations'; -@import 'foundation-custom'; @import 'widgets/buttons'; -@import 'widgets/forms'; +@import 'widgets/base'; @import 'plugins/multiselect'; @@ -36,7 +30,6 @@ @import 'tailwindcss/utilities'; @import 'widget/assets/scss/utilities'; - html, body { font-family: 'PlusJakarta', sans-serif; diff --git a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss index 39e4ed2bd..8a118b906 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss @@ -1,112 +1 @@ -.settings { - @apply overflow-auto; -} - -.wizard-box { - .item { - @apply cursor-pointer py-4 pr-4 pl-6 relative; - - &::before, - &::after { - @apply bg-slate-75 dark:bg-slate-600 content-[''] h-full absolute top-5 w-0.5; - } - - &::before { - @apply h-4 top-0; - } - - &:first-child { - &::before { - @apply h-0; - } - } - - &:last-child { - &::after { - @apply h-0; - } - } - - &.active { - h3 { - @apply text-woot-500 dark:text-woot-500; - } - - .step { - @apply bg-woot-500 dark:bg-woot-500; - } - } - - &.over { - &::after { - @apply bg-woot-500 dark:bg-woot-500; - } - - .step { - @apply bg-woot-500 dark:bg-woot-500; - } - - & + .item { - &::before { - @apply bg-woot-500 dark:bg-woot-500; - } - } - } - - h3 { - @apply text-slate-800 dark:text-slate-100 text-base pl-6; - } - - .completed { - @apply text-green-500 dark:text-green-500 ml-1; - } - - p { - @apply text-slate-600 dark:text-slate-300 text-sm m-0 pl-6; - } - - .step { - @apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-[999] absolute text-center text-white dark:text-white text-xxs top-5; - - i { - @apply text-xxs; - } - } - } -} - -.wizard-body { - @apply border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6; - - &.height-auto { - @apply h-auto; - } -} - -.settings--content { - @apply my-2 mx-8; - - .title { - @apply font-medium; - } - - .code { - @apply bg-slate-50 dark:bg-slate-800 overflow-auto p-2.5 whitespace-nowrap; - - code { - @apply bg-transparent border-0; - } - } -} - -.login-init { - @apply pt-[30%] text-center; - - p { - @apply p-6; - } - - > a > img { - @apply w-60; - } -} +// to be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss new file mode 100644 index 000000000..d5d2e227f --- /dev/null +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -0,0 +1,145 @@ +// scss-lint:disable QualifyingElement + +// Base typography +h1, +h2, +h3, +h4, +h5, +h6 { + @apply font-medium text-slate-800 dark:text-slate-50; +} + +p { + text-rendering: optimizeLegibility; + + @apply mb-2 leading-[1.65] text-sm; + + a { + @apply text-woot-500 dark:text-woot-500 cursor-pointer; + } +} + +a { + @apply text-sm; +} + +hr { + @apply clear-both max-w-full h-0 my-5 mx-0 border-slate-300 dark:border-slate-600; +} + +ul, +ol, +dl { + @apply mb-2 list-disc list-outside leading-[1.65]; +} + +// Form elements +label { + @apply text-slate-800 dark:text-slate-200 block m-0 leading-7 text-sm font-medium; + + &.error { + input { + @apply mb-1; + } + } +} + +.input-wrap, +.help-text { + @apply text-slate-800 dark:text-slate-100 text-sm font-medium; + + .help-text { + @apply font-normal text-slate-600 dark:text-slate-400; + } +} + +// Focus outline removal +.button, +textarea, +input:focus { + outline: none; +} + +// Inputs +input[type='text']:not(.reset-base), +input[type='number']:not(.reset-base), +input[type='password']:not(.reset-base), +input[type='date']:not(.reset-base), +input[type='email']:not(.reset-base), +input[type='url']:not(.reset-base) { + @apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; + + &[disabled] { + @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed; + } +} + +input[type='file'] { + @apply bg-white dark:bg-slate-800 leading-[1.15] mb-4; +} + +// Select +select { + background-image: url("data:image/svg+xml;utf8,"); + background-position: right -1rem center; + background-size: 9px 6px; + @apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2 rounded-md w-full text-base font-normal appearance-none transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; +} + +// Textarea +textarea { + @apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-16 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600; + + &[disabled] { + @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed; + } +} + +// Error handling +.has-multi-select-error { + div.multiselect { + @apply mb-1; + } +} + +.error { + input, + input:not([type]), + textarea, + select, + .multiselect > .multiselect__tags, + .multiselect:not(.no-margin) { + @apply border border-solid border-red-400 dark:border-red-400 mb-1; + } + + .message { + @apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full; + } +} + +.input-group.small { + input { + @apply text-sm h-8; + } + + .error { + @apply border-red-400 dark:border-red-400; + } +} + +// Code styling +code { + font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', + '"Liberation Mono"', '"Courier New"', 'monospace'; + @apply text-xs border-0; + + &.hljs { + @apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50 rounded-lg p-5; + + .hljs-number, + .hljs-string { + @apply text-red-800 dark:text-red-400; + } + } +} diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 1b6166a18..587b810ca 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -1,8 +1,40 @@ +// scss-lint:disable SpaceAfterPropertyColon +// scss-lint:disable MergeableSelector +button { + font-family: inherit; + transition: + background-color 0.25s ease-out, + color 0.25s ease-out; + @apply inline-block items-center mb-0 text-center align-middle cursor-pointer text-sm mt-0 mx-0 py-1 px-2.5 border border-solid border-transparent dark:border-transparent rounded-[0.3125rem]; + + &:disabled, + &.disabled { + @apply opacity-40 cursor-not-allowed; + } +} + +.button-group { + @apply mb-0 flex flex-nowrap items-stretch; + + .button { + flex: 0 0 auto; + @apply m-0 text-sm rounded-none first:rounded-tl-[0.3125rem] first:rounded-bl-[0.3125rem] last:rounded-tr-[0.3125rem] last:rounded-br-[0.3125rem] rtl:space-x-reverse; + } + + .button--only-icon { + @apply w-10 justify-center pl-0 pr-0; + } +} + +.back-button { + @apply m-0; +} + .button { - @apply items-center inline-flex h-10 mb-0 gap-2; + @apply items-center bg-woot-500 dark:bg-woot-500 px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium; .button__content { - @apply w-full; + @apply w-full whitespace-nowrap overflow-hidden text-ellipsis; img, svg { @@ -10,12 +42,61 @@ } } + &:hover { + @apply bg-woot-600 dark:bg-woot-600; + } + + &:disabled, + &.disabled { + @apply opacity-40 cursor-not-allowed; + } + + &.success { + @apply bg-[#44ce4b] dark:bg-[#44ce4b] text-white dark:text-white; + } + + &.secondary { + @apply bg-slate-700 dark:bg-slate-600 text-white dark:text-white; + } + + &.primary { + @apply bg-woot-500 dark:bg-woot-500 text-white dark:text-white; + } + + &.clear { + @apply text-woot-500 dark:text-woot-500 bg-transparent dark:bg-transparent; + } + + &.alert { + @apply bg-red-500 dark:bg-red-500 text-white dark:text-white; + + &.clear { + @apply bg-transparent dark:bg-transparent; + } + } + + &.warning { + @apply bg-[#ffc532] dark:bg-[#ffc532] text-white dark:text-white; + + &.clear { + @apply bg-transparent dark:bg-transparent; + } + } + + &.tiny { + @apply h-6 text-[10px]; + } + + &.small { + @apply h-8 text-xs; + } + .spinner { @apply px-2 py-0; } // @TODDO - Remove after moving all buttons to woot-button - .icon + .button__content { + .icon+.button__content { @apply w-auto; } @@ -34,10 +115,10 @@ } &.hollow { - @apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; + @apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; &.secondary { - @apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; + @apply text-slate-700 border-slate-100 dark:border-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; } &.success { diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss deleted file mode 100644 index 04f610dd2..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ /dev/null @@ -1 +0,0 @@ -// File to be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss deleted file mode 100644 index d060dc2ee..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss +++ /dev/null @@ -1,16 +0,0 @@ -@keyframes left-shift-animation { - 0%, - 100% { - transform: translateX(0); - } - - 50% { - transform: translateX(1px); - } -} - -.conversation { - &.active { - animation: left-shift-animation 0.25s $swift-ease-out-function; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 5cc3caa27..b51cbe07d 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -79,7 +79,7 @@ @apply rounded-r-lg rounded-l mr-auto break-words; &:not(.is-unsupported) { - @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 + @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50; } &.is-image { @@ -91,7 +91,7 @@ } .file { - .text-block-title { + .attachment-name { @apply text-slate-700 dark:text-woot-300; } @@ -222,20 +222,6 @@ @apply flex relative flex-col; } -.typing-indicator-wrap { - @apply items-center flex h-0 absolute w-full -top-8; - - .typing-indicator { - @include elegant-card; - @include round-corner; - @apply py-2 pr-4 pl-5 bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-100 text-xs font-semibold my-2.5 mx-auto; - - .gif { - @apply ml-2 w-6; - } - } -} - .left .bubble .text-content { h1, h2, diff --git a/app/javascript/dashboard/assets/scss/widgets/_forms.scss b/app/javascript/dashboard/assets/scss/widgets/_forms.scss deleted file mode 100644 index 4688b648c..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_forms.scss +++ /dev/null @@ -1,78 +0,0 @@ -// scss-lint:disable QualifyingElement - -label { - @apply text-slate-800 dark:text-slate-200; -} - -textarea { - @apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600; -} - -input { - @apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600; - - &[disabled] { - @apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600; - } -} - -input[type='file'] { - @apply bg-white dark:bg-slate-800; -} - -select { - @apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600; -} - -.error { - input[type='color'], - input[type='date'], - input[type='datetime'], - input[type='datetime-local'], - input[type='email'], - input[type='month'], - input[type='number'], - input[type='password'], - input[type='search'], - input[type='tel'], - input[type='text'], - input[type='time'], - input[type='url'], - input[type='week'], - input:not([type]), - textarea, - select, - .multiselect > .multiselect__tags { - @apply border border-solid border-red-400 dark:border-red-400; - } - - .message { - @apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full; - } -} - -.button, -textarea, -input { - &:focus { - outline: none; - } -} - -.input-wrap { - @apply text-slate-800 dark:text-slate-100 text-sm font-medium; -} - -.help-text { - @apply font-normal text-slate-600 dark:text-slate-400; -} - -.input-group.small { - input { - @apply text-sm h-8; - } - - .error { - @apply border-red-400 dark:border-red-400; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_login.scss b/app/javascript/dashboard/assets/scss/widgets/_login.scss deleted file mode 100644 index a5b140ede..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_login.scss +++ /dev/null @@ -1,66 +0,0 @@ -.auth-wrap { - width: 100%; -} - -// Outside login wrapper -.login { - @include full-height; - overflow-y: auto; - padding-top: $space-larger * 1.2; - - .login__hero { - margin-bottom: $space-larger; - - .hero__logo { - width: 180px; - } - - .hero__title { - font-weight: $font-weight-light; - margin-top: $space-larger; - } - - .hero__sub { - color: $medium-gray; - font-size: $font-size-medium; - } - } - - // Login box - .login-box { - @include background-white; - @include border-normal; - @include elegant-card; - - border-radius: $space-smaller; - padding: $space-large; - - label { - color: $color-gray; - font-size: $font-size-default; - - input { - font-size: $font-size-default; - height: $space-larger; - padding: $space-slab; - } - - .error { - font-size: $font-size-small; - } - } - - .button { - height: $space-larger; - } - } - - .sigin__footer { - font-size: $font-size-default; - padding: $space-medium; - - > a { - font-weight: $font-weight-bold; - } - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss deleted file mode 100644 index 4a3ca61c1..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ /dev/null @@ -1,75 +0,0 @@ -.modal-mask { - // @include flex; - // @include flex-align(center, middle); - @apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full; -} - -.page-top-bar { - @apply px-8 pt-9 pb-0; - - img { - @apply max-h-[3.75rem]; - } -} - -.modal-container { - @apply shadow-md rounded-sm max-h-full overflow-auto relative w-[37.5rem]; - - &.medium { - @apply max-w-[80%] w-[56.25rem]; - } - - .content-box { - @apply h-auto p-0; - } - - h2 { - @apply text-slate-800 dark:text-slate-100 text-lg font-semibold; - } - - p { - @apply text-sm m-0 p-0 text-slate-600 mt-2 text-sm dark:text-slate-300; - } - - .content { - @apply p-8; - } - - form, - .modal-content { - @apply pt-4 pb-8 px-8 self-center; - - a { - @apply p-4; - } - } - - .modal-footer { - // @include flex; - // @include flex-align($x: flex-end, $y: middle); - @apply flex justify-end items-center py-2 px-0 gap-2; - - &.justify-content-end { - @apply justify-end; - } - } - - .delete-item { - @apply p-8; - - button { - @apply m-0; - } - } -} - -.modal-enter, -.modal-leave { - @apply opacity-0; -} - -.modal-enter .modal-container, -.modal-leave .modal-container { - transform: scale(1.1); - // @apply transform scale-110; -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss deleted file mode 100644 index d15150f36..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss +++ /dev/null @@ -1,60 +0,0 @@ -.reply-box { - transition: box-shadow 0.35s $swift-ease-out-function, - height 2s $swift-ease-out-function; - - &.is-focused { - box-shadow: var(--shadow); - } - - .reply-box__top { - .icon { - color: $medium-gray; - cursor: pointer; - font-size: $font-size-medium; - margin-right: $space-small; - - &.active { - color: $color-woot; - } - } - - .attachment { - cursor: pointer; - margin-right: $space-one; - padding: 0 $space-small; - } - - .video-js { - background: transparent; - // Override min-height : 50px in foundation - // - max-height: $space-mega * 2.4; - min-height: 3rem; - padding: var(--space-normal) 0 0; - resize: none; - } - - > textarea { - @include ghost-input(); - background: transparent; - margin: 0; - max-height: $space-mega * 2.4; - // Override min-height : 50px in foundation - min-height: 3rem; - padding: var(--space-normal) 0 0; - resize: none; - } - } - - &.is-private { - @apply bg-yellow-100 dark:bg-yellow-800; - - .reply-box__top { - @apply bg-yellow-100 dark:bg-yellow-800; - - > input { - @apply bg-yellow-100 dark:bg-yellow-800; - } - } - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss deleted file mode 100644 index d07f1dd3d..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ /dev/null @@ -1,60 +0,0 @@ -.report-card { - @include custom-border-top(3px, transparent); - - cursor: pointer; - margin: 0; - padding: var(--space-normal); - - &.active { - @include custom-border-top(3px, var(--color-woot)); - @include background-white; - .heading, - .metric { - color: var(--color-woot); - } - } - - .heading { - align-items: center; - color: var(--color-heading); - display: flex; - font-size: var(--font-size-small); - font-weight: var(--font-weight-bold); - margin: 0; - } - - .info-icon { - color: var(--b-400); - margin-left: var(--space-micro); - } - - .metric-wrap { - align-items: center; - display: flex; - } - - .metric { - font-size: var(--font-size-big); - font-weight: var(--font-weight-feather); - margin-top: var(--space-smaller); - } - - .metric-trend { - font-size: var(--font-size-small); - margin: 0 var(--space-small); - } - - .metric-up { - color: $success-color; - } - - .metric-down { - color: $alert-color; - } - - .desc { - font-size: var(--font-size-small); - margin: 0; - text-transform: capitalize; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_reports.scss b/app/javascript/dashboard/assets/scss/widgets/_reports.scss deleted file mode 100644 index dcd65366a..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_reports.scss +++ /dev/null @@ -1,29 +0,0 @@ -.reports-option__rounded--item { - border-radius: 100%; - height: var(--space-two); - width: var(--space-two); -} - -.reports-option__item { - flex-shrink: 0; - margin-right: var(--space-small); -} - -.reports-option__label--swatch { - border: 1px solid var(--color-border); -} - -.reports-option__wrap { - align-items: center; - display: flex; -} - -.reports-option__title { - margin: 0 var(--space-small); -} - - -.switch { - margin-bottom: var(--space-zero); - margin-left: var(--space-small); -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_search-box.scss b/app/javascript/dashboard/assets/scss/widgets/_search-box.scss deleted file mode 100644 index 643757e60..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_search-box.scss +++ /dev/null @@ -1,18 +0,0 @@ -.search { - @include flex; - @include flex-align($x: left, $y: middle); - @include flex-shrink; - - padding: $space-one $space-normal; - transition: all 0.3s var(--ease-in-out-quad); - - > .icon { - color: $medium-gray; - font-size: $font-size-medium; - } - - > input { - @include ghost-input(); - margin: 0; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss deleted file mode 100644 index 3de072a77..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss +++ /dev/null @@ -1,78 +0,0 @@ -.side-menu { - i { - margin-right: var(--space-smaller); - min-width: var(--space-two); - } -} - -.sidebar { - z-index: 1024 - 1; - - //logo - .logo { - img { - max-height: 108px; - padding: $woot-logo-padding; - } - } - - .nested { - a { - font-size: var(--font-size-small); - margin-bottom: var(--space-micro); - margin-top: var(--space-micro); - - .inbox-icon { - display: inline-block; - margin-right: var(--space-micro); - min-width: var(--space-normal); - text-align: center; - } - } - } -} - -// bottom-nav -.bottom-nav { - @include flex; - @include space-between-column; - @include border-normal-top; - flex-direction: column; - padding: var(--space-one) var(--space-normal) var(--space-one) - var(--space-one); - position: relative; - - &:hover { - background: var(--color-background-light); - } - - .dropdown-pane { - bottom: 3.75rem; - display: block; - visibility: visible; - width: fit-content; - } - - .active { - border-bottom: 2px solid $medium-gray; - } -} - -.hamburger--menu { - cursor: pointer; - display: block; - margin-right: var(--space-normal); -} - -.header--icon { - display: block; - margin: 0 var(--space-small) 0 var(--space-smaller); - - @media screen and (max-width: 1200px) { - display: none; - } -} - -.header-title { - margin: 0 var(--space-small); -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss b/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss deleted file mode 100644 index ee556682f..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss +++ /dev/null @@ -1,45 +0,0 @@ -.ui-snackbar-container { - left: 0; - margin: 0 auto; - max-width: 25rem; - overflow: hidden; - position: absolute; - right: 0; - text-align: center; - top: $space-normal; - z-index: 9999; -} - -.ui-snackbar { - @include shadow; - background-color: $woot-snackbar-bg; - border-radius: $space-smaller; - display: inline-flex; - margin-bottom: $space-small; - max-width: 25rem; - min-height: 1.875rem; - min-width: 15rem; - padding: $space-slab $space-medium; - text-align: left; -} - -.ui-snackbar-text { - color: $color-white; - font-size: $font-size-small; - font-weight: $font-weight-medium; -} - -.ui-snackbar-action { - margin-left: auto; - padding-left: 1.875rem; - - button { - background: none; - border: 0; - color: $woot-snackbar-button; - font-size: $font-size-small; - margin: 0; - padding: 0; - text-transform: uppercase; - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_states.scss b/app/javascript/dashboard/assets/scss/widgets/_states.scss deleted file mode 100644 index 7eb840221..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_states.scss +++ /dev/null @@ -1 +0,0 @@ -// To be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_status-bar.scss b/app/javascript/dashboard/assets/scss/widgets/_status-bar.scss deleted file mode 100644 index ad0f805e2..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_status-bar.scss +++ /dev/null @@ -1,46 +0,0 @@ -.status-bar { - @include flex; - @include flex-align($x: center, $y: middle); - background: lighten($warning-color, 36%); - flex-direction: column; - margin: 0; - padding: $space-normal $space-smaller; - - .message { - font-weight: $font-weight-medium; - margin-bottom: $zero; - } - - .button { - margin: $space-smaller $zero $zero; - padding: $space-small $space-normal; - } - - &.danger { - background: lighten($alert-color, 30%); - - .button { - // Default and disabled states - &, - &.disabled, - &[disabled], - &.disabled:hover, - &[disabled]:hover, - &.disabled:focus, - &[disabled]:focus { - background-color: $alert-color; - color: $color-white; - } - - &:hover, - &:focus { - background-color: darken($alert-color, 7%); - color: $color-white; - } - } - } - - &.warning { - background: lighten($warning-color, 36%); - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index 35a9ee9ed..b9999fac9 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -7,7 +7,7 @@ } .tabs { - @apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4; + @apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4 list-none mb-0; } .tabs--with-scroll { @@ -31,7 +31,7 @@ } .tabs-title { - @apply flex-shrink-0 my-0 mx-2 ; + @apply flex-shrink-0 my-0 mx-2; .badge { @apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0; @@ -53,7 +53,7 @@ } a { - @apply flex items-center flex-row border-b border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative; + @apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative; transition: border-color 0.15s $swift-ease-out-function; } diff --git a/app/javascript/dashboard/assets/scss/widgets/_widget_builder.scss b/app/javascript/dashboard/assets/scss/widgets/_widget_builder.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss index e7a1adbca..97535bf0d 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss @@ -1,9 +1,9 @@ table { - @apply border-spacing-0 text-sm; + @apply border-spacing-0 text-sm w-full; thead { th { - @apply font-semibold tracking-[1px] text-left uppercase text-slate-900 dark:text-slate-200; + @apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200; } } diff --git a/app/javascript/dashboard/components/Accordion/AccordionItem.vue b/app/javascript/dashboard/components/Accordion/AccordionItem.vue index a8f7a0e3a..87370185d 100644 --- a/app/javascript/dashboard/components/Accordion/AccordionItem.vue +++ b/app/javascript/dashboard/components/Accordion/AccordionItem.vue @@ -1,7 +1,7 @@