diff --git a/.circleci/config.yml b/.circleci/config.yml index f758b5492..e2c6dc583 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ defaults: &defaults # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: cimg/postgres:14.1 + - image: cimg/postgres:15.3 - image: cimg/redis:6.2.6 environment: - RAILS_LOG_TO_STDOUT: false @@ -73,7 +73,7 @@ jobs: - run: name: yarn - command: yarn install --cache-folder ~/.cache/yarn + command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn # Store yarn / webpacker cache - save_cache: @@ -104,9 +104,8 @@ jobs: fi curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json - + # Database setup - - run: yarn install --check-files - run: bundle exec rake db:create - run: bundle exec rake db:schema:load @@ -117,7 +116,7 @@ jobs: - run: name: Rubocop command: bundle exec rubocop - + # - run: # name: Brakeman # command: bundle exec brakeman @@ -126,6 +125,21 @@ jobs: name: eslint command: yarn run eslint + # Run frontend tests + - run: + name: Run frontend tests + command: | + mkdir -p ~/tmp/test-results/frontend_specs + ~/tmp/cc-test-reporter before-build + TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings) + yarn test:coverage --profile 10 \ + --out ~/tmp/test-results/yarn.xml \ + -- ${TESTFILES} + - run: + name: Code Climate Test Coverage + command: | + ~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json" + # Run rails tests - run: name: Run backend tests @@ -145,20 +159,6 @@ jobs: command: | ~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json" - - run: - name: Run frontend tests - command: | - mkdir -p ~/tmp/test-results/frontend_specs - ~/tmp/cc-test-reporter before-build - TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings) - yarn test:coverage --profile 10 \ - --out ~/tmp/test-results/yarn.xml \ - -- ${TESTFILES} - - run: - name: Code Climate Test Coverage - command: | - ~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json" - - persist_to_workspace: root: coverage paths: diff --git a/.env.example b/.env.example index f405ea03d..a3a5bf7cc 100644 --- a/.env.example +++ b/.env.example @@ -230,3 +230,11 @@ AZURE_APP_SECRET= ## Change these values to fine tune performance # control the concurrency setting of sidekiq # SIDEKIQ_CONCURRENCY=10 + + +# AI powered features +## OpenAI key +# OPENAI_API_KEY= + +# Sentiment analysis model file path +SENTIMENT_FILE_PATH= diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml new file mode 100644 index 000000000..ec8f6f4ab --- /dev/null +++ b/.github/workflows/lint_pr.yml @@ -0,0 +1,23 @@ +# ref: https://github.com/amannn/action-semantic-pull-request +# ensure PR title is in semantic format + +name: "Lint PR" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/logging_percentage_check.yml b/.github/workflows/logging_percentage_check.yml new file mode 100644 index 000000000..7b5627d45 --- /dev/null +++ b/.github/workflows/logging_percentage_check.yml @@ -0,0 +1,55 @@ +name: Log Lines Percentage Check + +on: + pull_request: + branches: + - develop + +jobs: + log_lines_check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Check for log lines and calculate percentage + run: | + # Define the log line pattern + LOG_LINE_PATTERN="Rails\.logger" + + # Get the list of changed files in the pull request + CHANGED_FILES=$(git diff --name-only) + + # Initialize a flag to track if any files have insufficient log lines + INSUFFICIENT_LOGS=0 + + for file in $CHANGED_FILES; do + if [[ $file =~ \.rb$ && ! $file =~ _spec\.rb$ ]]; then + # Count the total number of lines in the file + total_lines=$(wc -l < "$file") + + # Count the number of log lines in the file + log_lines=$(grep -c "$LOG_LINE_PATTERN" "$file") + + # Calculate the percentage of log lines + if [ "$total_lines" -gt 0 ]; then + percentage=$(awk "BEGIN { pc=100*${log_lines}/${total_lines}; i=int(pc); print (pc-i<0.5)?i:i+1 }") + else + percentage=0 + fi + + # Check if the percentage is less than 5% + if [ "$percentage" -lt 5 ]; then + echo "Error: Log lines percentage is less than 5% ($percentage%) in $file. Please add more log lines using Rails.logger statements." + INSUFFICIENT_LOGS=1 + else + echo "Log lines percentage is $percentage% in $file. Code looks good!" + fi + fi + done + + # If any files have insufficient log lines, fail the action + if [ "$INSUFFICIENT_LOGS" -eq 1 ]; then + exit 1 + fi diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index 51688944c..71009f5a0 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -18,11 +18,12 @@ jobs: runs-on: ubuntu-20.04 services: postgres: - image: postgres:10.8 + image: postgres:15.3 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: "" POSTGRES_DB: postgres + POSTGRES_HOST_AUTH_METHOD: trust ports: - 5432:5432 # needed because the postgres container does not provide a healthcheck diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml new file mode 100644 index 000000000..daa89494a --- /dev/null +++ b/.github/workflows/run_response_bot_spec.yml @@ -0,0 +1,78 @@ +# # +# # This workflow will run specs related to response bot +# # This can only be activated in installations Where vector extension is available. +# # + +name: Run Response Bot spec +on: + push: + branches: + - develop + - master + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-20.04 + services: + postgres: + image: ankane/pgvector + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: "" + POSTGRES_DB: postgres + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + # tmpfs makes DB faster by using RAM + options: >- + --mount type=tmpfs,destination=/var/lib/postgresql/data + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: yarn + run: yarn install + + - name: Create database + run: bundle exec rake db:create + + - name: Seed database + run: bundle exec rake db:schema:load + + - name: Enable ResponseBotService in installation + run: RAILS_ENV=test bundle exec rails runner "Features::ResponseBotService.new.enable_in_installation" + + # Run Response Bot specs + - name: Run backend tests + run: | + bundle exec rspec spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 --profile=10 --format documentation + + - name: Upload rails log folder + uses: actions/upload-artifact@v3 + if: always() + with: + name: rails-log-folder + path: log diff --git a/.gitignore b/.gitignore index 82f4a4df5..028a97f09 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,5 @@ yalc.lock /yarn-error.log yarn-debug.log* .yarn-integrity + +/storybook-static \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 0627d197d..47979412e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.17.4 +16.20.1 diff --git a/.rubocop.yml b/.rubocop.yml index 914308551..2befbac4b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -87,6 +87,7 @@ Style/ClassAndModuleChildren: EnforcedStyle: compact Exclude: - 'config/application.rb' + - 'config/initializers/monkey_patches/*' Style/MapToHash: Enabled: false Style/HashSyntax: diff --git a/.storybook/main.js b/.storybook/main.js index b749e262d..cb32634c2 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -23,6 +23,18 @@ module.exports = { }, '@storybook/addon-links', '@storybook/addon-essentials', + { + /** + * Fix Storybook issue with PostCSS@8 + * @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085 + */ + name: '@storybook/addon-postcss', + options: { + postcssLoaderOptions: { + implementation: require('postcss'), + }, + }, + }, ], webpackFinal: config => { const newConfig = { @@ -35,7 +47,7 @@ module.exports = { newConfig.module.rules.push({ test: /\.scss$/, - use: ['style-loader', 'css-loader', 'sass-loader'], + use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'], include: path.resolve(__dirname, '../app/javascript'), }); diff --git a/.storybook/preview.js b/.storybook/preview.js index e553e514a..3f98c2cd8 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -4,10 +4,12 @@ import Vuex from 'vuex'; import VueI18n from 'vue-i18n'; import Vuelidate from 'vuelidate'; import Multiselect from 'vue-multiselect'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import WootUiKit from '../app/javascript/dashboard/components'; import i18n from '../app/javascript/dashboard/i18n'; +import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer'; import '../app/javascript/dashboard/assets/scss/storybook.scss'; @@ -15,6 +17,8 @@ Vue.use(VueI18n); Vue.use(Vuelidate); Vue.use(WootUiKit); Vue.use(Vuex); +Vue.use(VueDOMPurifyHTML, domPurifyConfig); + Vue.component('multiselect', Multiselect); Vue.component('fluent-icon', FluentIcon); diff --git a/Gemfile b/Gemfile index d49157502..7e79d2e00 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ ruby '3.2.2' ##-- base gems for rails --## gem 'rack-cors', require: 'rack/cors' -gem 'rails', '~> 7' +gem 'rails', '~> 7.0.5.1' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', require: false @@ -34,7 +34,7 @@ gem 'commonmarker' # Validate Data against JSON Schema gem 'json_schemer' # Rack middleware for blocking & throttling abusive requests -gem 'rack-attack' +gem 'rack-attack', '>= 6.7.0' # a utility tool for streaming, flexible and safe downloading of remote files gem 'down' # authentication type to fetch and send mail over oauth2.0 @@ -74,7 +74,7 @@ gem 'devise_token_auth' gem 'jwt' gem 'pundit' # super admin -gem 'administrate' +gem 'administrate', '>= 0.19.0' gem 'administrate-field-active_storage' ##--- gems for pubsub service ---## @@ -108,9 +108,9 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', require: false gem 'scout_apm', require: false -gem 'sentry-rails', require: false +gem 'sentry-rails', '>= 5.10.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', require: false +gem 'sentry-sidekiq', '>= 5.10.0', require: false ##-- background job processing --## gem 'sidekiq' @@ -153,7 +153,7 @@ gem 'stripe' gem 'faker' # Include logrange conditionally in intializer using env variable -gem 'lograge', '~> 0.12.0', require: false +gem 'lograge', '~> 0.13.0', require: false # worked with microsoft refresh token gem 'omniauth-oauth2' @@ -165,6 +165,16 @@ gem 'omniauth' gem 'omniauth-google-oauth2' gem 'omniauth-rails_csrf_protection', '~> 1.0' +## Gems for reponse bot +# adds cosine similarity to postgres using vector extension +gem 'neighbor' +gem 'pgvector' +# Convert Website HTML to Markdown +gem 'reverse_markdown' + +# Sentiment analysis +gem 'informers' + ### Gems required only in specific deployment environments ### ############################################################## @@ -187,7 +197,7 @@ group :development do gem 'squasher' # profiling - gem 'rack-mini-profiler', require: false + gem 'rack-mini-profiler', '>= 3.1.1', require: false gem 'stackprof' end @@ -210,6 +220,7 @@ group :development, :test do gem 'bundle-audit', require: false gem 'byebug', platform: :mri gem 'climate_control' + gem 'debug', '~> 1.8' gem 'factory_bot_rails' gem 'listen' gem 'mock_redis' diff --git a/Gemfile.lock b/Gemfile.lock index c4d6f7e56..f2b3ea86a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,70 +33,70 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.5) - actionpack (= 7.0.5) - activesupport (= 7.0.5) + actioncable (7.0.5.1) + actionpack (= 7.0.5.1) + activesupport (= 7.0.5.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.5) - actionpack (= 7.0.5) - activejob (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + actionmailbox (7.0.5.1) + actionpack (= 7.0.5.1) + activejob (= 7.0.5.1) + activerecord (= 7.0.5.1) + activestorage (= 7.0.5.1) + activesupport (= 7.0.5.1) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.5) - actionpack (= 7.0.5) - actionview (= 7.0.5) - activejob (= 7.0.5) - activesupport (= 7.0.5) + actionmailer (7.0.5.1) + actionpack (= 7.0.5.1) + actionview (= 7.0.5.1) + activejob (= 7.0.5.1) + activesupport (= 7.0.5.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.5) - actionview (= 7.0.5) - activesupport (= 7.0.5) + actionpack (7.0.5.1) + actionview (= 7.0.5.1) + activesupport (= 7.0.5.1) 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.5) - actionpack (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + actiontext (7.0.5.1) + actionpack (= 7.0.5.1) + activerecord (= 7.0.5.1) + activestorage (= 7.0.5.1) + activesupport (= 7.0.5.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.5) - activesupport (= 7.0.5) + actionview (7.0.5.1) + activesupport (= 7.0.5.1) 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.5) - activesupport (= 7.0.5) + activejob (7.0.5.1) + activesupport (= 7.0.5.1) globalid (>= 0.3.6) - activemodel (7.0.5) - activesupport (= 7.0.5) - activerecord (7.0.5) - activemodel (= 7.0.5) - activesupport (= 7.0.5) + activemodel (7.0.5.1) + activesupport (= 7.0.5.1) + activerecord (7.0.5.1) + activemodel (= 7.0.5.1) + activesupport (= 7.0.5.1) activerecord-import (1.4.1) activerecord (>= 4.2) - activestorage (7.0.5) - actionpack (= 7.0.5) - activejob (= 7.0.5) - activerecord (= 7.0.5) - activesupport (= 7.0.5) + activestorage (7.0.5.1) + actionpack (= 7.0.5.1) + activejob (= 7.0.5.1) + activerecord (= 7.0.5.1) + activesupport (= 7.0.5.1) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.5) + activesupport (7.0.5.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -105,7 +105,7 @@ GEM activerecord (>= 6.0, < 7.1) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) - administrate (0.18.0) + administrate (0.19.0) actionpack (>= 5.0) actionview (>= 5.0) activerecord (>= 5.0) @@ -145,6 +145,7 @@ GEM statsd-ruby (~> 1.1) bcrypt (3.1.18) bindex (0.8.1) + blingfire (0.1.8) bootsnap (1.16.0) msgpack (~> 1.2) brakeman (5.4.1) @@ -161,9 +162,9 @@ GEM byebug (11.1.3) climate_control (1.2.0) coderay (1.1.3) - commonmarker (0.23.9) + commonmarker (0.23.10) concurrent-ruby (1.2.2) - connection_pool (2.4.0) + connection_pool (2.4.1) crack (0.4.5) rexml crass (1.0.6) @@ -183,6 +184,9 @@ GEM libddwaf (~> 1.8.2.0.0) msgpack debase-ruby_core_source (3.2.0) + debug (1.8.0) + irb (>= 1.5.0) + reline (>= 0.3.1) declarative (0.0.20) devise (4.9.2) bcrypt (~> 3.0) @@ -361,11 +365,18 @@ 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) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jquery-rails (4.5.1) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -418,7 +429,7 @@ GEM llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) - lograge (0.12.0) + lograge (0.13.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -440,17 +451,19 @@ GEM mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.8.2) - minitest (5.18.0) + mini_portile2 (2.8.4) + minitest (5.19.0) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.3.0) + neighbor (0.2.3) + activerecord (>= 5.2) net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.3.4) + net-imap (0.3.6) date net-protocol net-pop (0.1.2) @@ -465,15 +478,16 @@ GEM sidekiq newrelic_rpm (8.16.0) nio4r (2.5.9) - nokogiri (1.15.2) + nokogiri (1.15.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.15.2-arm64-darwin) + nokogiri (1.15.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-darwin) + nokogiri (1.15.3-x86_64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-linux) + nokogiri (1.15.3-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) @@ -502,6 +516,14 @@ GEM omniauth-rails_csrf_protection (1.0.1) 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) @@ -512,6 +534,7 @@ GEM pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) + pgvector (0.1.1) procore-sift (1.0.0) activerecord (>= 6.1) pry (0.14.2) @@ -525,13 +548,13 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.0) - rack (2.2.7) - rack-attack (6.6.1) - rack (>= 1.0, < 3) + racc (1.7.1) + rack (2.2.8) + rack-attack (6.7.0) + rack (>= 1.0, < 4) rack-cors (2.0.1) rack (>= 2.0.0) - rack-mini-profiler (3.1.0) + rack-mini-profiler (3.1.1) rack (>= 1.2.0) rack-protection (3.0.6) rack @@ -540,29 +563,30 @@ GEM rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rails (7.0.5) - actioncable (= 7.0.5) - actionmailbox (= 7.0.5) - actionmailer (= 7.0.5) - actionpack (= 7.0.5) - actiontext (= 7.0.5) - actionview (= 7.0.5) - activejob (= 7.0.5) - activemodel (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + rails (7.0.5.1) + actioncable (= 7.0.5.1) + actionmailbox (= 7.0.5.1) + actionmailer (= 7.0.5.1) + actionpack (= 7.0.5.1) + actiontext (= 7.0.5.1) + actionview (= 7.0.5.1) + activejob (= 7.0.5.1) + activemodel (= 7.0.5.1) + activerecord (= 7.0.5.1) + activestorage (= 7.0.5.1) + activesupport (= 7.0.5.1) bundler (>= 1.15.0) - railties (= 7.0.5) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 7.0.5.1) + rails-dom-testing (2.1.1) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.5) - actionpack (= 7.0.5) - activesupport (= 7.0.5) + railties (7.0.5.1) + actionpack (= 7.0.5.1) + activesupport (= 7.0.5.1) method_source rake (>= 12.2) thor (~> 1.0) @@ -579,6 +603,8 @@ GEM redis-namespace (1.10.0) redis (>= 4) regexp_parser (2.8.0) + reline (0.3.6) + io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -594,6 +620,8 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.2) + reverse_markdown (2.1.1) + nokogiri rexml (3.2.5) rspec-core (3.12.2) rspec-support (~> 3.12.0) @@ -669,18 +697,18 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.9.0) + sentry-rails (5.10.0) railties (>= 5.0) - sentry-ruby (~> 5.9.0) - sentry-ruby (5.9.0) + sentry-ruby (~> 5.10.0) + sentry-ruby (5.10.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.9.0) - sentry-ruby (~> 5.9.0) + sentry-sidekiq (5.10.0) + sentry-ruby (~> 5.10.0) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.1.0) + sidekiq (7.1.2) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) @@ -731,7 +759,7 @@ GEM time_diff (0.3.0) activesupport i18n - timeout (0.3.2) + timeout (0.4.0) trailblazer-option (0.1.2) twilio-ruby (5.77.0) faraday (>= 0.9, < 3.0) @@ -784,7 +812,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.8) + zeitwerk (2.6.9) PLATFORMS arm64-darwin-20 @@ -801,7 +829,7 @@ DEPENDENCIES active_record_query_trace activerecord-import acts-as-taggable-on - administrate + administrate (>= 0.19.0) administrate-field-active_storage annotate attr_extras @@ -821,6 +849,7 @@ DEPENDENCIES cypress-on-rails database_cleaner ddtrace + debug (~> 1.8) devise devise-secure_password! devise_token_auth @@ -846,6 +875,7 @@ DEPENDENCIES hashie html2text! image_processing + informers jbuilder json_refs json_schemer @@ -856,9 +886,10 @@ DEPENDENCIES line-bot-api liquid listen - lograge (~> 0.12.0) + lograge (~> 0.13.0) maxminddb mock_redis + neighbor newrelic-sidekiq-metrics newrelic_rpm omniauth @@ -867,19 +898,21 @@ DEPENDENCIES omniauth-rails_csrf_protection (~> 1.0) pg pg_search + pgvector procore-sift pry-rails puma pundit - rack-attack + rack-attack (>= 6.7.0) rack-cors - rack-mini-profiler + rack-mini-profiler (>= 3.1.1) rack-timeout - rails (~> 7) + rails (~> 7.0.5.1) redis redis-namespace responders rest-client + reverse_markdown rspec-rails rspec_junit_formatter rubocop @@ -889,9 +922,9 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails + sentry-rails (>= 5.10.0) sentry-ruby - sentry-sidekiq + sentry-sidekiq (>= 5.10.0) shoulda-matchers sidekiq sidekiq-cron diff --git a/Makefile b/Makefile index 499d0ab6b..16eb80718 100644 --- a/Makefile +++ b/Makefile @@ -30,9 +30,23 @@ burn: bundle && yarn run: + @if [ -f ./.overmind.sock ]; then \ + echo "Overmind is already running. Use 'make force_run' to start a new instance."; \ + else \ + overmind start -f Procfile.dev; \ + fi + +force_run: + rm -f ./.overmind.sock overmind start -f Procfile.dev +debug: + overmind connect backend + +debug_worker: + overmind connect worker + docker: docker build -t $(APP_NAME) -f ./docker/Dockerfile . -.PHONY: setup db_create db_migrate db_seed db console server burn docker run +.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker diff --git a/app/assets/javascripts/dashboardChart.js b/app/assets/javascripts/dashboardChart.js index 6bfe56bda..46f278b80 100644 --- a/app/assets/javascripts/dashboardChart.js +++ b/app/assets/javascripts/dashboardChart.js @@ -11,7 +11,7 @@ function prepareData(data) { function getChartOptions() { var fontFamily = - 'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; + 'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; return { responsive: true, legend: { labels: { fontFamily } }, diff --git a/app/assets/stylesheets/administrate/library/_variables.scss b/app/assets/stylesheets/administrate/library/_variables.scss index 3fdfcfd8d..5bb3c0710 100644 --- a/app/assets/stylesheets/administrate/library/_variables.scss +++ b/app/assets/stylesheets/administrate/library/_variables.scss @@ -1,10 +1,10 @@ // Typography -$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", +$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif !default; $heading-font-family: $base-font-family !default; -$base-font-size: 10px !default; +$base-font-size: 16px !default; $base-line-height: 1.5 !default; $heading-line-height: 1.2 !default; diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index aca06cd04..af312bf2e 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -48,17 +48,22 @@ class Messages::MessageBuilder def process_emails return unless @conversation.inbox&.inbox_type == 'Email' - cc_emails = [] - cc_emails = @params[:cc_emails].gsub(/\s+/, '').split(',') if @params[:cc_emails].present? + cc_emails = process_email_string(@params[:cc_emails]) + bcc_emails = process_email_string(@params[:bcc_emails]) + to_emails = process_email_string(@params[:to_emails]) - bcc_emails = [] - bcc_emails = @params[:bcc_emails].gsub(/\s+/, '').split(',') if @params[:bcc_emails].present? - - all_email_addresses = cc_emails + bcc_emails + all_email_addresses = cc_emails + bcc_emails + to_emails validate_email_addresses(all_email_addresses) @message.content_attributes[:cc_emails] = cc_emails @message.content_attributes[:bcc_emails] = bcc_emails + @message.content_attributes[:to_emails] = to_emails + end + + def process_email_string(email_string) + return [] if email_string.blank? + + email_string.gsub(/\s+/, '').split(',') end def validate_email_addresses(all_emails) diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index b5fb8045b..c96950ee9 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -20,7 +20,7 @@ class V2::ReportBuilder # For backward compatible with old report def build - if %w[avg_first_response_time avg_resolution_time].include?(params[:metric]) + if %w[avg_first_response_time avg_resolution_time reply_time].include?(params[:metric]) timeseries.each_with_object([]) do |p, arr| arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] } end @@ -33,18 +33,19 @@ class V2::ReportBuilder def summary { - conversations_count: conversations_count.values.sum, - incoming_messages_count: incoming_messages_count.values.sum, - outgoing_messages_count: outgoing_messages_count.values.sum, + conversations_count: conversations.count, + incoming_messages_count: incoming_messages.count, + outgoing_messages_count: outgoing_messages.count, avg_first_response_time: avg_first_response_time_summary, avg_resolution_time: avg_resolution_time_summary, - resolutions_count: resolutions_count.values.sum + resolutions_count: resolutions.count, + reply_time: reply_time_summary } end def conversation_metrics if params[:type].equal?(:account) - conversations + live_conversations else agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse end @@ -89,12 +90,12 @@ class V2::ReportBuilder email: @user.email, thumbnail: @user.avatar_url, availability: account_user.availability_status, - metric: conversations + metric: live_conversations } end end - def conversations + def live_conversations @open_conversations = scope.conversations.where(account_id: @account.id).open metric = { open: @open_conversations.count, diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index 3575e28e6..6e119ca3d 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -14,8 +14,18 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController @facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel) set_instagram_id(page_access_token, facebook_channel) set_avatar(@facebook_inbox, page_id) - rescue StandardError => e - ChatwootExceptionTracker.new(e).capture_exception + end + rescue StandardError => e + ChatwootExceptionTracker.new(e).capture_exception + Rails.logger.error "Error in register_facebook_page: #{e.message}" + # Additional log statements + log_additional_info + end + + def log_additional_info + Rails.logger.debug do + "user_access_token: #{params[:user_access_token]} , page_access_token: #{params[:page_access_token]} , + page_id: #{params[:page_id]}, inbox_name: #{params[:inbox_name]}" end end @@ -30,6 +40,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController instagram_id = response['instagram_business_account']['id'] facebook_channel.update(instagram_id: instagram_id) + rescue StandardError => e + Rails.logger.error "Error in set_instagram_id: #{e.message}" end # get params[:inbox_id], current_account. params[:omniauth_token] @@ -61,6 +73,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController fb_page&.reauthorized! rescue StandardError => e ChatwootExceptionTracker.new(e).capture_exception + Rails.logger.error "Error in update_fb_page: #{e.message}" end end @@ -77,7 +90,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', '')) koala.exchange_access_token_info(omniauth_token)['access_token'] rescue StandardError => e - Rails.logger.error e + Rails.logger.error "Error in long_lived_token: #{e.message}" end def mark_already_existing_facebook_pages(data) diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index e6e5b63c3..d2d51baef 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -18,7 +18,11 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: end def authenticate_twilio - client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token]) + 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]) + else + Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token]) + end client.messages.list(limit: 1) end @@ -40,6 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: @twilio_channel = Current.account.twilio_sms.create!( account_sid: permitted_params[:account_sid], auth_token: permitted_params[:auth_token], + api_key_sid: permitted_params[:api_key_sid], messaging_service_sid: permitted_params[:messaging_service_sid].presence, phone_number: phone_number, medium: medium @@ -52,7 +57,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: def permitted_params params.require(:twilio_channel).permit( - :account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium + :account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium, :api_key_sid ) end end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index ba78cb805..587e082ce 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -4,6 +4,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction] sort_on :phone_number, type: :string sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction] + sort_on :created_at, internal_name: :order_on_created_at, type: :scope, scope_params: [:direction] sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction] sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction] sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction] diff --git a/app/controllers/api/v1/accounts/conversations/base_controller.rb b/app/controllers/api/v1/accounts/conversations/base_controller.rb index da821a3e5..500c7772f 100644 --- a/app/controllers/api/v1/accounts/conversations/base_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/base_controller.rb @@ -1,5 +1,4 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController - include EnsureCurrentAccountHelper before_action :conversation private diff --git a/app/controllers/api/v1/accounts/custom_filters_controller.rb b/app/controllers/api/v1/accounts/custom_filters_controller.rb index 6cd70b07d..f458c018f 100644 --- a/app/controllers/api/v1/accounts/custom_filters_controller.rb +++ b/app/controllers/api/v1/accounts/custom_filters_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController + before_action :check_authorization before_action :fetch_custom_filters, except: [:create] before_action :fetch_custom_filter, only: [:show, :update, :destroy] DEFAULT_FILTER_TYPE = 'conversation'.freeze diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 70c3f2a23..011faaf28 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -63,7 +63,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def destroy - ::DeleteObjectJob.perform_later(@inbox) if @inbox.present? + ::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present? render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') } end @@ -124,7 +124,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def inbox_attributes [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, - :lock_to_single_conversation, :portal_id] + :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name] end def permitted_params(channel_attributes = []) diff --git a/app/controllers/api/v1/accounts/integrations/slack_controller.rb b/app/controllers/api/v1/accounts/integrations/slack_controller.rb index b5571b245..eaa18c2de 100644 --- a/app/controllers/api/v1/accounts/integrations/slack_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/slack_controller.rb @@ -1,27 +1,27 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController before_action :check_admin_authorization? - before_action :fetch_hook, only: [:update, :destroy] + before_action :fetch_hook, only: [:update, :destroy, :list_all_channels] + + def list_all_channels + @channels = channel_builder.fetch_channels + end def create - ActiveRecord::Base.transaction do - builder = Integrations::Slack::HookBuilder.new( - account: Current.account, - code: params[:code], - inbox_id: params[:inbox_id] - ) - @hook = builder.perform - create_chatwoot_slack_channel - end + hook_builder = Integrations::Slack::HookBuilder.new( + account: Current.account, + code: params[:code], + inbox_id: params[:inbox_id] + ) + @hook = hook_builder.perform end def update - create_chatwoot_slack_channel - render json: @hook + @hook = channel_builder.update(permitted_params[:reference_id]) + render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank? end def destroy @hook.destroy! - head :ok end @@ -31,11 +31,11 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack') end - def create_chatwoot_slack_channel - channel = params[:channel] || 'customer-conversations' - builder = Integrations::Slack::ChannelBuilder.new( - hook: @hook, channel: channel - ) - builder.perform + def channel_builder + Integrations::Slack::ChannelBuilder.new(hook: @hook) + end + + def permitted_params + params.permit(:reference_id) end end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index 76efb2c42..306a5f46f 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -17,7 +17,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController @message.update!(submitted_email: contact_email) ContactIdentifyAction.new( contact: @contact, - params: { email: contact_email, name: contact_name } + params: { email: contact_email, name: contact_name }, + retain_original_contact_name: true ).perform else @message.update!(message_update_params[:message]) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index ff6b5f57e..9e59758ea 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,6 +1,7 @@ class DashboardController < ActionController::Base include SwitchLocale + before_action :set_application_pack before_action :set_global_config around_action :switch_locale before_action :ensure_installation_onboarding, only: [:index] @@ -14,7 +15,7 @@ class DashboardController < ActionController::Base def set_global_config @global_config = GlobalConfig.get( - 'LOGO', 'LOGO_THUMBNAIL', + 'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL', 'TERMS_URL', 'PRIVACY_URL', @@ -60,4 +61,12 @@ class DashboardController < ActionController::Base GIT_SHA: GIT_HASH } end + + def set_application_pack + @application_pack = if request.path.include?('/auth') || request.path.include?('/login') + 'v3app' + else + 'application' + end + end end diff --git a/app/controllers/public/api/v1/inboxes/conversations_controller.rb b/app/controllers/public/api/v1/inboxes/conversations_controller.rb index a399bc749..3d86ca87c 100644 --- a/app/controllers/public/api/v1/inboxes/conversations_controller.rb +++ b/app/controllers/public/api/v1/inboxes/conversations_controller.rb @@ -1,4 +1,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController + include Events::Types + before_action :set_conversation, only: [:toggle_typing, :update_last_seen] + def index @conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations end @@ -7,12 +10,36 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox @conversation = create_conversation end + def toggle_typing + case params[:typing_status] + when 'on' + trigger_typing_event(CONVERSATION_TYPING_ON) + when 'off' + trigger_typing_event(CONVERSATION_TYPING_OFF) + end + head :ok + end + + def update_last_seen + @conversation.contact_last_seen_at = DateTime.now.utc + @conversation.save! + head :ok + end + private + def set_conversation + @conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id]) + end + def create_conversation ::Conversation.create!(conversation_params) end + def trigger_typing_event(event) + Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: @conversation.contact) + end + def conversation_params { account_id: @contact_inbox.contact.account_id, diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 1f8be3ad7..aba385c83 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -8,13 +8,22 @@ 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? - @articles.order(position: :asc) + order_by_sort_param + @articles.page(list_params[:page]) if list_params[:page].present? end def show; end private + def order_by_sort_param + @articles = if list_params[:sort].present? && list_params[:sort] == 'views' + @articles.order_by_views + else + @articles.order_by_position + end + end + def set_article @article = @portal.articles.find_by(slug: permitted_params[:article_slug]) @article.increment_view_count @@ -35,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B end def list_params - params.permit(:query, :locale) + params.permit(:query, :locale, :sort) end def permitted_params diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index c5297e6e4..851b8c549 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -1,8 +1,13 @@ class Public::Api::V1::Portals::BaseController < PublicController + before_action :show_plain_layout around_action :set_locale private + def show_plain_layout + @is_plain_layout_enabled = params[:show_plain_layout] == 'true' + end + def set_locale(&) switch_locale_with_portal(&) if params[:locale].present? switch_locale_with_article(&) if params[:article_slug].present? diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb index 50cd3f8f6..fea975c0a 100644 --- a/app/dashboards/account_dashboard.rb +++ b/app/dashboards/account_dashboard.rb @@ -86,7 +86,10 @@ class AccountDashboard < Administrate::BaseDashboard "##{account.id} #{account.name}" end - def permitted_attributes + # We do not use the action parameter but we still need to define it + # to prevent an error from being raised (wrong number of arguments) + # Reference: https://github.com/thoughtbot/administrate/pull/2356/files#diff-4e220b661b88f9a19ac527c50d6f1577ef6ab7b0bed2bfdf048e22e6bfa74a05R204 + def permitted_attributes(action) super + [limits: {}] end end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index a072b68b3..cbbd60caa 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -6,7 +6,8 @@ class ConversationFinder latest: 'latest', sort_on_created_at: 'sort_on_created_at', last_user_message_at: 'last_user_message_at', - sort_on_priority: 'sort_on_priority' + sort_on_priority: 'sort_on_priority', + sort_on_waiting_since: 'sort_on_waiting_since' }.with_indifferent_access # assumptions diff --git a/app/helpers/billing_helper.rb b/app/helpers/billing_helper.rb new file mode 100644 index 000000000..26669e38d --- /dev/null +++ b/app/helpers/billing_helper.rb @@ -0,0 +1,21 @@ +module BillingHelper + private + + def default_plan?(account) + installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS') + default_plan = installation_config&.value&.first + + # Return false if not plans are configured, so that no checks are enforced + return false if default_plan.blank? + + account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan['name'] + end + + def conversations_this_month(account) + account.conversations.where('created_at > ?', 30.days.ago).count + end + + def non_web_inboxes(account) + account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count + end +end diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb index 8ee747eb9..2b0f43b3f 100644 --- a/app/helpers/report_helper.rb +++ b/app/helpers/report_helper.rb @@ -17,21 +17,36 @@ module ReportHelper end def conversations_count - (get_grouped_values scope.conversations.where(account_id: account.id)).count + (get_grouped_values conversations).count end def incoming_messages_count - (get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count + (get_grouped_values incoming_messages).count end def outgoing_messages_count - (get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count + (get_grouped_values outgoing_messages).count end def resolutions_count - object_scope = scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved, - conversations: { status: :resolved }).distinct - (get_grouped_values object_scope).count + (get_grouped_values resolutions).count + end + + def conversations + scope.conversations.where(account_id: account.id, created_at: range) + end + + def incoming_messages + scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order) + end + + def outgoing_messages + scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order) + end + + def resolutions + scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved, + conversations: { status: :resolved }, created_at: range).distinct end def avg_first_response_time @@ -41,6 +56,13 @@ module ReportHelper grouped_reporting_events.average(:value) end + def reply_time + grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'reply_time', account_id: account.id)) + return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] + + grouped_reporting_events.average(:value) + end + def avg_resolution_time grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved', account_id: account.id)) return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] @@ -51,17 +73,35 @@ module ReportHelper def avg_resolution_time_summary reporting_events = scope.reporting_events .where(name: 'conversation_resolved', account_id: account.id, created_at: range) - avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) + avg_rt = if params[:business_hours].present? + reporting_events.average(:value_in_business_hours) + else + reporting_events.average(:value) + end return 0 if avg_rt.blank? avg_rt end + def reply_time_summary + reporting_events = scope.reporting_events + .where(name: 'reply_time', account_id: account.id, created_at: range) + reply_time = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) + + return 0 if reply_time.blank? + + reply_time + end + def avg_first_response_time_summary reporting_events = scope.reporting_events .where(name: 'first_response', account_id: account.id, created_at: range) - avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) + avg_frt = if params[:business_hours].present? + reporting_events.average(:value_in_business_hours) + else + reporting_events.average(:value) + end return 0 if avg_frt.blank? diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 203ccec30..05888c5c7 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -4,8 +4,13 @@ id="app" class="app-wrapper app-root" :class="{ 'app-rtl--wrapper': isRTLView }" + :dir="isRTLView ? 'rtl' : 'ltr'" > + @@ -25,9 +30,12 @@ import AddAccountModal from '../dashboard/components/layout/sidebarComponents/Ad import LoadingState from './components/widgets/LoadingState.vue'; import NetworkNotification from './components/NetworkNotification'; import UpdateBanner from './components/app/UpdateBanner.vue'; +import UpgradeBanner from './components/app/UpgradeBanner.vue'; +import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import vueActionCable from './helper/actionCable'; import WootSnackbarBox from './components/SnackbarContainer'; import rtlMixin from 'shared/mixins/rtlMixin'; +import { setColorTheme } from './helper/themeHelper'; import { registerSubscription, verifyServiceWorkerExistence, @@ -41,7 +49,9 @@ export default { LoadingState, NetworkNotification, UpdateBanner, + PaymentPendingBanner, WootSnackbarBox, + UpgradeBanner, }, mixins: [rtlMixin], @@ -59,6 +69,7 @@ export default { currentUser: 'getCurrentUser', globalConfig: 'globalConfig/get', authUIFlags: 'getAuthUIFlags', + accountUIFlags: 'accounts/getUIFlags', currentAccountId: 'getCurrentAccountId', }), hasAccounts() { @@ -80,9 +91,18 @@ export default { }, }, mounted() { + this.initializeColorTheme(); + this.listenToThemeChanges(); this.setLocale(window.chatwootConfig.selectedLocale); }, methods: { + initializeColorTheme() { + setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches); + }, + listenToThemeChanges() { + const mql = window.matchMedia('(prefers-color-scheme: dark)'); + mql.onchange = e => setColorTheme(e.matches); + }, setLocale(locale) { this.$root.$i18n.locale = locale; }, diff --git a/app/javascript/dashboard/api/CacheEnabledApiClient.js b/app/javascript/dashboard/api/CacheEnabledApiClient.js index 2c00389e5..9af939c00 100644 --- a/app/javascript/dashboard/api/CacheEnabledApiClient.js +++ b/app/javascript/dashboard/api/CacheEnabledApiClient.js @@ -18,6 +18,10 @@ class CacheEnabledApiClient extends ApiClient { return this.getFromCache(); } + return this.getFromNetwork(); + } + + getFromNetwork() { return axios.get(this.url); } @@ -32,7 +36,12 @@ class CacheEnabledApiClient extends ApiClient { } async getFromCache() { - await this.dataManager.initDb(); + try { + // IDB is not supported in Firefox private mode: https://bugzilla.mozilla.org/show_bug.cgi?id=781982 + await this.dataManager.initDb(); + } catch { + return this.getFromNetwork(); + } const { data } = await axios.get( `/api/v1/accounts/${this.accountIdFromRoute}/cache_keys` @@ -55,16 +64,22 @@ class CacheEnabledApiClient extends ApiClient { } async refetchAndCommit(newKey = null) { - await this.dataManager.initDb(); - const response = await axios.get(this.url); - this.dataManager.replace({ - modelName: this.cacheModelName, - data: this.extractDataFromResponse(response), - }); + const response = await this.getFromNetwork(); - await this.dataManager.setCacheKeys({ - [this.cacheModelName]: newKey, - }); + try { + await this.dataManager.initDb(); + + this.dataManager.replace({ + modelName: this.cacheModelName, + data: this.extractDataFromResponse(response), + }); + + await this.dataManager.setCacheKeys({ + [this.cacheModelName]: newKey, + }); + } catch { + // Ignore error + } return response; } diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 040c27313..883644d17 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -3,47 +3,11 @@ import Cookies from 'js-cookie'; import endPoints from './endPoints'; import { - setAuthCredentials, clearCookiesOnLogout, deleteIndexedDBOnLogout, } from '../store/utils/api'; export default { - login(creds) { - return new Promise((resolve, reject) => { - axios - .post('auth/sign_in', creds) - .then(response => { - setAuthCredentials(response); - resolve(response.data); - }) - .catch(error => { - reject(error.response); - }); - }); - }, - - register(creds) { - const urlData = endPoints('register'); - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url, { - account_name: creds.accountName.trim(), - user_full_name: creds.fullName.trim(), - email: creds.email, - password: creds.password, - h_captcha_client_response: creds.hCaptchaClientResponse, - }) - .then(response => { - setAuthCredentials(response); - resolve(response); - }) - .catch(error => { - reject(error); - }); - }); - return fetchPromise; - }, validityCheck() { const urlData = endPoints('validityCheck'); return axios.get(urlData.url); @@ -73,45 +37,6 @@ export default { } return false; }, - verifyPasswordToken({ confirmationToken }) { - return new Promise((resolve, reject) => { - axios - .post('auth/confirmation', { - confirmation_token: confirmationToken, - }) - .then(response => { - setAuthCredentials(response); - resolve(response); - }) - .catch(error => { - reject(error.response); - }); - }); - }, - - setNewPassword({ resetPasswordToken, password, confirmPassword }) { - return new Promise((resolve, reject) => { - axios - .put('auth/password', { - reset_password_token: resetPasswordToken, - password_confirmation: confirmPassword, - password, - }) - .then(response => { - setAuthCredentials(response); - resolve(response); - }) - .catch(error => { - reject(error.response); - }); - }); - }, - - resetPassword({ email }) { - const urlData = endPoints('resetPassword'); - return axios.post(urlData.url, { email }); - }, - profileUpdate({ password, password_confirmation, diff --git a/app/javascript/dashboard/api/enterprise/account.js b/app/javascript/dashboard/api/enterprise/account.js index bc0d51dfc..bb95335ad 100644 --- a/app/javascript/dashboard/api/enterprise/account.js +++ b/app/javascript/dashboard/api/enterprise/account.js @@ -13,6 +13,10 @@ class EnterpriseAccountAPI extends ApiClient { subscription() { return axios.post(`${this.url}subscription`); } + + getLimits() { + return axios.get(`${this.url}limits`); + } } export default new EnterpriseAccountAPI(); diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index 098826ce3..fc77b24f5 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -10,6 +10,7 @@ export const buildCreatePayload = ({ files, ccEmails = '', bccEmails = '', + toEmails = '', templateParams, }) => { let payload; @@ -25,6 +26,9 @@ export const buildCreatePayload = ({ payload.append('echo_id', echoId); payload.append('cc_emails', ccEmails); payload.append('bcc_emails', bccEmails); + if (toEmails) { + payload.append('to_emails', toEmails); + } } else { payload = { content: message, @@ -33,6 +37,7 @@ export const buildCreatePayload = ({ content_attributes: contentAttributes, cc_emails: ccEmails, bcc_emails: bccEmails, + to_emails: toEmails, template_params: templateParams, }; } @@ -53,6 +58,7 @@ class MessageApi extends ApiClient { files, ccEmails = '', bccEmails = '', + toEmails = '', templateParams, }) { return axios({ @@ -66,6 +72,7 @@ class MessageApi extends ApiClient { files, ccEmails, bccEmails, + toEmails, templateParams, }), }); diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js index 72e433c25..2b816e603 100644 --- a/app/javascript/dashboard/api/integrations.js +++ b/app/javascript/dashboard/api/integrations.js @@ -8,11 +8,19 @@ class IntegrationsAPI extends ApiClient { } connectSlack(code) { - return axios.post(`${this.baseUrl()}/integrations/slack`, { - code: code, + return axios.post(`${this.baseUrl()}/integrations/slack`, { code }); + } + + updateSlack({ referenceId }) { + return axios.patch(`${this.baseUrl()}/integrations/slack`, { + reference_id: referenceId, }); } + listAllSlackChannels() { + return axios.get(`${this.baseUrl()}/integrations/slack/list_all_channels`); + } + delete(integrationId) { return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`); } diff --git a/app/javascript/dashboard/api/integrations/openapi.js b/app/javascript/dashboard/api/integrations/openapi.js index 641cfcfbd..ad203a14c 100644 --- a/app/javascript/dashboard/api/integrations/openapi.js +++ b/app/javascript/dashboard/api/integrations/openapi.js @@ -2,18 +2,62 @@ import ApiClient from '../ApiClient'; +/** + * Represents the data object for a OpenAI hook. + * @typedef {Object} ConversationMessageData + * @property {string} [tone] - The tone of the message. + * @property {string} [content] - The content of the message. + * @property {string} [conversation_display_id] - The display ID of the conversation (optional). + */ + +/** + * A client for the OpenAI API. + * @extends ApiClient + */ class OpenAIAPI extends ApiClient { + /** + * Creates a new OpenAIAPI instance. + */ constructor() { super('integrations', { accountScoped: true }); + + /** + * The conversation events supported by the API. + * @type {string[]} + */ + this.conversation_events = [ + 'summarize', + 'reply_suggestion', + 'label_suggestion', + ]; + + /** + * The message events supported by the API. + * @type {string[]} + */ + this.message_events = ['rephrase']; } + /** + * Processes an event using the OpenAI API. + * @param {Object} options - The options for the event. + * @param {string} [options.type='rephrase'] - The type of event to process. + * @param {string} [options.content] - The content of the event. + * @param {string} [options.tone] - The tone of the event. + * @param {string} [options.conversationId] - The ID of the conversation to process the event for. + * @param {string} options.hookId - The ID of the hook to use for processing the event. + * @returns {Promise} A promise that resolves with the result of the event processing. + */ processEvent({ type = 'rephrase', content, tone, conversationId, hookId }) { + /** + * @type {ConversationMessageData} + */ let data = { tone, content, }; - if (type === 'reply_suggestion' || type === 'summarize') { + if (this.conversation_events.includes(type)) { data = { conversation_display_id: conversationId, }; diff --git a/app/javascript/dashboard/api/specs/integrations.spec.js b/app/javascript/dashboard/api/specs/integrations.spec.js index 05391ceb6..890cc7c84 100644 --- a/app/javascript/dashboard/api/specs/integrations.spec.js +++ b/app/javascript/dashboard/api/specs/integrations.spec.js @@ -11,7 +11,9 @@ describe('#integrationAPI', () => { expect(integrationAPI).toHaveProperty('update'); expect(integrationAPI).toHaveProperty('delete'); expect(integrationAPI).toHaveProperty('connectSlack'); - expect(integrationAPI).toHaveProperty('createHook'); + expect(integrationAPI).toHaveProperty('updateSlack'); + expect(integrationAPI).toHaveProperty('updateSlack'); + expect(integrationAPI).toHaveProperty('listAllSlackChannels'); expect(integrationAPI).toHaveProperty('deleteHook'); }); describeWithAPIMock('API calls', context => { @@ -26,6 +28,24 @@ describe('#integrationAPI', () => { ); }); + it('#updateSlack', () => { + const updateObj = { referenceId: 'SDFSDGSVE' }; + integrationAPI.updateSlack(updateObj); + expect(context.axiosMock.patch).toHaveBeenCalledWith( + '/api/v1/integrations/slack', + { + reference_id: updateObj.referenceId, + } + ); + }); + + it('#listAllSlackChannels', () => { + integrationAPI.listAllSlackChannels(); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v1/integrations/slack/list_all_channels' + ); + }); + it('#delete', () => { integrationAPI.delete(2); expect(context.axiosMock.delete).toHaveBeenCalledWith( diff --git a/app/javascript/dashboard/assets/images/no-chat-dark.svg b/app/javascript/dashboard/assets/images/no-chat-dark.svg new file mode 100644 index 000000000..924b07d07 --- /dev/null +++ b/app/javascript/dashboard/assets/images/no-chat-dark.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/assets/images/no-chat.svg b/app/javascript/dashboard/assets/images/no-chat.svg new file mode 100644 index 000000000..44c5a1677 --- /dev/null +++ b/app/javascript/dashboard/assets/images/no-chat.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/assets/scss/_date-picker.scss b/app/javascript/dashboard/assets/scss/_date-picker.scss index 4377480c9..920fb8fa2 100644 --- a/app/javascript/dashboard/assets/scss/_date-picker.scss +++ b/app/javascript/dashboard/assets/scss/_date-picker.scss @@ -1,37 +1,64 @@ @import '~vue2-datepicker/scss/index'; .mx-datepicker-popup { - z-index: 99999; + @apply z-[99999]; } .date-picker { &.no-margin { .mx-input { - margin-bottom: 0; + @apply mb-0; } } &:not(.auto-width) { .mx-datepicker-range { - width: 320px; + @apply w-[320px]; } } .mx-datepicker { - width: 100%; + @apply w-full; } .mx-input { - border: 1px solid var(--s-200); - border-radius: var(--border-radius-normal); - box-shadow: none; - display: flex; - height: 4.0rem; + @apply h-[2.5rem] flex border border-solid border-slate-200 dark:border-slate-600 rounded-md shadow-none; } .mx-input:disabled, .mx-input[readonly] { - background-color: var(--white); - cursor: pointer; + @apply bg-white dark:bg-slate-900 cursor-pointer; + } +} + +.mx-calendar-content .cell:hover { + @apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100; +} + +.mx-datepicker-inline { + @apply w-full; + + .mx-calendar { + @apply w-full; + } + + .cell.disabled { + @apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300; + } + + .mx-time-item.disabled { + @apply bg-slate-25 dark:bg-slate-900; + } + + .today { + @apply font-semibold; + } + + .mx-datepicker-main { + @apply border-0 bg-white dark:bg-slate-800; + } + + .mx-time-header { + @apply border-0; } } diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index 5753e7593..8230d1f45 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -8,7 +8,7 @@ } select { - height: 4.0rem; + height: 2.5rem; } .card { @@ -16,19 +16,6 @@ select { padding: var(--space-normal); } -.button-wrapper .button.grey-btn { - margin-left: var(--space-normal); -} - -.tooltip { - background-color: var(--black-transparent); - border-radius: $space-smaller; - font-size: $font-size-mini; - max-width: var(--space-giga); - padding: $space-smaller $space-small; - z-index: 999; -} - code { border: 0; font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', @@ -39,6 +26,7 @@ code { background: $color-background; border-radius: var(--border-radius-large); padding: $space-two; + @apply bg-slate-50 dark:bg-slate-600 text-slate-800 dark:text-slate-100; } } diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index a6e0b1aa3..3fd834cdd 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -48,7 +48,7 @@ // Disable contrast warnings in Foundation. $contrast-warnings: false; -$global-font-size: 10px; +$global-font-size: 16px; $global-width: 100%; $global-lineheight: 1.5; $foundation-palette: (primary: $color-woot, @@ -63,7 +63,7 @@ $black: #000; $white: #fff; $body-background: $white; $body-font-color: $color-body; -$body-font-family: 'Inter', +$body-font-family: 'PlusJakarta', -apple-system, system-ui, BlinkMacSystemFont, @@ -117,7 +117,7 @@ $header-font-style: normal; $font-family-monospace: $body-font-family; $header-color: $color-heading; $header-lineheight: 1.4; -$header-margin-bottom: 0.5rem; +$header-margin-bottom: 0.3125rem; $header-styles: (small: ("h1": ("font-size": 24), "h2": ("font-size": 20), "h3": ("font-size": 19), @@ -133,7 +133,7 @@ $header-styles: (small: ("h1": ("font-size": 24), $header-text-rendering: optimizeLegibility; $small-font-size: 80%; $header-small-font-color: $medium-gray; -$paragraph-lineheight: 1.45; +$paragraph-lineheight: 1.65; $paragraph-margin-bottom: var(--space-small); $paragraph-text-rendering: optimizeLegibility; $code-color: $black; @@ -153,11 +153,11 @@ $list-lineheight: $paragraph-lineheight; $list-margin-bottom: $paragraph-margin-bottom; $list-style-type: disc; $list-style-position: outside; -$list-side-margin: 1.25rem; -$list-nested-side-margin: 1.25rem; -$defnlist-margin-bottom: 1rem; +$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.3rem; +$defnlist-term-margin-bottom: 0.1875rem; $blockquote-color: $dark-gray; $blockquote-padding: rem-calc(9 20 0 19); $blockquote-border: 1px solid $medium-gray; @@ -179,9 +179,9 @@ $lead-lineheight: 1.6; $subheader-lineheight: 1.4; $subheader-color: $dark-gray; $subheader-font-weight: $global-weight-normal; -$subheader-margin-top: 0.2rem; -$subheader-margin-bottom: 0.5rem; -$stat-font-size: 2.5rem; +$subheader-margin-top: 0.125rem; +$subheader-margin-bottom: 0.3125rem; +$stat-font-size: 1.5625rem; // 6. Abide // -------- @@ -202,11 +202,11 @@ $accordion-plusminus: true; $accordion-title-font-size: rem-calc(12); $accordion-item-color: $primary-color; $accordion-item-background-hover: $light-gray; -$accordion-item-padding: 1.25rem 1rem; +$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: 1rem; +$accordion-content-padding: 0.625rem; // 8. Accordion Menu // ----------------- @@ -234,7 +234,7 @@ $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.75rem; +$breadcrumbs-item-margin: 0.46875rem; $breadcrumbs-item-uppercase: true; $breadcrumbs-item-slash: true; @@ -275,8 +275,8 @@ $buttongroup-radius-on-each: false; $callout-background: $white; $callout-background-fade: 85%; $callout-border: 1px solid rgba($black, 0.25); -$callout-margin: 0 0 1rem 0; -$callout-padding: 1rem; +$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; @@ -320,10 +320,10 @@ $drilldown-background: $white; // 17. Dropdown // ------------ -$dropdown-padding: 1rem; +$dropdown-padding: 0.625rem; $dropdown-background: $body-background; $dropdown-border: 1px solid $medium-gray; -$dropdown-font-size: 1rem; +$dropdown-font-size: 0.625rem; $dropdown-width: 300px; $dropdown-radius: $global-radius; $dropdown-sizes: (tiny: 100px, @@ -354,7 +354,7 @@ $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: 1rem; +$input-prefix-padding: 0.625rem; $form-label-color: $color-body; $form-label-font-size: rem-calc(14); $form-label-font-weight: $font-weight-medium; @@ -406,14 +406,14 @@ $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.25rem; +$menu-icon-spacing: 0.15625rem; $menu-item-background-hover: $light-gray; $menu-border: $light-gray; // 23. Meter // --------- -$meter-height: 1rem; +$meter-height: 0.625rem; $meter-radius: $global-radius; $meter-background: $medium-gray; $meter-fill-good: $success-color; @@ -423,11 +423,11 @@ $meter-fill-bad: $alert-color; // 24. Off-canvas // -------------- -$offcanvas-sizes: (small: 23rem, - medium: 23rem, +$offcanvas-sizes: (small: 14.375, + medium: 14.375, ); -$offcanvas-vertical-sizes: (small: 23rem, - medium: 23rem, +$offcanvas-vertical-sizes: (small: 14.375, + medium: 14.375, ); $offcanvas-background: $light-gray; $offcanvas-shadow: 0 0 10px rgba($black, 0.7); @@ -445,14 +445,14 @@ $maincontent-class: 'off-canvas-content'; $orbit-bullet-background: $medium-gray; $orbit-bullet-background-active: $dark-gray; -$orbit-bullet-diameter: 1.2rem; -$orbit-bullet-margin: 0.1rem; -$orbit-bullet-margin-top: 0.8rem; -$orbit-bullet-margin-bottom: 0.8rem; +$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: 1rem; +$orbit-caption-padding: 0.625rem; $orbit-control-background-hover: rgba($black, 0.5); -$orbit-control-padding: 1rem; +$orbit-control-padding: 0.625rem; $orbit-control-zindex: 10; // 26. Pagination @@ -476,7 +476,7 @@ $pagination-arrows: true; // 27. Progress Bar // ---------------- -$progress-height: 1rem; +$progress-height: 0.625rem; $progress-background: $medium-gray; $progress-margin-bottom: $global-margin; $progress-meter-background: $primary-color; @@ -504,13 +504,13 @@ $reveal-overlay-background: rgba($black, 0.45); // 30. Slider // ---------- -$slider-width-vertical: 0.5rem; +$slider-width-vertical: 0.3125rem; $slider-transition: all 0.2s ease-in-out; -$slider-height: 0.5rem; +$slider-height: 0.3125rem; $slider-background: $light-gray; $slider-fill-background: $medium-gray; -$slider-handle-height: 1.4rem; -$slider-handle-width: 1.4rem; +$slider-handle-height: 0.875rem; +$slider-handle-width: 0.875rem; $slider-handle-background: $primary-color; $slider-opacity-disabled: 0.25; $slider-radius: $global-radius; @@ -569,7 +569,7 @@ $tab-expand-max: 6; $tab-content-background: transparent; $tab-content-border: transparent; $tab-content-color: foreground($tab-background, $primary-color); -$tab-content-padding: 1rem; +$tab-content-padding: 0.625rem; // 34. Thumbnail // ------------- @@ -586,11 +586,11 @@ $thumbnail-radius: $global-radius; $titlebar-background: $black; $titlebar-color: $white; -$titlebar-padding: 0.5rem; +$titlebar-padding: 0.3125rem; $titlebar-text-font-weight: bold; $titlebar-icon-color: $white; $titlebar-icon-color-hover: $medium-gray; -$titlebar-icon-spacing: 0.25rem; +$titlebar-icon-spacing: 0.15625rem; // 36. Tooltip // ----------- @@ -599,19 +599,19 @@ $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.75rem; +$tooltip-padding: 0.46875rem; $tooltip-font-size: $font-size-mini; -$tooltip-pip-width: 0.75rem; +$tooltip-pip-width: 0.46875rem; $tooltip-pip-height: $tooltip-pip-width * 0.866; $tooltip-radius: $global-radius; // 37. Top Bar // ----------- -$topbar-padding: 0.5rem; +$topbar-padding: 0.3125; $topbar-background: $light-gray; $topbar-submenu-background: $topbar-background; -$topbar-title-spacing: 0.5rem 1rem 0.5rem 0; +$topbar-title-spacing: 0.3125 0.625rem 0.3125 0; $topbar-input-width: 200px; $topbar-unstack-breakpoint: medium; diff --git a/app/javascript/dashboard/assets/scss/_helper-classes.scss b/app/javascript/dashboard/assets/scss/_helper-classes.scss index a8a5a0b93..0b400c4e9 100644 --- a/app/javascript/dashboard/assets/scss/_helper-classes.scss +++ b/app/javascript/dashboard/assets/scss/_helper-classes.scss @@ -1,5 +1,5 @@ .bg-light { - @include background-light; + @apply bg-slate-25 dark:bg-slate-800; } .flex-center { diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss index 41ba95276..2fbb9fa7c 100644 --- a/app/javascript/dashboard/assets/scss/_mixins.scss +++ b/app/javascript/dashboard/assets/scss/_mixins.scss @@ -22,43 +22,43 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7); } @mixin border-normal() { - border: 1px solid $color-border; + @apply border border-slate-50 dark:border-slate-700; } @mixin border-normal-left() { - border-left: 1px solid $color-border; + @apply border-l border-slate-50 dark:border-slate-700; } @mixin border-normal-top() { - border-top: 1px solid $color-border; + @apply border-t border-slate-50 dark:border-slate-700; } @mixin border-normal-right() { - border-right: 1px solid $color-border; + @apply border-r border-slate-50 dark:border-slate-700; } @mixin border-normal-bottom() { - border-bottom: 1px solid $color-border; + @apply border-b border-slate-50 dark:border-slate-700; } @mixin border-light() { - border: 1px solid $color-border-light; + @apply border border-slate-25 dark:border-slate-700; } @mixin border-light-left() { - border-left: 1px solid $color-border-light; + @apply border-l border-slate-25 dark:border-slate-700; } @mixin border-light-top() { - border-top: 1px solid $color-border-light; + @apply border-t border-slate-25 dark:border-slate-700; } @mixin border-light-right() { - border-right: 1px solid $color-border-light; + @apply border-r border-slate-25 dark:border-slate-700; } @mixin border-light-bottom() { - border-bottom: 1px solid $color-border-light; + @apply border-b border-slate-25 dark:border-slate-700; } // background @@ -67,11 +67,11 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7); } @mixin background-light() { - background: $color-background-light; + @apply bg-slate-50 dark:bg-slate-800; } @mixin background-white() { - background: $color-white; + @apply bg-white dark:bg-slate-900; } // input form @@ -237,8 +237,8 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7); white-space: nowrap; } -@mixin three-column-grid($column-one-width: 25.6rem, - $column-three-width: 25.6rem) { +@mixin three-column-grid($column-one-width: 16rem, + $column-three-width: 16rem) { width: 100%; height: 100%; display: grid; diff --git a/app/javascript/dashboard/assets/scss/_rtl.scss b/app/javascript/dashboard/assets/scss/_rtl.scss index 4f7efaab9..14897b245 100644 --- a/app/javascript/dashboard/assets/scss/_rtl.scss +++ b/app/javascript/dashboard/assets/scss/_rtl.scss @@ -1,90 +1,6 @@ .app-rtl--wrapper { direction: rtl; - // Primary sidebar - .primary--sidebar { - border-left: 1px solid var(--s-50); - border-right: 0; - - .options-menu.dropdown-pane { - right: var(--space-smaller); - - .auto-offline--toggle { - padding: var(--space-smaller) var(--space-one) var(--space-smaller) - var(--space-smaller); - } - - .status-items .button { - text-align: right; - } - } - } - - // Secondary sidebar - .secondary-sidebar { - .secondary-menu { - border-left: 1px solid var(--s-50); - border-right: 0; - - .nested.vertical.menu { - .badge--icon { - margin-left: var(--space-smaller); - margin-right: unset; - } - - .menu-label { - text-align: right; - } - } - - .secondary-menu--icon { - margin-left: var(--space-smaller); - margin-right: unset; - } - - .account-context--group .account-context--switch-group { - --overlay-shadow: linear-gradient( - to left, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 1) 50% - ); - background-image: var(--overlay-shadow); - } - - // Help center sidebar - .sidebar-header--wrap .header-title--wrap { - margin-left: unset; - margin-right: var(--space-small); - } - } - } - - // Woot button - .button { - .icon--emoji + .button__content { - padding-left: 0; - padding-right: var(--space-small); - } - - .icon--font + .button__content { - padding-left: 0; - padding-right: var(--space-small); - } - - .icon + .button__content { - padding-left: 0; - padding-right: var(--space-small); - } - } - - // Settings header - .settings-header { - .header--icon { - margin-left: var(--space-small); - margin-right: var(--space-smaller); - } - } - .header-section.back-button { direction: initial; margin-left: var(--space-normal); @@ -133,23 +49,6 @@ // Conversation details .conversation-details-wrap { - .conv-header { - .user { - margin-left: var(--space-normal); - margin-right: unset; - - .user--profile__meta { - margin-left: unset; - margin-right: var(--space-small); - } - } - - .actions--container .resolve-actions { - margin-left: unset; - margin-right: var(--space-small); - } - } - .conversation-panel { // Message text .text-content { @@ -197,11 +96,6 @@ } } - // Conversation sidebar toggle button - .sidebar-toggle--button { - transform: rotate(180deg); - } - // Conversation sidebar close button .close-button--rtl { transform: rotate(180deg); @@ -345,31 +239,6 @@ } } - // scss-lint:disable SelectorDepth - .container .header-wrap .header-left-wrap .header-left-wrap > .page-title { - margin-right: var(--space-small); - } - - .portal-container .container { - margin-left: unset !important; - margin-right: var(--space-small); - - .configuration-items--wrap { - margin-left: var(--space-mega); - margin-right: unset !important; - } - - thead th { - padding-left: var(--space-small); - padding-right: 0; - } - - tbody td { - padding-left: var(--space-small); - padding-right: 0; - } - } - .portal-popover__container .portal { .actions-container { margin-left: unset; @@ -439,7 +308,7 @@ } span { - --minus-space-one-point-five: -1.5rem; + --minus-space-one-point-five: -0.9375rem; &.active { transform: translate( @@ -473,11 +342,6 @@ direction: initial; } - .inbox--name .inbox--icon { - margin-left: var(--space-micro); - margin-right: 0; - } - .colorpicker--chrome { direction: initial; } diff --git a/app/javascript/dashboard/assets/scss/_typography.scss b/app/javascript/dashboard/assets/scss/_typography.scss index 7db0f2a46..ee35a9cf0 100644 --- a/app/javascript/dashboard/assets/scss/_typography.scss +++ b/app/javascript/dashboard/assets/scss/_typography.scss @@ -29,4 +29,5 @@ a { 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 index 06c963a93..a8025ba66 100644 --- a/app/javascript/dashboard/assets/scss/_utility-helpers.scss +++ b/app/javascript/dashboard/assets/scss/_utility-helpers.scss @@ -43,17 +43,13 @@ } .border-right { - border-right: 1px solid var(--color-border); + @apply border-r border-slate-50 dark:border-slate-700; } .border-left { border-left: 1px solid var(--color-border); } -.bg-white { - background-color: var(--white); -} - .text-ellipsis { overflow: hidden; text-overflow: ellipsis; diff --git a/app/javascript/dashboard/assets/scss/_variables.scss b/app/javascript/dashboard/assets/scss/_variables.scss index f24f00830..930fe5f0e 100644 --- a/app/javascript/dashboard/assets/scss/_variables.scss +++ b/app/javascript/dashboard/assets/scss/_variables.scss @@ -1,30 +1,30 @@ // Font sizes -$font-size-nano: 0.8rem; -$font-size-micro: 1.0rem; -$font-size-mini: 1.2rem; -$font-size-small: 1.4rem; -$font-size-default: 1.6rem; -$font-size-medium: 1.8rem; -$font-size-large: 2.2rem; -$font-size-big: 2.4rem; -$font-size-bigger: 3.0rem; -$font-size-mega: 3.4rem; -$font-size-giga: 4.0rem; +$font-size-nano: 0.5rem; +$font-size-micro: 0.675rem; +$font-size-mini: 0.75rem; +$font-size-small: 0.875rem; +$font-size-default: 1rem; +$font-size-medium: 1.125rem; +$font-size-large: 1.375rem; +$font-size-big: 1.5rem; +$font-size-bigger: 1.75rem; +$font-size-mega: 2.125rem; +$font-size-giga: 2.5rem; // spaces $zero: 0; -$space-micro: 0.2rem; -$space-smaller: 0.4rem; -$space-small: 0.8rem; -$space-one: 1rem; -$space-slab: 1.2rem; -$space-normal: 1.6rem; -$space-two: 2.0rem; -$space-medium: 2.4rem; -$space-large: 3.2rem; -$space-larger: 4.8rem; -$space-jumbo: 6.4rem; -$space-mega: 10.0rem; +$space-micro: 0.125rem; +$space-smaller: 0.25rem; +$space-small: 0.5rem; +$space-one: 0.675rem; +$space-slab: 0.75rem; +$space-normal: 1rem; +$space-two: 1.25rem; +$space-medium: 1.5rem; +$space-large: 2rem; +$space-larger: 3rem; +$space-jumbo: 4rem; +$space-mega: 6.25rem; // font-weight $font-weight-feather: 100; @@ -35,8 +35,8 @@ $font-weight-bold: 600; $font-weight-black: 700; //Navbar -$nav-bar-width: 23rem; -$header-height: 5.6rem; +$nav-bar-width: 14.375rem; +$header-height: 3.5rem; $woot-logo-padding: $space-large $space-two; @@ -71,20 +71,20 @@ $color-primary-light: #c7e3ff; $color-primary-dark: darken($color-woot, 20%); // Thumbnail -$thumbnail-radius: 4rem; +$thumbnail-radius: 2.5rem; // chat-header -$conv-header-height: 4rem; +$conv-header-height: 2.5rem; // Inbox List -$inbox-thumb-size: 4.8rem; +$inbox-thumb-size: 3rem; // Spinner $spinkit-spinner-color: $color-white !default; -$spinkit-spinner-margin: 0 0 0 1.6rem !default; -$spinkit-size: 1.6rem !default; +$spinkit-spinner-margin: 0 0 0 1rem !default; +$spinkit-size: 1rem !default; // Snackbar default $woot-snackbar-bg: #323232; @@ -101,5 +101,5 @@ $ionicons-font-path: '~ionicons/fonts'; $transition-ease-in: all 0.250s ease-in; :root { - --dashboard-app-tabs-height: 3.9rem; + --dashboard-app-tabs-height: 2.4375rem; } diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index ce06475a7..2773ced6f 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -1,4 +1,8 @@ -@import 'shared/assets/fonts/inter'; +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; + +@import 'shared/assets/fonts/plus-jakarta'; @import 'shared/assets/stylesheets/animations'; @import 'shared/assets/stylesheets/colors'; @import 'shared/assets/stylesheets/spacing'; @@ -67,3 +71,7 @@ @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; +} diff --git a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss index 8513053b8..100662394 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss @@ -7,6 +7,7 @@ z-index: var(--z-index-very-high); &.dropdown-pane--open { + @apply bg-white dark:bg-slate-800; display: block; visibility: visible; } diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index f111a414f..fbeecbc5e 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -1,93 +1,94 @@ @mixin label-multiselect-hover { &::after { - color: $color-primary-dark; + @apply text-woot-600 dark:text-woot-600; } &:hover { - background: $color-background; + @apply bg-slate-50 dark:bg-slate-700; &::after { - color: $color-woot; + @apply text-woot-500 dark:text-woot-500; } } } .multiselect { &:not(.no-margin) { - margin-bottom: var(--space-normal); + @apply mb-4; } &.multiselect--disabled { - opacity: 0.8; + @apply opacity-50 border border-slate-200 dark:border-slate-600 rounded-md cursor-not-allowed; + + .multiselect__select { + @apply cursor-not-allowed bg-white dark:bg-slate-900 rounded-md; + } + + .multiselect__tags { + @apply border-0; + } } .multiselect--active { - >.multiselect__tags { - border-color: var(--w-500); + > .multiselect__tags { + @apply border-woot-500 dark:border-woot-500; } } .multiselect__select { - min-height: 4.6rem; - padding: 0; - right: 0; - top: 0; + @apply min-h-[2.875rem] p-0 right-0 top-0; &::before { - right: 0; + @apply right-0; } } + .multiselect__content-wrapper { + @apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 text-slate-800 dark:text-slate-100; + } + .multiselect__content { - max-width: 100%; + @apply max-w-full; .multiselect__option { - font-size: $font-size-small; - font-weight: $font-weight-normal; + @apply text-sm font-normal; span { - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: fit-content; + @apply inline-block overflow-hidden text-ellipsis whitespace-nowrap w-fit; } p { - margin-bottom: 0; + @apply mb-0; } &.multiselect__option--highlight { - background: var(--white); - color: var(--color-body); + @apply bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-100; } &.multiselect__option--highlight:hover { - background: var(--w-50); - color: var(--color-body); + @apply bg-woot-50 dark:bg-woot-600 text-slate-800 dark:text-slate-100; &::after { - background: var(--w-50); - color: var(--s-600); + @apply bg-woot-50 dark:bg-woot-600 text-slate-600 dark:text-slate-200; } } &.multiselect__option--highlight::after { - background: transparent; + @apply bg-transparent; } &.multiselect__option--selected { - background: var(--w-75); + @apply bg-woot-50 dark:bg-woot-600 text-slate-800 dark:text-slate-100; &.multiselect__option--highlight:hover { - background: var(--w-75); + @apply bg-woot-75 dark:bg-woot-600; &::after { - background: transparent; + @apply bg-transparent; } &::after:hover { - color: var(--color-body); + @apply text-slate-800 dark:text-slate-100; } } } @@ -95,175 +96,130 @@ } .multiselect__tags { - border: 1px solid var(--s-200); - border-color: var(--s-200); - margin: 0; - min-height: 4.4rem; - padding-top: $zero; + @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; } .multiselect__tags-wrap { - display: inline-block; - line-height: 1; - margin-top: $space-smaller; + @apply inline-block leading-none mt-1; } .multiselect__placeholder { - color: $color-gray; - font-weight: $font-weight-normal; - padding-top: var(--space-slab); + @apply text-slate-400 dark:text-slate-400 font-normal pt-3; } .multiselect__tag { - $vertical-space: $space-smaller + $space-micro; - background: $color-background; - color: $color-heading; - margin-top: $space-smaller; - padding: $vertical-space $space-medium $vertical-space $space-one; + @apply bg-slate-50 dark:bg-slate-800 mt-1 text-slate-800 dark:text-slate-100 pr-6 pl-2.5 py-1.5; } .multiselect__tag-icon { @include label-multiselect-hover; - line-height: $space-medium + $space-micro; } .multiselect__input { @include ghost-input; - font-size: $font-size-small; - height: 4.4rem; - margin-bottom: $zero; - padding: 0; + @apply text-sm h-[2.875rem] mb-0 p-0; } .multiselect__single { - @include text-ellipsis; - display: inline-block; - margin-bottom: 0; - padding: var(--space-slab) var(--space-one); + @apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 inline-block mb-0 py-3 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis; } } .sidebar-labels-wrap { - &.has-edited, &:hover { .multiselect { - cursor: pointer; + @apply cursor-pointer; } } .multiselect { - >.multiselect__select { - visibility: hidden; + > .multiselect__select { + @apply invisible; } - >.multiselect__tags { - border-color: transparent; + > .multiselect__tags { + @apply border-transparent; } - &.multiselect--active>.multiselect__tags { - border-color: $color-woot; + &.multiselect--active > .multiselect__tags { + @apply border-woot-500 dark:border-woot-500; } } } .multiselect-wrap--small { - $multiselect-height: 4.0rem; - .multiselect__tags, .multiselect__input { - align-items: center; - display: flex; + @apply items-center flex; } .multiselect__tags, .multiselect__input, .multiselect { - background: var(--white); - font-size: var(--font-size-small); - height: $multiselect-height; - min-height: $multiselect-height; + @apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm h-10 min-h-[2.5rem]; } .multiselect__input { - height: $multiselect-height - $space-micro; - min-height: $multiselect-height - $space-micro; + @apply h-[2.375rem] min-h-[2.375rem]; } .multiselect__single { - align-items: center; - display: flex; - font-size: var(--font-size-small); - margin: 0; - max-height: 3.8rem; - padding: var(--space-smaller) var(--space-micro); + @apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-1 px-0.5; } .multiselect__placeholder { - margin: 0; - padding: var(--space-smaller) var(--space-micro); + @apply m-0 py-1 px-0.5; } .multiselect__select { - min-height: $multiselect-height; + @apply min-h-[2.5rem]; } .multiselect--disabled .multiselect__current, .multiselect--disabled .multiselect__select { - background: transparent; + @apply bg-transparent; } .multiselect__tags-wrap { - flex-shrink: 0; + @apply flex-shrink-0; } } .multiselect-wrap--medium { - $multiselect-height: 4.8rem; - .multiselect__tags, .multiselect__input { - align-items: center; - display: flex; + @apply items-center flex; } .multiselect__tags, .multiselect__input, .multiselect { - background: var(--white); - font-size: var(--font-size-small); - height: $multiselect-height; - min-height: $multiselect-height; + @apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 text-sm h-12 min-h-[3rem]; } .multiselect__input { - height: $multiselect-height - $space-micro; - min-height: $multiselect-height - $space-micro; + @apply h-[2.875rem] min-h-[2.875rem]; } .multiselect__single { - align-items: center; - display: flex; - font-size: var(--font-size-small); - margin: 0; - padding: var(--space-smaller) var(--space-micro); + @apply items-center flex m-0 text-sm py-1 px-0.5 text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900; } .multiselect__placeholder { - margin: 0; - padding: var(--space-smaller) var(--space-micro); + @apply m-0 py-1 px-0.5; } .multiselect__select { - min-height: $multiselect-height; + @apply min-h-[3rem]; } .multiselect--disabled .multiselect__current, .multiselect--disabled .multiselect__select { - background: transparent; + @apply bg-transparent; } .multiselect__tags-wrap { - flex-shrink: 0; + @apply flex-shrink-0; } } diff --git a/app/javascript/dashboard/assets/scss/storybook.scss b/app/javascript/dashboard/assets/scss/storybook.scss index e9973b150..e735ed3d7 100644 --- a/app/javascript/dashboard/assets/scss/storybook.scss +++ b/app/javascript/dashboard/assets/scss/storybook.scss @@ -1,4 +1,5 @@ @import 'shared/assets/fonts/inter'; +@import 'shared/assets/fonts/plus-jakarta'; @import 'shared/assets/stylesheets/animations'; @import 'shared/assets/stylesheets/colors'; @import 'shared/assets/stylesheets/spacing'; @@ -9,7 +10,7 @@ @import 'variables'; @import '~spinkit/scss/spinners/7-three-bounce'; -@import '~vue-multiselect/dist/vue-multiselect.min.css'; +@import 'vue-multiselect/dist/vue-multiselect.min.css'; @import '~shared/assets/stylesheets/ionicons'; @import 'mixins'; @@ -29,3 +30,18 @@ @import 'widgets/forms'; @import 'plugins/multiselect'; + +@import 'widget/assets/scss/reset'; +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; +@import 'widget/assets/scss/utilities'; + + +html, +body { + font-family: 'PlusJakarta', sans-serif; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + height: 100%; +} diff --git a/app/javascript/dashboard/assets/scss/super_admin/pages.scss b/app/javascript/dashboard/assets/scss/super_admin/pages.scss index 8e5b86989..a33da2693 100644 --- a/app/javascript/dashboard/assets/scss/super_admin/pages.scss +++ b/app/javascript/dashboard/assets/scss/super_admin/pages.scss @@ -1,3 +1,3 @@ -@import 'shared/assets/fonts/inter'; +@import 'shared/assets/fonts/plus-jakarta'; @import '../variables'; @import '~shared/assets/stylesheets/ionicons'; diff --git a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss index 870caf7ee..39e4ed2bd 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss @@ -1,173 +1,112 @@ .settings { - overflow: auto; -} - -// Conversation header - Light BG -.settings-header { - @include background-white; - @include flex; - @include flex-align($x: justify, $y: middle); - border-bottom: 1px solid var(--s-50); - height: $header-height; - min-height: $header-height; - padding: $space-small $space-normal; - - // Resolve Button - .button { - margin: 0; - } - - // User thumbnail and text - .page-title { - @include flex; - @include flex-align($x: center, $y: middle); - margin: 0; - } + @apply overflow-auto; } .wizard-box { .item { - @include background-light; - - cursor: pointer; - padding: $space-normal $space-normal $space-normal $space-medium; - position: relative; + @apply cursor-pointer py-4 pr-4 pl-6 relative; &::before, &::after { - background: $color-border; - content: ''; - height: 100%; - position: absolute; - top: $space-normal; - width: 2px; + @apply bg-slate-75 dark:bg-slate-600 content-[''] h-full absolute top-5 w-0.5; } &::before { - height: $space-normal; - top: $zero; + @apply h-4 top-0; } &:first-child { &::before { - height: 0; + @apply h-0; } } &:last-child { &::after { - height: $zero; + @apply h-0; } } &.active { h3 { - color: $color-woot; + @apply text-woot-500 dark:text-woot-500; } .step { - background: $color-woot; + @apply bg-woot-500 dark:bg-woot-500; } } &.over { &::after { - background: $color-woot; + @apply bg-woot-500 dark:bg-woot-500; } .step { - background: $color-woot; + @apply bg-woot-500 dark:bg-woot-500; } & + .item { &::before { - background: $color-woot; + @apply bg-woot-500 dark:bg-woot-500; } } } h3 { - color: $color-body; - font-size: $font-size-default; - line-height: 1; - padding-left: $space-medium; + @apply text-slate-800 dark:text-slate-100 text-base pl-6; } .completed { - color: $success-color; - margin-left: $space-smaller; + @apply text-green-500 dark:text-green-500 ml-1; } p { - color: $color-light-gray; - font-size: $font-size-small; - margin: 0; - padding-left: $space-medium; + @apply text-slate-600 dark:text-slate-300 text-sm m-0 pl-6; } .step { - background: $color-border; - border-radius: 20px; - color: $color-white; - font-size: $font-size-micro; - font-weight: $font-weight-medium; - height: $space-normal; - left: $space-normal; - line-height: $space-normal; - position: absolute; - text-align: center; - top: $space-normal; - width: $space-normal; - z-index: 999; + @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 { - font-size: $font-size-micro; + @apply text-xxs; } } } } .wizard-body { - @include background-white; - @include border-light; - @include full-height(); - padding: $space-medium; + @apply border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6; &.height-auto { - height: auto; + @apply h-auto; } } .settings--content { - margin: $space-small $space-large; + @apply my-2 mx-8; .title { - font-weight: $font-weight-medium; + @apply font-medium; } .code { - background: $color-background; - max-height: $space-mega; - overflow: auto; - padding: $space-one; - white-space: nowrap; + @apply bg-slate-50 dark:bg-slate-800 overflow-auto p-2.5 whitespace-nowrap; code { - background: transparent; - border: 0; + @apply bg-transparent border-0; } } } .login-init { - padding-top: 30%; - text-align: center; + @apply pt-[30%] text-center; p { - padding: $space-medium; + @apply p-6; } > a > img { - width: $space-larger * 5; + @apply w-60; } } diff --git a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss index 590890499..8a118b906 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss @@ -1,41 +1 @@ -.integrations-wrap { - .integration { - background: $color-white; - border: 1px solid $color-border; - border-radius: $space-smaller; - margin-bottom: $space-normal; - padding: $space-normal; - - .integration--image { - display: flex; - height: 10rem; - width: 10rem; - - img { - max-width: 100%; - padding: $space-medium; - } - } - - .integration--type { - display: flex; - flex-direction: column; - justify-content: center; - margin: 0 var(--space-normal); - } - - .integration--title { - font-size: var(--font-size-large); - } - - .button-wrap { - @include flex; - @include flex-align(center, middle); - margin-bottom: 0; - } - } -} - -.help-wrap { - padding-left: $space-large; -} +// to be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 58945ffb5..ab41dafdf 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -1,207 +1,201 @@ -$default-button-height: 4.0rem; - .button { - align-items: center; - display: inline-flex; - height: $default-button-height; - margin-bottom: 0; + @apply items-center inline-flex h-10 mb-0; .button__content { - width: 100%; + @apply w-full; + + img, + svg { + @apply inline-block; + } } .spinner { - padding: 0 var(--space-small); + @apply px-2 py-0; } - .icon--emoji+.button__content { - padding-left: var(--space-small); + .icon--emoji + .button__content { + @apply pl-2 rtl:pr-2 rtl:pl-0; } - .icon--font+.button__content { - padding-left: var(--space-small); + .icon--font + .button__content { + @apply pl-2 rtl:pr-2 rtl:pl-0; } // @TODDO - Remove after moving all buttons to woot-button - .icon+.button__content { - padding-left: var(--space-small); - width: auto; + .icon + .button__content { + @apply pl-2 w-auto rtl:pr-2 rtl:pl-0; } &.expanded { - display: flex; - justify-content: center; + @apply flex justify-center text-center; } &.round { - border-radius: $space-larger; + @apply rounded-full; } // @TODO Use with link &.compact { - padding-bottom: 0; - padding-top: 0; + @apply pb-0 pt-0; } &.hollow { - border-color: var(--s-200); - color: var(--w-700); + @apply border border-slate-200 dark:border-slate-600 text-woot-700 dark:text-woot-100 hover:bg-woot-50 dark:hover:bg-woot-900; &.secondary { - border-color: var(--s-200); - color: var(--s-700); + @apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; } &.success { - border-color: var(--s-200); - color: var(--g-700); + @apply text-green-700 dark:text-green-100 hover:bg-green-50 dark:hover:bg-green-800; } &.alert { - border-color: var(--s-200); - color: var(--r-700); + @apply text-red-700 dark:text-red-100 hover:bg-red-50 dark:hover:bg-red-800; } &.warning { - border-color: var(--s-200); - color: var(--y-700); + @apply text-yellow-700 dark:text-yellow-100 hover:bg-yellow-50 dark:hover:bg-yellow-800; } &:hover { - background: var(--s-75); - border-color: var(--s-100); + @apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700; &.secondary { - border-color: var(--s-100); + @apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100; } &.success { - border-color: var(--s-100); + @apply border-slate-100 dark:border-slate-700 text-green-800 dark:text-green-100; } &.alert { - border-color: var(--s-100); + @apply border-slate-100 dark:border-slate-700 text-red-700 dark:text-red-100; } &.warning { - border-color: var(--s-100); + @apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-700; } } } // Smooth style &.smooth { - @include button-style(var(--w-50), var(--w-100), var(--w-700)); - + @apply bg-woot-50 dark:bg-woot-800 text-woot-700 dark:text-woot-100 hover:text-woot-700 dark:hover:text-woot-700 hover:bg-woot-100 dark:hover:bg-woot-900; &.secondary { - @include button-style(var(--s-50), var(--s-100), var(--s-700)); + @apply bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800; } &.success { - @include button-style(var(--g-50), var(--g-100), var(--g-700)); + @apply bg-green-50 dark:bg-green-700 text-green-700 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100; } &.alert { - @include button-style(var(--r-50), var(--r-100), var(--r-700)); + @apply bg-red-50 dark:bg-red-700 dark:bg-opacity-50 text-red-700 dark:text-red-100 hover:bg-red-100 dark:hover:bg-red-800 dark:hover:bg-opacity-30; } &.warning { - @include button-style(var(--y-100), var(--y-200), var(--y-700)); + @apply bg-yellow-100 dark:bg-yellow-100 text-yellow-700 dark:text-yellow-700 hover:bg-yellow-200 dark:hover:bg-yellow-200; } } &.clear { - color: var(--w-700); + @apply text-woot-500 dark:text-woot-500; &.secondary { - color: var(--s-700); + @apply text-slate-700 dark:text-slate-100; } &.success { - color: var(--g-700); + @apply text-green-700 dark:text-green-100; } &.alert { - color: var(--r-700); + @apply text-red-700 dark:text-red-100; } &.warning { - color: var(--y-700); + @apply text-yellow-700 dark:text-yellow-600; } &:hover { - background: var(--w-50); + @apply hover:bg-woot-50 dark:hover:bg-woot-900/50 hover:text-woot-500 dark:hover:text-woot-100; &.secondary { - background: var(--s-50); + @apply hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-800 dark:hover:text-slate-100; } &.success { - background: var(--g-50); + @apply hover:bg-green-50 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100; } &.alert { - background: var(--r-50); + @apply hover:bg-red-50 dark:hover:bg-red-800 hover:text-red-700 dark:hover:text-red-100; } &.warning { - background: var(--y-50); + @apply hover:bg-yellow-100 dark:hover:bg-yellow-800 hover:text-yellow-700 dark:hover:text-yellow-600; + } + } + + &:active { + &.secondary { + @apply active:bg-slate-100 dark:active:bg-slate-900; + } + } + + &:focus { + &.secondary { + @apply focus:bg-slate-50 dark:focus:bg-slate-700; } } } // Sizes &.tiny { - height: var(--space-medium); + @apply h-6; - .icon+.button__content { - padding-left: var(--space-micro); + .icon + .button__content { + @apply pl-1 rtl:pr-1 rtl:pl-0; } } &.small { - height: var(--space-large); - padding-bottom: var(--space-smaller); - padding-top: var(--space-smaller); + @apply h-8 pb-1 pt-1; - .icon+.button__content { - padding-left: var(--space-smaller); + .icon + .button__content { + @apply pl-1 rtl:pr-1 rtl:pl-0; } } &.large { - height: var(--space-larger); + @apply h-12; } &.button--only-icon { - justify-content: center; - padding-left: 0; - padding-right: 0; - width: $default-button-height; + @apply justify-center pl-0 pr-0 w-10; &.tiny { - width: var(--space-medium); + @apply w-6; } &.small { - width: var(--space-large); + @apply w-8; } &.large { - width: var(--space-larger); + @apply w-12; } } &.link { - height: auto; - margin: 0; - padding: 0; + @apply h-auto m-0 p-0; &:hover { - text-decoration: underline; + @apply underline; } } - } diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss index c352ff540..04f610dd2 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss @@ -1,71 +1 @@ -$resolve-button-width: 13.2rem; - -// Conversation header - Light BG -.conv-header { - @include background-white; - @include flex; - @include flex-align($x: justify, $y: middle); - @include border-normal-bottom; - padding: var(--space-small) var(--space-normal); - - .multiselect-box { - @include flex; - @include flex-align($x: justify, $y: middle); - border: 1px solid var(--color-border); - border-radius: var(--space-smaller); - margin-right: var(--space-small); - width: 21.6rem; - - .icon { - color: $medium-gray; - font-size: $font-size-default; - line-height: 3.8rem; - padding-left: $space-slab; - padding-right: $space-smaller; - } - - .multiselect { - border-radius: var(--border-radius-small); - margin: 0; - min-width: 0; - - .multiselect__tags { - border-color: transparent; - } - } - } - - // User thumbnail and text - .user { - @include flex; - @include flex-align($x: center, $y: middle); - margin-right: var(--space-normal); - min-width: 0; - - .user--profile__meta { - align-items: flex-start; - display: flex; - flex-direction: column; - justify-content: flex-start; - margin-left: var(--space-small); - min-width: 0; - } - } -} - -.header-actions-wrap { - align-items: center; - display: flex; - flex-direction: row; - flex-grow: 1; - justify-content: flex-end; - margin-top: var(--space-small); - - @include breakpoint(medium up) { - margin-top: 0; - } - - &.has-open-sidebar { - justify-content: flex-end; - } -} +// 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 index e94cee45b..d060dc2ee 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss @@ -10,135 +10,7 @@ } .conversation { - @include flex; - @include flex-shrink; - border-bottom: 1px solid transparent; - border-left: var(--space-micro) solid transparent; - border-top: 1px solid transparent; - cursor: pointer; - padding: 0 var(--space-normal); - position: relative; - &.active { animation: left-shift-animation 0.25s $swift-ease-out-function; - background: var(--color-background); - border-bottom-color: var(--color-border-light); - border-left-color: var(--color-woot); - border-top-color: var(--color-border-light); - - .conversation--details { - border-top-color: transparent; - } - - + .conversation .conversation--details { - border-top-color: transparent; - } - } - - &:first-child { - .conversation--details { - border-top-color: transparent; - } - } - - &:last-child { - .conversation--details { - border-bottom-color: var(--color-border-light); - } - } - - .conversation--details { - @include border-light-bottom; - @include border-light-top; - border-bottom-color: transparent; - padding: var(--space-slab) 0; - } - - .conversation--user { - font-size: var(--font-size-small); - margin: 0 var(--space-small); - text-transform: capitalize; - - .label { - left: var(--space-one); - max-width: var(--space-jumbo); - overflow: hidden; - position: relative; - text-overflow: ellipsis; - top: var(--space-one); - white-space: nowrap; - } - } - - .conversation--message { - color: var(--color-body); - font-size: var(--font-size-small); - font-weight: var(--font-weight-normal); - height: var(--space-medium); - line-height: var(--space-medium); - margin: 0 var(--space-small); - max-width: 96%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 27rem; - } - - .conversation--meta { - @include flex; - flex-direction: column; - position: absolute; - right: var(--space-normal); - top: var(--space-normal); - - .unread { - @include round-corner; - @include light-shadow; - background: darken($success-color, 3%); - color: var(--white); - display: none; - font-size: var(--font-size-micro); - font-weight: var(--font-weight-black); - height: var(--space-normal); - line-height: var(--space-normal); - margin-left: auto; - margin-top: var(--space-smaller); - min-width: var(--space-normal); - padding: 0 var(--space-smaller); - text-align: center; - } - - .timestamp { - color: $dark-gray; - font-size: var(--font-size-micro); - font-weight: var(--font-weight-normal); - line-height: var(--space-normal); - margin-left: auto; - } - } - - &.unread-chat { - .unread { - display: inline-block; - } - - .conversation--message { - font-weight: var(--font-weight-bold); - } - - .conversation--user { - font-weight: var(--font-weight-bold); - } - } - - &.compact { - padding-left: 0; - - .conversation--details { - border-radius: var(--border-radius-small); - margin-left: 0; - padding-left: var(--space-two); - padding-right: var(--space-small); - } } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index f95ee8a65..f6fae6737 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -1,352 +1,229 @@ // scss-lint:disable MergeableSelector -@mixin bubble-with-types { - padding: $space-small $space-normal; - margin: 0; - background: $color-woot; - border-radius: $space-one; - color: var(--white); - font-size: $font-size-small; - font-weight: $font-weight-normal; - position: relative; - - .message-text__wrap { - position: relative; - - .link { - color: var(--white); - text-decoration: underline; - } +@tailwind utilities; +@layer utilities { + .custom-gradient { + background-image: linear-gradient( + -180deg, + transparent 3%, + rgb(76 81 85) 130% + ); } - .image, - .video { - cursor: pointer; - position: relative; + .bubble-with-types { + @apply py-2 text-sm font-normal bg-woot-500 dark:bg-woot-500 relative px-4 m-0 text-white dark:text-white; - .modal-container { - text-align: center; + .message-text__wrap { + @apply relative; + + .link { + @apply text-white dark:text-white underline; + } } - .modal-image { - max-height: 80vh; - max-width: 80vw; + .image, + .video { + @apply cursor-pointer relative; + + .modal-container { + @apply text-center; + } + + .modal-image { + @apply max-h-[76vh] max-w-[76vw]; + } + + .modal-video { + @apply max-h-[76vh] max-w-[76vw]; + } + + &::before { + @apply custom-gradient bottom-0 h-[20%] content-[''] left-0 absolute w-full opacity-80; + } } - - .modal-video { - max-height: 80vh; - max-width: 80vw; - } - - &::before { - background-image: linear-gradient( - -180deg, - transparent 3%, - $color-heading 130% - ); - bottom: 0; - content: ''; - height: 20%; - left: 0; - opacity: 0.8; - position: absolute; - width: 100%; - } - } -} - -.conversations-list-wrap { - @include flex; - border-right: 1px solid var(--s-50); - flex-direction: column; - - .load-more-conversations { - font-size: $font-size-small; - margin: 0; - padding: $space-normal; - width: 100%; - } - - .end-of-list-text { - padding: $space-normal; - } - - .conversations-list { - flex: 1 1; - overflow-y: auto; - - @include breakpoint(large up) { - @include scroll-on-hover; - } - } - - .chat-list__top { - @include flex; - align-items: center; - justify-content: space-between; - padding: 0 var(--space-normal); - } - - .content-box { - text-align: center; } } .conversation-panel { - @include flex; - flex: 1 1 1px; - flex-direction: column; - height: 100%; - margin: 0; - overflow-y: auto; - padding-bottom: var(--space-normal); - position: relative; + @apply flex-shrink flex-grow basis-px flex flex-col overflow-y-auto relative h-full m-0 pb-4; } .conversation-panel > li { - @include flex; - @include flex-shrink; - margin: $zero $zero $space-micro; - position: relative; - - &:first-child { - margin-top: auto; - } - - &:last-child { - margin-bottom: 0; - } + @apply flex flex-shrink-0 flex-grow-0 flex-auto max-w-full mt-0 mr-0 mb-1 ml-0 relative first:mt-auto last:mb-0; &.unread--toast { + .right { - margin-bottom: var(--space-micro); + @apply mb-1; } + .left { - margin-bottom: 0; + @apply mb-0; } span { - @include elegant-card; - @include round-corner; - background: $color-woot; - color: var(--white); - font-size: $font-size-mini; - font-weight: $font-weight-medium; - margin: $space-one auto; - padding: $space-smaller $space-two; + @apply shadow-lg rounded-full bg-woot-500 dark:bg-woot-500 text-white dark:text-white text-xs font-medium my-2.5 mx-auto px-2.5 py-1.5; } } .bubble { - @include bubble-with-types; - text-align: left; - word-wrap: break-word; + @apply bubble-with-types text-left break-words; .aplayer { - box-shadow: none; + @apply shadow-none; font-family: inherit; } } &.left { .bubble { - @include border-normal; - background: $white; - border-bottom-left-radius: $space-smaller; - border-top-left-radius: $space-smaller; - color: $color-body; - margin-right: auto; - word-break: break-word; + @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 rounded-r-lg rounded-l mr-auto break-words; &.is-image { - border-radius: var(--border-radius-large); + @apply rounded-lg; } .link { - color: $color-primary-dark; + @apply text-woot-600 dark:text-woot-600; } .file { .text-block-title { - color: $color-body; + @apply text-slate-700 dark:text-woot-300; } .icon-wrap { - color: $color-woot; + @apply text-woot-600 dark:text-woot-600; } .download { - color: $color-primary-dark; + @apply text-woot-600 dark:text-woot-600; } } } + .right { - margin-top: $space-one; + @apply mt-2.5; .bubble { - border-top-right-radius: $space-one; + @apply rounded-tr-lg; } } + .unread--toast { + .right { - margin-top: $space-one; + @apply mt-2.5; .bubble { - border-top-right-radius: $space-one; + @apply rounded-tr-lg; } } + .left { - margin-top: 0; + @apply mt-0; } } } &.right { - @include flex-align(right, null); + @apply justify-end; .wrap { - align-items: flex-end; - display: flex; - margin-right: $space-normal; - text-align: right; + @apply flex items-end mr-4 text-right; .sender--info { - padding: var(--space-small) 0 var(--space-smaller) var(--space-small); + @apply pt-2 pb-1 pr-0 pl-2; } } .bubble { - border-bottom-right-radius: $space-smaller; - border-top-right-radius: $space-smaller; - margin-left: auto; - word-break: break-word; + @apply ml-auto break-words rounded-l-lg rounded-r; &.is-private { - background: lighten($warning-color, 32%); - border: 1px solid lighten($warning-color, 15%); - color: $color-heading; - position: relative; + @apply text-black-900 dark:text-white relative border border-solid bg-yellow-100 dark:bg-yellow-700 border-yellow-200 dark:border-yellow-600/25; - &::before { - bottom: 0; - color: $medium-gray; - position: absolute; - right: $space-one; - top: $space-smaller + $space-micro; + blockquote { + @apply border-slate-400 dark:border-slate-400 text-slate-800 dark:text-slate-300; + + p { + @apply text-slate-600 dark:text-slate-300; + } } } &.is-image { - border-radius: var(--border-radius-large); + @apply rounded-lg; } } + .left { - margin-top: $space-one; + @apply mt-2.5; .bubble { - border-top-left-radius: $space-one; + @apply rounded-tl-lg; } } + .unread--toast { + .left { - margin-top: $space-one; + @apply rounded-lg; .bubble { - border-top-left-radius: $space-one; + @apply rounded-tl-lg; } } + .right { - margin-top: 0; + @apply mt-0; } } } &.center { - justify-content: center; + @apply items-center justify-center; } .wrap { - --bubble-max-width: 49.6rem; - margin: $zero $space-normal; - max-width: Min(var(--bubble-max-width), 84%); + max-width: Min(31rem, 84%); + @apply my-0 mx-4; .sender--name { - font-size: $font-size-mini; - margin-bottom: $space-smaller; + @apply text-xs mb-1; } } .sender--thumbnail { - @include round-corner(); - height: $space-slab; - margin-right: $space-one; - margin-top: $space-micro; - width: $space-slab; + @apply h-3 mr-3 mt-0.5 w-3 rounded-full; } .activity-wrap { - background: var(--s-50); - border: 1px solid var(--s-100); - border-radius: var(--border-radius-medium); - display: flex; - font-size: var(--font-size-small); - justify-content: center; - margin: var(--space-smaller) 0; - padding: var(--space-smaller) var(--space-micro) var(--space-smaller) - var(--space-one); + @apply flex justify-center text-sm my-1 mx-0 py-1 pr-0.5 pl-2.5 bg-slate-50 dark:bg-slate-600 text-slate-800 dark:text-slate-100 rounded-md border border-slate-100 dark:border-slate-600 border-solid; .is-text { - align-items: center; - display: inline-flex; - text-align: start; - - @include breakpoint(xxxlarge up) { - display: flex; - } + @apply inline-flex items-center text-start 2xl:flex; } } } .activity-wrap .message-text__wrap { .text-content p { - margin-bottom: 0; + @apply mb-0; } } .conversation-footer { - display: flex; - flex-direction: column; - position: relative; + @apply flex relative flex-col; } .typing-indicator-wrap { - align-items: center; - display: flex; - height: 0; - position: absolute; - top: -$space-large; - width: 100%; + @apply items-center flex h-0 absolute w-full -top-8; .typing-indicator { @include elegant-card; @include round-corner; - background: var(--white); - color: $color-light-gray; - font-size: $font-size-mini; - font-weight: $font-weight-bold; - margin: $space-one auto; - padding: $space-small $space-normal $space-small $space-two; + @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 { - margin-left: $space-small; - width: $space-medium; + @apply ml-2 w-6; } } } @@ -358,16 +235,15 @@ h4, h5, h6 { - color: var(--color-body); + @apply text-slate-800 dark:text-slate-100; } a { - color: var(--color-woot); - text-decoration: underline; + @apply text-woot-500 dark:text-woot-500 underline; } p:last-child { - margin-bottom: 0; + @apply mb-0; } } @@ -378,15 +254,14 @@ h4, h5, h6 { - color: var(--white); + @apply text-white dark:text-white; } a { - color: var(--white); - text-decoration: underline; + @apply text-white dark:text-white underline; } p:last-child { - margin-bottom: 0; + @apply mb-0; } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_forms.scss b/app/javascript/dashboard/assets/scss/widgets/_forms.scss index 17c0dc859..4688b648c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_forms.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_forms.scss @@ -1,5 +1,29 @@ // 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'], @@ -19,17 +43,11 @@ textarea, select, .multiselect > .multiselect__tags { - @include thin-border(var(--r-400)); + @apply border border-solid border-red-400 dark:border-red-400; } .message { - color: var(--r-400); - display: block; - font-size: var(--font-size-small); - font-weight: $font-weight-normal; - margin-bottom: $space-one; - margin-top: -$space-normal; - width: 100%; + @apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full; } } @@ -42,22 +60,19 @@ input { } .input-wrap { - color: $color-heading; - font-size: $font-size-small; - font-weight: $font-weight-medium; + @apply text-slate-800 dark:text-slate-100 text-sm font-medium; } .help-text { - font-weight: $font-weight-normal; + @apply font-normal text-slate-600 dark:text-slate-400; } .input-group.small { input { - font-size: var(--font-size-small); - height: var(--space-large); + @apply text-sm h-8; } .error { - border-color: var(--r-400); + @apply border-red-400 dark:border-red-400; } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index cc22dc778..f20ba72b1 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -1,117 +1,79 @@ -@import '~dashboard/assets/scss/variables'; -@import '~dashboard/assets/scss/mixins'; - .modal-mask { - @include flex; - @include flex-align(center, middle); - background-color: $masked-bg; - height: 100%; - left: 0; - position: fixed; - top: 0; - width: 100%; - z-index: 9990; + // @include flex; + // @include flex-align(center, middle); + @apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full; } .modal--close { - position: absolute; - right: $space-small; - top: $space-small; - - &:hover { - background: $color-background; - } + @apply absolute right-2 rtl:right-[unset] rtl:left-2 top-2; } .page-top-bar { - padding: $space-large $space-large $zero; + @apply px-8 pt-9 pb-0; img { - max-height: 6rem; + @apply max-h-[3.75rem]; } } .modal-container { - @include normal-shadow; - - background-color: $color-white; - border-radius: $space-smaller; - max-height: 100%; - overflow: auto; - position: relative; - width: 60rem; + @apply shadow-md rounded-sm max-h-full overflow-auto relative w-[37.5rem]; &.medium { - max-width: 80%; - width: 90rem; + @apply max-w-[80%] w-[56.25rem]; } .content-box { - height: auto; - padding: 0; + @apply h-auto p-0; } h2 { - color: $color-heading; - font-size: $font-size-medium; - font-weight: $font-weight-bold; + @apply text-slate-800 dark:text-slate-100 text-lg font-semibold; } p { - font-size: $font-size-small; - margin: 0; - padding: 0; + @apply text-sm m-0 p-0 text-slate-600 mt-2 text-sm dark:text-slate-300; } .content { - padding: $space-large; + @apply p-8; } form, .modal-content { - align-self: center; - padding: $space-large; + @apply pt-4 pb-8 px-8 self-center; a { - padding: $space-normal; + @apply p-4; } } .modal-footer { - @include flex; - @include flex-align($x: flex-end, $y: middle); - padding: $space-small $zero; - - .button { - margin-left: var(--space-small); - } - - &:first-child { - .button { - margin-left: 0; - } - } + // @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 { - justify-content: end; + @apply justify-end; } } .delete-item { - padding: $space-large; + @apply p-8; button { - margin: 0; + @apply m-0; } } } .modal-enter, .modal-leave { - opacity: 0; + @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 index d33f558e3..d15150f36 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss @@ -29,7 +29,7 @@ // Override min-height : 50px in foundation // max-height: $space-mega * 2.4; - min-height: 4.8rem; + min-height: 3rem; padding: var(--space-normal) 0 0; resize: none; } @@ -40,20 +40,20 @@ margin: 0; max-height: $space-mega * 2.4; // Override min-height : 50px in foundation - min-height: 4.8rem; + min-height: 3rem; padding: var(--space-normal) 0 0; resize: none; } } &.is-private { - background: var(--y-50); + @apply bg-yellow-100 dark:bg-yellow-800; .reply-box__top { - background: var(--y-50); + @apply bg-yellow-100 dark:bg-yellow-800; > input { - background: var(--y-50); + @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 index e9ce5d0bd..d07f1dd3d 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -58,31 +58,3 @@ text-transform: capitalize; } } - -.report-bar { - @include background-white; - @include border-light; - margin: var(--space-minus-micro) 0; - padding: var(--space-small) var(--space-medium); - - .chart-container { - @include flex; - @include flex-align(center, middle); - flex-direction: column; - - div { - width: 100%; - } - - .empty-state { - color: $color-gray; - font-size: var(--font-size-default); - margin: var(--space-jumbo); - } - - .business-hours { - margin: var(--space-normal); - text-align: center; - } - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_reports.scss b/app/javascript/dashboard/assets/scss/widgets/_reports.scss index c848bc0c8..dcd65366a 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reports.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reports.scss @@ -22,18 +22,6 @@ margin: 0 var(--space-small); } -.business-hours { - align-items: center; - display: flex; - justify-content: flex-start; - margin-left: auto; - padding-right: var(--space-normal); -} - -.business-hours-text { - font-size: var(--font-size-small); - margin: 0 var(--space-small); -} .switch { margin-bottom: var(--space-zero); diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss index e1cdaa491..3de072a77 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss @@ -47,7 +47,7 @@ } .dropdown-pane { - bottom: 6rem; + bottom: 3.75rem; display: block; visibility: visible; width: fit-content; diff --git a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss b/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss index 85d8f747d..ee556682f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss @@ -1,7 +1,7 @@ .ui-snackbar-container { left: 0; margin: 0 auto; - max-width: 40rem; + max-width: 25rem; overflow: hidden; position: absolute; right: 0; @@ -16,9 +16,9 @@ border-radius: $space-smaller; display: inline-flex; margin-bottom: $space-small; - max-width: 40rem; - min-height: 3rem; - min-width: 24rem; + max-width: 25rem; + min-height: 1.875rem; + min-width: 15rem; padding: $space-slab $space-medium; text-align: left; } @@ -31,7 +31,7 @@ .ui-snackbar-action { margin-left: auto; - padding-left: 3rem; + padding-left: 1.875rem; button { background: none; diff --git a/app/javascript/dashboard/assets/scss/widgets/_states.scss b/app/javascript/dashboard/assets/scss/widgets/_states.scss index 0ca0b5c76..7eb840221 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_states.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_states.scss @@ -1,42 +1 @@ -.loading-state { - padding: $space-jumbo $space-smaller; - - .message { - color: $color-gray; - display: block; - text-align: center; - width: 100%; - } - - .spinner { - float: none; - top: -$space-smaller; - } -} - -// EMPTY STATES -.empty-state { - padding: $space-jumbo $space-smaller; - - .title, - .message { - display: block; - text-align: center; - width: 100%; - } - - .title { - font-size: $font-size-giga; - font-weight: $font-weight-feather; - } - - .message { - color: $color-gray; - margin: $space-normal auto; - width: 90%; - } - - .button { - margin-top: $space-medium; - } -} +// To be removed diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index ae55a1a41..35a9ee9ed 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -1,100 +1,69 @@ .tabs--container { - display: flex; + @apply flex; } .tabs--container--with-border { - @include border-normal-bottom; + @apply border-b border-slate-50 dark:border-slate-800/50; } .tabs { - border-left-width: 0; - border-right-width: 0; - border-top-width: 0; - display: flex; - min-width: var(--space-mega); - padding: 0 var(--space-normal); + @apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4; } .tabs--with-scroll { + @apply overflow-hidden py-0 px-1; max-width: calc(100% - 64px); - overflow: hidden; - padding: 0 var(--space-smaller); } .tabs--scroll-button { - align-items: center; - border-radius: 0; - cursor: pointer; - display: flex; - height: auto; - justify-content: center; - min-width: var(--space-large); + @apply items-center rounded-none cursor-pointer flex h-auto justify-center min-w-[2rem]; } // Tab chat type .tab--chat-type { - @include flex; + @apply flex; .tabs-title { a { - font-size: var(--font-size-default); - font-weight: var(--font-weight-medium); - padding-bottom: var(--space-slab); - padding-top: var(--space-slab); + @apply text-base font-medium py-3; } } } .tabs-title { - flex-shrink: 0; - margin: 0 var(--space-small); + @apply flex-shrink-0 my-0 mx-2 ; .badge { - background: var(--color-background); - border-radius: var(--space-small); - color: var(--color-gray); - font-size: var(--font-size-micro); - font-weight: var(--font-weight-black); - margin: 0 var(--space-smaller); - padding: var(--space-smaller); + @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; } &:first-child { - margin-left: 0; + @apply ml-0; } &:last-child { - margin-right: 0; + @apply mr-0; } &:hover, &:focus { a { - color: darken($medium-gray, 20%); + @apply text-slate-800 dark:text-slate-100; } } a { - align-items: center; - border-bottom: 2px solid transparent; - color: $medium-gray; - display: flex; - flex-direction: row; - font-size: var(--font-size-small); - position: relative; - top: 1px; + @apply flex items-center flex-row border-b border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative; transition: border-color 0.15s $swift-ease-out-function; } &.is-active { a { - border-bottom-color: var(--color-woot); - color: var(--color-woot); + @apply border-b border-woot-500 text-woot-500 dark:text-woot-500; } .badge { - background: $color-extra-light-blue; - color: var(--color-woot); + @apply bg-woot-50 dark:bg-woot-500 text-woot-500 dark:text-woot-50 dark:bg-opacity-40; } } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_widget_builder.scss b/app/javascript/dashboard/assets/scss/widgets/_widget_builder.scss new file mode 100644 index 000000000..e69de29bb diff --git a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss index 5bbe1248d..8d6581f82 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss @@ -1,22 +1,19 @@ table { - border-spacing: 0; - font-size: var(--font-size-small); + @apply border-spacing-0 text-sm; thead { th { - font-weight: var(--font-weight-bold); - text-align: left; - text-transform: uppercase; + @apply font-semibold tracking-[1px] text-left uppercase text-slate-900 dark:text-slate-200; } } tbody { tr { - border-bottom: 1px solid var(--color-border-light); + @apply border-b border-slate-50 dark:border-slate-800/30; } td { - padding: var(--space-small); + @apply p-2.5 text-slate-700 dark:text-slate-100; } } } @@ -24,37 +21,68 @@ table { .woot-table { tr { .show-if-hover { - opacity: 0; transition: opacity 0.2s $swift-ease-out-function; + @apply opacity-0; } &:hover { .show-if-hover { - opacity: 1; + @apply opacity-100; } } } .agent-name { - display: block; - font-weight: var(--font-weight-medium); - text-transform: capitalize; + @apply block font-medium capitalize; } .woot-thumbnail { - border-radius: 50%; - height: 5rem; - width: 5rem; + @apply rounded-full h-[3.125rem] w-[3.125rem]; } .button-wrapper { - @include flex-align(left, null); - @include flex; - flex-direction: row; - min-width: 20rem; + @apply flex justify-start flex-row min-w-[12.5rem] gap-1; } .button { margin: 0; } } + +.ve-table { + .ve-table-container.ve-table-border-around { + @apply border-slate-200 dark:border-slate-700; + } + + .ve-table-content { + .ve-table-header .ve-table-header-tr .ve-table-header-th { + @apply bg-slate-50 dark:bg-slate-800 text-slate-800 dark:text-slate-100 border-slate-100 dark:border-slate-700/50; + } + + .ve-table-body .ve-table-body-tr .ve-table-body-td { + @apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-75 dark:border-slate-800; + } + + .ve-table-body.ve-table-row-hover .ve-table-body-tr:hover td { + @apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100; + } + } +} + +.table-pagination { + .ve-pagination-total { + @apply text-slate-600 dark:text-slate-200; + } + + .ve-pagination-goto { + @apply text-slate-600 dark:text-slate-200; + } + + .ve-pagination-li { + @apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200 border-slate-75 dark:border-slate-700; + } + + .ve-pagination-goto-input { + @apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200; + } +} diff --git a/app/javascript/dashboard/components/Accordion/AccordionItem.vue b/app/javascript/dashboard/components/Accordion/AccordionItem.vue index b24b6f1be..0f0983c05 100644 --- a/app/javascript/dashboard/components/Accordion/AccordionItem.vue +++ b/app/javascript/dashboard/components/Accordion/AccordionItem.vue @@ -1,15 +1,20 @@