diff --git a/.circleci/config.yml b/.circleci/config.yml index 15c40dc77..459faf1c8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,84 +1,87 @@ -# Ruby CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-ruby/ for more details -# -version: 2 +version: 2.1 +orbs: + node: circleci/node@6.1.0 + defaults: &defaults working_directory: ~/build - docker: - # specify the version you desire here - - image: cimg/ruby:3.3.3-browsers - - # 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:15.3 - - image: cimg/redis:6.2.6 - environment: - - RAILS_LOG_TO_STDOUT: false - - COVERAGE: true - - LOG_LEVEL: warn - parallelism: 4 + machine: + image: ubuntu-2204:2024.05.1 resource_class: large + environment: + RAILS_LOG_TO_STDOUT: false + COVERAGE: true + LOG_LEVEL: warn + parallelism: 4 jobs: build: <<: *defaults steps: - checkout + - node/install: + node-version: '20.12' + - node/install-pnpm + - node/install-packages: + pkg-manager: pnpm + override-ci-command: pnpm i + - run: node --version + - run: pnpm --version - run: - name: Configure Bundler + name: Install System Dependencies command: | - echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV - source $BASH_ENV + sudo apt-get update + DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ + libpq-dev \ + redis-server \ + postgresql \ + build-essential \ + git \ + curl \ + libssl-dev \ + zlib1g-dev \ + libreadline-dev \ + libyaml-dev \ + openjdk-11-jdk \ + jq \ + software-properties-common \ + ca-certificates \ + imagemagick \ + libxml2-dev \ + libxslt1-dev \ + file \ + g++ \ + gcc \ + autoconf \ + gnupg2 \ + patch \ + ruby-dev \ + liblzma-dev \ + libgmp-dev \ + libncurses5-dev \ + libffi-dev \ + libgdbm6 \ + libgdbm-dev \ + libvips + + - run: + name: Install RVM and Ruby 3.3.3 + command: | + sudo apt-get install -y gpg + gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB + \curl -sSL https://get.rvm.io | bash -s stable + echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV + source ~/.rvm/scripts/rvm + rvm install "3.3.3" + rvm use 3.3.3 --default gem install bundler - run: - name: Which bundler? - command: bundle -v - - - run: - name: Swap node versions + name: Install Application Dependencies command: | - set +e - wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" - nvm install v20 - echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV - echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV - - # Run bundler - # Load installed gems from cache if possible, bundle install then save cache - # Multiple caches are used to increase the chance of a cache hit - - - restore_cache: - keys: - - chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }} - - - run: bundle install --frozen --path ~/.bundle - - save_cache: - paths: - - ~/.bundle - key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }} - - # Only necessary if app uses webpacker or yarn in some other way - - restore_cache: - keys: - - chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} - - chatwoot-yarn- - - - run: - name: yarn - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - # Store yarn / webpacker cache - - save_cache: - key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn + source ~/.rvm/scripts/rvm + bundle install + # pnpm install - run: name: Download cc-test-reporter @@ -86,12 +89,8 @@ jobs: mkdir -p ~/tmp curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter chmod +x ~/tmp/cc-test-reporter - - persist_to_workspace: - root: ~/tmp - paths: - - cc-test-reporter - # verify swagger specification + # Swagger verification - run: name: Verify swagger API specification command: | @@ -104,45 +103,62 @@ jobs: 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: bundle exec rake db:create - - run: bundle exec rake db:schema:load + # we remove the FRONTED_URL from the .env before running the tests + - run: + name: Database Setup and Configure Environment Variables + command: | + pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '') + sed -i "s/REPLACE_WITH_PASSWORD/${pg_pass}/g" ${PWD}/.circleci/setup_chatwoot.sql + chmod 644 ${PWD}/.circleci/setup_chatwoot.sql + mv ${PWD}/.circleci/setup_chatwoot.sql /tmp/ + sudo -i -u postgres psql -f /tmp/setup_chatwoot.sql + cp .env.example .env + sed -i '/^FRONTEND_URL/d' .env + sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env + sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env + sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env + sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env + echo -en "\nINSTALLATION_ENV=circleci" >> ".env" + # Database setup + - run: + name: Run DB migrations + command: bundle exec rails db:chatwoot_prepare + + # Bundle audit - run: name: Bundle audit command: bundle exec bundle audit update && bundle exec bundle audit check -v + # Rubocop linting - run: name: Rubocop command: bundle exec rubocop - # - run: - # name: Brakeman - # command: bundle exec brakeman - + # ESLint linting - run: name: eslint - command: yarn run eslint + command: pnpm run eslint - # Run frontend tests - run: name: Run frontend tests command: | - mkdir -p ~/tmp/test-results/frontend_specs + mkdir -p ~/build/coverage/frontend ~/tmp/cc-test-reporter before-build - yarn test:coverage - - run: - name: Code Climate Test Coverage - command: | - ~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json" + pnpm run test:coverage - # Run rails tests + - run: + name: Code Climate Test Coverage (Frontend) + command: | + ~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json" + + # Run backend tests - run: name: Run backend tests command: | mkdir -p ~/tmp/test-results/rspec mkdir -p ~/tmp/test-artifacts - mkdir -p coverage + mkdir -p ~/build/coverage/backend ~/tmp/cc-test-reporter before-build TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) bundle exec rspec --format progress \ @@ -150,54 +166,18 @@ jobs: --out ~/tmp/test-results/rspec.xml \ -- ${TESTFILES} no_output_timeout: 30m + - run: - name: Code Climate Test Coverage + name: Code Climate Test Coverage (Backend) command: | - ~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json" + ~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json" + + - run: + name: List coverage directory contents + command: | + ls -R ~/build/coverage - persist_to_workspace: - root: coverage + root: ~/build paths: - - codeclimate.*.json - # collect reports - - store_test_results: - path: ~/tmp/test-results - - store_artifacts: - path: ~/tmp/test-artifacts - - store_artifacts: - path: log - - upload-coverage: - working_directory: ~/build - docker: - # specify the version you desire here - - image: circleci/ruby:3.0.2-node-browsers - environment: - - CC_TEST_REPORTER_ID: caf26a895e937974a90860cfadfded20891cfd1373a5aaafb3f67406ab9d433f - steps: - - attach_workspace: - at: ~/build - - run: - name: Download cc-test-reporter - command: | - mkdir -p ~/tmp - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter - chmod +x ~/tmp/cc-test-reporter - - persist_to_workspace: - root: ~/tmp - paths: - - cc-test-reporter - - run: - name: Upload coverage results to Code Climate - command: | - ~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input - - -workflows: - version: 2 - - commit: - jobs: - - build - - upload-coverage: - requires: - - build + - coverage diff --git a/.circleci/setup_chatwoot.sql b/.circleci/setup_chatwoot.sql new file mode 100644 index 000000000..4e5430f1d --- /dev/null +++ b/.circleci/setup_chatwoot.sql @@ -0,0 +1,11 @@ +CREATE USER chatwoot CREATEDB; +ALTER USER chatwoot PASSWORD 'REPLACE_WITH_PASSWORD'; +ALTER ROLE chatwoot SUPERUSER; + +UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1'; +DROP DATABASE template1; +CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE'; +UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1'; + +\c template1; +VACUUM FREEZE; diff --git a/.codeclimate.yml b/.codeclimate.yml index d8b8d985b..213f7d172 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -27,7 +27,8 @@ checks: threshold: 50 exclude_patterns: - 'spec/' - - '**/specs/' + - '**/specs/**/**' + - '**/spec/**/**' - 'db/*' - 'bin/**/*' - 'db/**/*' @@ -56,3 +57,5 @@ exclude_patterns: - 'app/javascript/shared/constants/locales.js' - 'app/javascript/dashboard/helper/specs/macrosFixtures.js' - 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js' + - '**/fixtures/**' + - '**/*/fixtures.js' diff --git a/.eslintrc.js b/.eslintrc.js index f3d050ab3..a2932b2a9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,16 +2,36 @@ module.exports = { extends: [ 'airbnb-base/legacy', 'prettier', - 'plugin:vue/recommended', - 'plugin:storybook/recommended', - 'plugin:cypress/recommended', + 'plugin:vue/vue3-recommended', + 'plugin:vitest-globals/recommended', ], + overrides: [ + { + files: ['**/*.spec.{j,t}s?(x)'], + env: { + 'vitest-globals/env': true, + }, + }, + { + files: ['**/*.story.vue'], + rules: { + 'vue/no-undef-components': [ + 'error', + { + ignorePatterns: ['Variant', 'Story'], + }, + ], + // Story files can have static strings, it doesn't need to handle i18n always. + 'vue/no-bare-strings-in-template': 'off', + 'no-console': 'off', + }, + }, + ], + plugins: ['html', 'prettier'], parserOptions: { - parser: '@babel/eslint-parser', - ecmaVersion: 2020, + ecmaVersion: 'latest', sourceType: 'module', }, - plugins: ['html', 'prettier', 'babel'], rules: { 'prettier/prettier': ['error'], camelcase: 'off', @@ -210,13 +230,6 @@ module.exports = { 'import/extensions': ['off'], 'no-console': 'error', }, - settings: { - 'import/resolver': { - webpack: { - config: 'config/webpack/resolve.js', - }, - }, - }, env: { browser: true, node: true, diff --git a/.github/workflows/frontend-fe.yml b/.github/workflows/frontend-fe.yml new file mode 100644 index 000000000..15bb6f5e9 --- /dev/null +++ b/.github/workflows/frontend-fe.yml @@ -0,0 +1,43 @@ +name: Frontend Lint & Test + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + test: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + 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 + + - uses: pnpm/action-setup@v4 + with: + version: 9.3.0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install pnpm dependencies + run: pnpm install --frozen-lockfile + + - name: Run eslint + run: pnpm run eslint + + - name: Run frontend tests with coverage + run: | + mkdir -p coverage + pnpm run test:coverage diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index c42909856..5420e829b 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -25,13 +25,5 @@ jobs: with: issue-inactive-days: '30' issue-lock-reason: 'resolved' - issue-comment: > - This issue has been automatically locked since there - has not been any recent activity after it was closed. - Please open a new issue for related bugs. pr-inactive-days: '30' pr-lock-reason: 'resolved' - pr-comment: > - This pull request has been automatically locked since there - has not been any recent activity after it was closed. - Please open a new issue for related bugs. diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index b0b2372ae..6a018b314 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -21,7 +21,7 @@ jobs: image: postgres:15.3 env: POSTGRES_USER: postgres - POSTGRES_PASSWORD: "" + POSTGRES_PASSWORD: '' POSTGRES_DB: postgres POSTGRES_HOST_AUTH_METHOD: trust ports: @@ -41,47 +41,49 @@ jobs: options: --entrypoint redis-server steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + 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: ruby/setup-ruby@v1 + with: + bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' - - name: yarn - run: yarn install + - name: Install pnpm dependencies + run: pnpm i - - name: Strip enterprise code - run: | - rm -rf enterprise - rm -rf spec/enterprise + - name: Strip enterprise code + run: | + rm -rf enterprise + rm -rf spec/enterprise - - name: Create database - run: bundle exec rake db:create + - name: Create database + run: bundle exec rake db:create - - name: Seed database - run: bundle exec rake db:schema:load + - name: Seed database + run: bundle exec rake db:schema:load - - name: yarn check-files - run: yarn install --check-files + - name: Run frontend tests + run: pnpm run test:coverage - # Run rails tests - - name: Run backend tests - run: | - bundle exec rspec --profile=10 --format documentation - env: - NODE_OPTIONS: --openssl-legacy-provider + # Run rails tests + - name: Run backend tests + run: | + bundle exec rspec --profile=10 --format documentation + env: + NODE_OPTIONS: --openssl-legacy-provider - - name: Upload rails log folder - uses: actions/upload-artifact@v4 - if: always() - with: - name: rails-log-folder - path: log + - name: Upload rails log folder + uses: actions/upload-artifact@v4 + if: always() + with: + name: rails-log-folder + path: log diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml index c594ff404..c788a0400 100644 --- a/.github/workflows/run_response_bot_spec.yml +++ b/.github/workflows/run_response_bot_spec.yml @@ -49,13 +49,17 @@ jobs: with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - uses: pnpm/action-setup@v2 + with: + version: 9.3.0 + - uses: actions/setup-node@v4 with: node-version: 20 - cache: yarn + cache: pnpm - - name: yarn - run: yarn install + - name: pnpm + run: pnpm install - name: Create database run: bundle exec rake db:create diff --git a/.github/workflows/size-limit.yml b/.github/workflows/size-limit.yml index 0526aeedb..0758ca7d0 100644 --- a/.github/workflows/size-limit.yml +++ b/.github/workflows/size-limit.yml @@ -19,23 +19,33 @@ jobs: with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - uses: pnpm/action-setup@v2 + with: + version: 9.3.0 + - uses: actions/setup-node@v4 with: node-version: 20 - cache: 'yarn' + cache: 'pnpm' - - name: yarn - run: yarn install + - name: pnpm + run: pnpm install - name: Strip enterprise code run: | rm -rf enterprise rm -rf spec/enterprise + - name: setup env + run: | + cp .env.example .env + - name: Run asset compile run: bundle exec rake assets:precompile env: - NODE_OPTIONS: --openssl-legacy-provider + RAILS_ENV: production - name: Size Check - run: yarn run size + run: pnpm run size + + diff --git a/.gitignore b/.gitignore index 028a97f09..5eb883db0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,16 @@ master.key public/uploads public/packs* +public/assets/administrate* +public/assets/action*.js +public/assets/activestorage*.js +public/assets/trix* +public/assets/belongs_to*.js +public/assets/manifest*.js +public/assets/manifest*.js +public/assets/*.js.gz +public/assets/secretField* +public/assets/.sprockets-manifest-*.json # VIM files *.swp @@ -75,4 +85,8 @@ yalc.lock yarn-debug.log* .yarn-integrity -/storybook-static \ No newline at end of file +# Vite Ruby +/public/vite* +# Vite uses dotenv and suggests to ignore local-only env files. See +# https://vitejs.dev/guide/env-and-mode.html#env-files +*.local diff --git a/.husky/pre-commit b/.husky/pre-commit index adda426ad..2525dde4e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,11 +1,11 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" +# #!/bin/sh +# . "$(dirname "$0")/_/husky.sh" -# lint js and vue files -npx --no-install lint-staged +# # lint js and vue files +# npx --no-install lint-staged -# lint only staged ruby files -git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a +# # lint only staged ruby files +# git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a -# stage rubocop changes to files -git diff --name-only --cached | xargs git add +# # stage rubocop changes to files +# git diff --name-only --cached | xargs git add diff --git a/.storybook/main.js b/.storybook/main.js deleted file mode 100644 index cb32634c2..000000000 --- a/.storybook/main.js +++ /dev/null @@ -1,56 +0,0 @@ -const path = require('path'); -const resolve = require('../config/webpack/resolve'); - -// Chatwoot's webpack.config.js -process.env.NODE_ENV = 'development'; -const custom = require('../config/webpack/environment'); - -module.exports = { - stories: [ - '../stories/**/*.stories.mdx', - '../app/javascript/**/*.stories.@(js|jsx|ts|tsx)', - ], - addons: [ - { - name: '@storybook/addon-docs', - options: { - vueDocgenOptions: { - alias: { - '@': path.resolve(__dirname, '../'), - }, - }, - }, - }, - '@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 = { - ...config, - resolve: { - ...config.resolve, - modules: custom.resolvedModules.map(i => i.value), - }, - }; - - newConfig.module.rules.push({ - test: /\.scss$/, - use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'], - include: path.resolve(__dirname, '../app/javascript'), - }); - - return newConfig; - }, -}; diff --git a/.storybook/preview.js b/.storybook/preview.js deleted file mode 100644 index 7c046d51a..000000000 --- a/.storybook/preview.js +++ /dev/null @@ -1,46 +0,0 @@ -import { addDecorator } from '@storybook/vue'; -import Vue from 'vue'; -import Vuex from 'vuex'; -import VueI18n from 'vue-i18n'; -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'; - -Vue.use(VueI18n); -Vue.use(WootUiKit); -Vue.use(Vuex); -Vue.use(VueDOMPurifyHTML, domPurifyConfig); - -Vue.component('multiselect', Multiselect); -Vue.component('fluent-icon', FluentIcon); - -const store = new Vuex.Store({}); -const i18nConfig = new VueI18n({ - locale: 'en', - messages: i18n, -}); - -addDecorator(() => ({ - template: '', - i18n: i18nConfig, - store, - beforeCreate: function () { - this.$root._i18n = this.$i18n; - }, -})); - -export const parameters = { - actions: { argTypesRegex: '^on[A-Z].*' }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -}; diff --git a/Gemfile b/Gemfile index 7d34ffda0..439b0f63e 100644 --- a/Gemfile +++ b/Gemfile @@ -49,7 +49,7 @@ gem 'aws-sdk-s3', require: false # original gem isn't maintained actively # we wanted updated version of faraday which is a dependency for slack-ruby-client gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby', branch: 'chatwoot', require: false -gem 'google-cloud-storage', require: false +gem 'google-cloud-storage', '>= 1.48.0', require: false gem 'image_processing' ##-- gems for database --# @@ -64,7 +64,7 @@ gem 'activerecord-import' gem 'dotenv-rails', '>= 3.0.0' gem 'foreman' gem 'puma' -gem 'webpacker' +gem 'vite_rails' # metrics on heroku gem 'barnes' @@ -96,12 +96,12 @@ gem 'koala' # slack client gem 'slack-ruby-client', '~> 2.2.0' # for dialogflow integrations -gem 'google-cloud-dialogflow-v2' +gem 'google-cloud-dialogflow-v2', '>= 0.24.0' gem 'grpc' # Translate integrations # 'google-cloud-translate' gem depends on faraday 2.0 version # this dependency breaks the slack-ruby-client gem -gem 'google-cloud-translate-v3' +gem 'google-cloud-translate-v3', '>= 0.7.0' ##-- apm and error monitoring ---# # loaded only when environment variables are set. @@ -111,12 +111,12 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'scout_apm', require: false -gem 'sentry-rails', '>= 5.18.2', require: false +gem 'sentry-rails', '>= 5.19.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.18.2', require: false +gem 'sentry-sidekiq', '>= 5.19.0', require: false ##-- background job processing --## -gem 'sidekiq', '>= 7.3.0' +gem 'sidekiq', '>= 7.3.1' # We want cron jobs gem 'sidekiq-cron', '>= 1.12.0' @@ -165,7 +165,7 @@ gem 'audited', '~> 5.4', '>= 5.4.1' # need for google auth gem 'omniauth', '>= 2.1.2' -gem 'omniauth-google-oauth2', '>= 1.1.2' +gem 'omniauth-google-oauth2', '>= 1.1.3' gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2' ## Gems for reponse bot @@ -181,6 +181,9 @@ gem 'reverse_markdown' group :production do # we dont want request timing out in development while using byebug gem 'rack-timeout' + # for heroku autoscaling + gem 'judoscale-rails', require: false + gem 'judoscale-sidekiq', require: false end group :development do @@ -200,12 +203,10 @@ group :development do gem 'rack-mini-profiler', '>= 3.2.0', require: false gem 'stackprof' # Should install the associated chrome extension to view query logs - gem 'meta_request', '>= 0.8.0' + gem 'meta_request', '>= 0.8.3' end group :test do - # Cypress in rails. - gem 'cypress-on-rails' # fast cleaning of database gem 'database_cleaner' # mock http calls @@ -228,7 +229,7 @@ group :development, :test do gem 'mock_redis' gem 'pry-rails' gem 'rspec_junit_formatter' - gem 'rspec-rails', '>= 6.1.3' + gem 'rspec-rails', '>= 6.1.5' gem 'rubocop', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false diff --git a/Gemfile.lock b/Gemfile.lock index cf522bf39..298d8e041 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,70 +33,70 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailbox (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.4) - actionpack (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailer (7.0.8.5) + actionpack (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.4) - actionview (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionpack (7.0.8.5) + actionview (= 7.0.8.5) + activesupport (= 7.0.8.5) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.4) - actionpack (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actiontext (7.0.8.5) + actionpack (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.0.8.5) + activesupport (= 7.0.8.5) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.3.6) - activemodel (7.0.8.4) - activesupport (= 7.0.8.4) - activerecord (7.0.8.4) - activemodel (= 7.0.8.4) - activesupport (= 7.0.8.4) + activemodel (7.0.8.5) + activesupport (= 7.0.8.5) + activerecord (7.0.8.5) + activemodel (= 7.0.8.5) + activesupport (= 7.0.8.5) activerecord-import (1.4.1) activerecord (>= 4.2) - activestorage (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activesupport (= 7.0.8.4) + activestorage (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activesupport (= 7.0.8.5) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.0.8.5) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -169,7 +169,7 @@ GEM climate_control (1.2.0) coderay (1.1.3) commonmarker (0.23.10) - concurrent-ruby (1.3.3) + concurrent-ruby (1.3.4) connection_pool (2.4.1) crack (1.0.0) bigdecimal @@ -178,8 +178,6 @@ GEM csv (3.3.0) csv-safe (3.3.1) csv (~> 3.0) - cypress-on-rails (1.16.0) - rack database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (2.1.0) @@ -211,7 +209,7 @@ GEM devise (> 3.5.2, < 5) rails (>= 4.2.0, < 7.2) diff-lcs (1.5.1) - digest-crc (0.6.4) + digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) docile (1.4.0) domain_name (0.5.20190701) @@ -222,6 +220,7 @@ GEM railties (>= 6.1) down (5.4.0) addressable (~> 2.8) + dry-cli (1.1.0) ecma-re-validator (0.4.0) regexp_parser (~> 2.2) elastic-apm (4.6.2) @@ -230,7 +229,7 @@ GEM ruby2_keywords email_reply_trimmer (0.1.13) erubi (1.13.0) - et-orbi (1.2.7) + et-orbi (1.2.11) tzinfo execjs (2.8.1) facebook-messenger (2.0.1) @@ -257,7 +256,7 @@ GEM faraday-net_http_persistent (2.1.0) faraday (~> 2.5) net-http-persistent (~> 4.0) - faraday-retry (2.1.0) + faraday-retry (2.2.1) faraday (~> 2.0) fcm (1.0.8) faraday (>= 1.0.0, < 3.0) @@ -268,10 +267,10 @@ GEM rake flag_shih_tzu (0.3.23) foreman (0.87.2) - fugit (1.9.0) - et-orbi (~> 1, >= 1.2.7) + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) - gapic-common (0.18.0) + gapic-common (0.20.0) faraday (>= 1.9, < 3.a) faraday-retry (>= 1.0, < 3.a) google-protobuf (~> 3.14) @@ -285,57 +284,57 @@ GEM activesupport (>= 6.1) gmail_xoauth (0.4.3) oauth (>= 0.3.6) - google-apis-core (0.11.0) + google-apis-core (0.15.1) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-iamcredentials_v1 (0.22.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.47.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-dialogflow-v2 (0.23.0) - gapic-common (>= 0.18.0, < 2.a) + google-cloud-dialogflow-v2 (0.31.0) + gapic-common (>= 0.20.0, < 2.a) google-cloud-errors (~> 1.0) google-cloud-location (>= 0.4, < 2.a) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) + google-cloud-env (2.2.1) + faraday (>= 1.0, < 3.a) google-cloud-errors (1.3.1) - google-cloud-location (0.4.0) - gapic-common (>= 0.17.1, < 2.a) + google-cloud-location (0.6.0) + gapic-common (>= 0.20.0, < 2.a) google-cloud-errors (~> 1.0) - google-cloud-storage (1.44.0) + google-cloud-storage (1.52.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-core (~> 0.13) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (~> 0.38) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - google-cloud-translate-v3 (0.6.0) - gapic-common (>= 0.17.1, < 2.a) + google-cloud-translate-v3 (0.10.0) + gapic-common (>= 0.20.0, < 2.a) google-cloud-errors (~> 1.0) - google-protobuf (3.25.3) - google-protobuf (3.25.3-arm64-darwin) - google-protobuf (3.25.3-x86_64-darwin) - google-protobuf (3.25.3-x86_64-linux) - googleapis-common-protos (1.4.0) - google-protobuf (~> 3.14) - googleapis-common-protos-types (~> 1.2) - grpc (~> 1.27) + google-protobuf (3.25.5) + google-protobuf (3.25.5-arm64-darwin) + google-protobuf (3.25.5-x86_64-darwin) + google-protobuf (3.25.5-x86_64-linux) + googleapis-common-protos (1.6.0) + google-protobuf (>= 3.18, < 5.a) + googleapis-common-protos-types (~> 1.7) + grpc (~> 1.41) googleapis-common-protos-types (1.14.0) google-protobuf (~> 3.18) - googleauth (1.5.2) - faraday (>= 0.17.3, < 3.a) + googleauth (1.11.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) @@ -374,7 +373,7 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) @@ -398,6 +397,13 @@ GEM hana (~> 1.3) regexp_parser (~> 2.0) uri_template (~> 0.7) + judoscale-rails (1.8.2) + judoscale-ruby (= 1.8.2) + railties + judoscale-ruby (1.8.2) + judoscale-sidekiq (1.8.2) + judoscale-ruby (= 1.8.2) + sidekiq (>= 5.0) jwt (2.8.1) base64 kaminari (1.2.2) @@ -446,7 +452,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -456,8 +462,7 @@ GEM net-smtp marcel (1.0.4) maxminddb (0.1.22) - memoist (0.16.2) - meta_request (0.8.2) + meta_request (0.8.3) rack-contrib (>= 1.1, < 3) railties (>= 3.0.0, < 8) method_source (1.1.0) @@ -466,21 +471,22 @@ GEM mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.7) - minitest (5.24.1) + mini_portile2 (2.8.8) + minitest (5.25.1) 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) + mutex_m (0.3.0) neighbor (0.2.3) activerecord (>= 5.2) net-http (0.4.1) uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.12) + net-imap (0.4.17) date net-protocol net-pop (0.1.2) @@ -496,14 +502,14 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.3) - nokogiri (1.16.6) + nokogiri (1.16.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.6-arm64-darwin) + nokogiri (1.16.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.6-x86_64-darwin) + nokogiri (1.16.8-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.6-x86_64-linux) + nokogiri (1.16.8-x86_64-linux) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -522,7 +528,7 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-google-oauth2 (1.1.2) + omniauth-google-oauth2 (1.1.3) jwt (>= 2.0) oauth2 (~> 2.0) omniauth (~> 2.0) @@ -552,13 +558,13 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (6.0.0) - puma (6.4.2) + puma (6.4.3) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.8.0) - rack (2.2.9) + racc (1.8.1) + rack (2.2.10) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) @@ -570,35 +576,35 @@ GEM rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) - rack-proxy (0.7.6) + rack-proxy (0.7.7) rack rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rails (7.0.8.4) - actioncable (= 7.0.8.4) - actionmailbox (= 7.0.8.4) - actionmailer (= 7.0.8.4) - actionpack (= 7.0.8.4) - actiontext (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activemodel (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rails (7.0.8.5) + actioncable (= 7.0.8.5) + actionmailbox (= 7.0.8.5) + actionmailer (= 7.0.8.5) + actionpack (= 7.0.8.5) + actiontext (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activemodel (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.0.8.5) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.1) loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) method_source rake (>= 12.2) thor (~> 1.0) @@ -634,20 +640,19 @@ GEM retriable (3.1.2) reverse_markdown (2.1.1) nokogiri - rexml (3.3.4) - strscan + rexml (3.3.9) rspec-core (3.13.0) rspec-support (~> 3.13.0) - rspec-expectations (3.13.1) + rspec-expectations (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.3) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) + rspec-rails (7.0.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) @@ -709,20 +714,19 @@ GEM activerecord (>= 4) activesupport (>= 4) selectize-rails (0.12.6) - semantic_range (3.0.0) - sentry-rails (5.18.2) + sentry-rails (5.19.0) railties (>= 5.0) - sentry-ruby (~> 5.18.2) - sentry-ruby (5.18.2) + sentry-ruby (~> 5.19.0) + sentry-ruby (5.19.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.18.2) - sentry-ruby (~> 5.18.2) + sentry-sidekiq (5.19.0) + sentry-ruby (~> 5.19.0) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.3.0) + sidekiq (7.3.1) concurrent-ruby (< 2) connection_pool (>= 2.3.0) logger @@ -766,7 +770,6 @@ GEM stackprof (0.2.25) statsd-ruby (1.5.0) stripe (8.5.0) - strscan (3.1.0) telephone_number (1.4.20) test-prof (1.2.1) thor (1.3.1) @@ -796,10 +799,17 @@ GEM uniform_notifier (1.16.0) uri (0.13.0) uri_template (0.7.0) - valid_email2 (4.0.6) + valid_email2 (5.2.6) activemodel (>= 3.2) mail (~> 2.5) version_gem (1.1.4) + vite_rails (3.0.17) + railties (>= 5.1, < 8) + vite_ruby (~> 3.0, >= 3.2.2) + vite_ruby (3.8.0) + dry-cli (>= 0.7, < 2) + rack-proxy (~> 0.6, >= 0.6.1) + zeitwerk (~> 2.2) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -814,12 +824,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.4.4) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) - webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -827,7 +831,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.16) + zeitwerk (2.6.17) PLATFORMS arm64-darwin-20 @@ -862,7 +866,6 @@ DEPENDENCIES climate_control commonmarker csv-safe - cypress-on-rails database_cleaner ddtrace debug (~> 1.8) @@ -881,9 +884,9 @@ DEPENDENCIES foreman geocoder gmail_xoauth - google-cloud-dialogflow-v2 - google-cloud-storage - google-cloud-translate-v3 + google-cloud-dialogflow-v2 (>= 0.24.0) + google-cloud-storage (>= 1.48.0) + google-cloud-translate-v3 (>= 0.7.0) groupdate grpc haikunator @@ -894,6 +897,8 @@ DEPENDENCIES jbuilder json_refs json_schemer + judoscale-rails + judoscale-sidekiq jwt kaminari koala @@ -903,14 +908,14 @@ DEPENDENCIES listen lograge (~> 0.14.0) maxminddb - meta_request (>= 0.8.0) + meta_request (>= 0.8.3) mock_redis neighbor net-smtp (~> 0.3.4) newrelic-sidekiq-metrics (>= 1.6.2) newrelic_rpm omniauth (>= 2.1.2) - omniauth-google-oauth2 (>= 1.1.2) + omniauth-google-oauth2 (>= 1.1.3) omniauth-oauth2 omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2) pg @@ -930,7 +935,7 @@ DEPENDENCIES responders (>= 3.1.1) rest-client reverse_markdown - rspec-rails (>= 6.1.3) + rspec-rails (>= 6.1.5) rspec_junit_formatter rubocop rubocop-performance @@ -939,11 +944,11 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails (>= 5.18.2) + sentry-rails (>= 5.19.0) sentry-ruby - sentry-sidekiq (>= 5.18.2) + sentry-sidekiq (>= 5.19.0) shoulda-matchers - sidekiq (>= 7.3.0) + sidekiq (>= 7.3.1) sidekiq-cron (>= 1.12.0) simplecov (= 0.17.1) slack-ruby-client (~> 2.2.0) @@ -960,10 +965,10 @@ DEPENDENCIES tzinfo-data uglifier valid_email2 + vite_rails web-console (>= 4.2.1) web-push (>= 3.0.1) webmock - webpacker wisper (= 2.0.0) working_hours diff --git a/Makefile b/Makefile index 7f2680a2d..1c5ce297c 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ RAILS_ENV ?= development setup: gem install bundler bundle install - yarn install + pnpm install db_create: RAILS_ENV=$(RAILS_ENV) bundle exec rails db:create @@ -30,7 +30,7 @@ server: RAILS_ENV=$(RAILS_ENV) bundle exec rails server -b 0.0.0.0 -p 3000 burn: - bundle && yarn + bundle && pnpm install run: @if [ -f ./.overmind.sock ]; then \ diff --git a/Procfile.dev b/Procfile.dev index 73c2af7e8..651b84edd 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,4 +1,4 @@ backend: bin/rails s -p 3000 -frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server # https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695 worker: dotenv bundle exec sidekiq -C config/sidekiq.yml +vite: bin/vite dev diff --git a/Procfile.test b/Procfile.test index 1770cdbc6..f7ac060ba 100644 --- a/Procfile.test +++ b/Procfile.test @@ -1,3 +1,3 @@ backend: RAILS_ENV=test bin/rails s -p 5050 -frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server +vite: bin/vite dev worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml diff --git a/README.md b/README.md index 152660fb7..e5f73eb97 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ +## 🚨 Note: This branch is unstable. For the stable branch's source code, please use the branch [3.x](https://github.com/chatwoot/chatwoot/tree/3.x) + + Chat dashboard dark mode Chat dashboard ___ -# Chatwoot +# Chatwoot Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.

@@ -98,7 +101,7 @@ Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app. ### Other deployment options -For other supported options, checkout our [deployment page](https://chatwoot.com/deploy). +For other supported options, checkout our [deployment page](https://chatwoot.com/deploy). ## Security diff --git a/VERSION_CW b/VERSION_CW index a5c4c7633..4eba2a62e 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -3.9.0 +3.13.0 diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 834f26295..4a36342fc 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -2.8.0 +3.0.0 diff --git a/app.json b/app.json index d50040814..08e725c8e 100644 --- a/app.json +++ b/app.json @@ -48,7 +48,7 @@ "size": "basic" } }, - "stack": "heroku-20", + "stack": "heroku-24", "image": "heroku/ruby", "addons": [ { @@ -58,7 +58,7 @@ "plan": "heroku-postgresql:essential-0" } ], - "stack": "heroku-20", + "stack": "heroku-24", "buildpacks": [ { "url": "heroku/nodejs" diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 2cc9549bc..00c9417ba 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -2,5 +2,4 @@ //= link administrate/application.css //= link administrate/application.js //= link administrate-field-active_storage/application.css -//= link dashboardChart.js //= link secretField.js diff --git a/app/assets/javascripts/dashboardChart.js b/app/assets/javascripts/dashboardChart.js deleted file mode 100644 index 46f278b80..000000000 --- a/app/assets/javascripts/dashboardChart.js +++ /dev/null @@ -1,55 +0,0 @@ -// eslint-disable-next-line -function prepareData(data) { - var labels = []; - var dataSet = []; - data.forEach(item => { - labels.push(item[0]); - dataSet.push(item[1]); - }); - return { labels, dataSet }; -} - -function getChartOptions() { - var fontFamily = - 'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; - return { - responsive: true, - legend: { labels: { fontFamily } }, - scales: { - xAxes: [ - { - barPercentage: 1.26, - ticks: { fontFamily }, - gridLines: { display: false }, - }, - ], - yAxes: [ - { - ticks: { fontFamily }, - gridLines: { display: false }, - }, - ], - }, - }; -} - -// eslint-disable-next-line -function drawSuperAdminDashboard(data) { - var ctx = document.getElementById('dashboard-chart').getContext('2d'); - var chartData = prepareData(data); - // eslint-disable-next-line - new Chart(ctx, { - type: 'bar', - data: { - labels: chartData.labels, - datasets: [ - { - label: 'Conversations', - data: chartData.dataSet, - backgroundColor: '#1f93ff', - }, - ], - }, - options: getChartOptions(), - }); -} diff --git a/app/assets/stylesheets/administrate/utilities/_variables.scss b/app/assets/stylesheets/administrate/utilities/_variables.scss index 3a1129c41..9123667e2 100644 --- a/app/assets/stylesheets/administrate/utilities/_variables.scss +++ b/app/assets/stylesheets/administrate/utilities/_variables.scss @@ -86,8 +86,5 @@ $swift-ease-out-duration: .4s !default; $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default; -// Ionicons -$ionicons-font-path: '~ionicons/fonts'; - // Transitions $transition-ease-in: all 0.250s ease-in; diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index f179c6405..6f7980586 100644 --- a/app/builders/account_builder.rb +++ b/app/builders/account_builder.rb @@ -32,11 +32,13 @@ class AccountBuilder end def validate_email + raise InvalidEmail.new({ domain_blocked: domain_blocked }) if domain_blocked? + address = ValidEmail2::Address.new(@email) - if address.valid? # && !address.disposable? + if address.valid? && !address.disposable? true else - raise InvalidEmail.new(valid: address.valid?) + raise InvalidEmail.new({ valid: address.valid?, disposable: address.disposable? }) end end @@ -79,4 +81,21 @@ class AccountBuilder @user.confirm if @confirmed @user.save! end + + def domain_blocked? + domain = @email.split('@').last + + blocked_domains.each do |blocked_domain| + return true if domain.match?(blocked_domain) + end + + false + end + + def blocked_domains + domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '') + return [] if domains.blank? + + domains.split("\n").map(&:strip) + end end diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 4eff10127..01aa42620 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -24,7 +24,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController def update @agent.update!(agent_params.slice(:name).compact) - @agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact) + @agent.current_account_user.update!(agent_params.slice(*account_user_attributes).compact) end def destroy @@ -67,8 +67,16 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController @agent = agents.find(params[:id]) end + def account_user_attributes + [:role, :availability, :auto_offline] + end + + def allowed_agent_params + [:name, :email, :name, :role, :availability, :auto_offline] + end + def agent_params - params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline) + params.require(:agent).permit(allowed_agent_params) end def new_agent_params @@ -101,3 +109,5 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController DeleteObjectJob.perform_later(agent) if agent.reload.account_users.blank? end end + +Api::V1::Accounts::AgentsController.prepend_mod_with('Api::V1::Accounts::AgentsController') diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index d164778ac..9148e4386 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -6,13 +6,15 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController def index @portal_articles = @portal.articles - @all_articles = @portal_articles.search(list_params) - @articles_count = @all_articles.count + + set_article_count + + @articles = @articles.search(list_params) @articles = if list_params[:category_slug].present? - @all_articles.order_by_position.page(@current_page) + @articles.order_by_position.page(@current_page) else - @all_articles.order_by_updated_at.page(@current_page) + @articles.order_by_updated_at.page(@current_page) end end @@ -43,6 +45,19 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController private + def set_article_count + # Search the params without status and author_id, use this to + # compute mine count published draft etc + base_search_params = list_params.except(:status, :author_id) + @articles = @portal_articles.search(base_search_params) + + @articles_count = @articles.count + @mine_articles_count = @articles.search_by_author(Current.user.id).count + @published_articles_count = @articles.published.count + @draft_articles_count = @articles.draft.count + @archived_articles_count = @articles.archived.count + end + def fetch_article @article = @portal.articles.find(params[:id]) end @@ -53,9 +68,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController def article_params params.require(:article).permit( - :title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, - :description, - { tags: [] }] + :title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, + :locale, meta: [:title, + :description, + { tags: [] }] ) end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 250c64a86..0d8e0ed93 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController before_action :check_authorization before_action :set_current_page, only: [:index, :active, :search, :filter] before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes] - before_action :set_include_contact_inboxes, only: [:index, :search, :filter] + before_action :set_include_contact_inboxes, only: [:index, :search, :filter, :show, :update] def index @contacts_count = resolved_contacts.count diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index d34561e36..63226f342 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: def destroy ActiveRecord::Base.transaction do - message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true }) + message.update!(content: I18n.t('conversations.messages.deleted'), content_type: :text, content_attributes: { deleted: true }) message.attachments.destroy_all end end diff --git a/app/controllers/api/v1/accounts/integrations/captain_controller.rb b/app/controllers/api/v1/accounts/integrations/captain_controller.rb index 7df0e61df..15e81b726 100644 --- a/app/controllers/api/v1/accounts/integrations/captain_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/captain_controller.rb @@ -1,22 +1,53 @@ class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::BaseController - before_action :check_admin_authorization? - before_action :fetch_hook + before_action :hook - def sso_url - params_string = - "token=#{URI.encode_www_form_component(@hook['settings']['access_token'])}" \ - "&email=#{URI.encode_www_form_component(@hook['settings']['account_email'])}" \ - "&account_id=#{URI.encode_www_form_component(@hook['settings']['account_id'])}" - - installation_config = InstallationConfig.find_by(name: 'CAPTAIN_APP_URL') - - sso_url = "#{installation_config.value}/sso?#{params_string}" - render json: { sso_url: sso_url }, status: :ok + def proxy + response = HTTParty.send(request_method, request_url, body: permitted_params[:body].to_json, headers: headers) + render plain: response.body, status: response.code end private - def fetch_hook - @hook = Current.account.hooks.find_by!(app_id: 'captain') + def headers + { + 'X-User-Email' => hook.settings['account_email'], + 'X-User-Token' => hook.settings['access_token'], + 'Content-Type' => 'application/json', + 'Accept' => '*/*' + } + end + + def request_path + request_route = with_leading_hash_on_route(params[:route]) + + return 'api/sessions/profile' if request_route == '/sessions/profile' + + "api/accounts/#{hook.settings['account_id']}#{request_route}" + end + + def request_url + base_url = InstallationConfig.find_by(name: 'CAPTAIN_API_URL').value + URI.join(base_url, request_path).to_s + end + + def hook + @hook ||= Current.account.hooks.find_by!(app_id: 'captain') + end + + def request_method + method = permitted_params[:method].downcase + raise 'Invalid or missing HTTP method' unless %w[get post put patch delete options head].include?(method) + + method + end + + def with_leading_hash_on_route(request_route) + return '' if request_route.blank? + + request_route.start_with?('/') ? request_route : "/#{request_route}" + end + + def permitted_params + params.permit(:method, :route, body: {}) end end diff --git a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb index d09ea2f00..087a9b78d 100644 --- a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb @@ -11,7 +11,17 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base end def process_event - render json: { message: @hook.process_event(params[:event]) } + response = @hook.process_event(params[:event]) + + # for cases like an invalid event, or when conversation does not have enough messages + # for a label suggestion, the response is nil + if response.nil? + render json: { message: nil } + elsif response[:error] + render json: { error: response[:error] }, status: :unprocessable_entity + else + render json: { message: response[:message] } + end end def destroy diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index bd8da5549..fe9a03ef5 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController end def create - @portal = Current.account.portals.build(portal_params) + @portal = Current.account.portals.build(portal_params.merge(live_chat_widget_params)) @portal.custom_domain = parsed_custom_domain @portal.save! process_attached_logo @@ -28,7 +28,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def update ActiveRecord::Base.transaction do - @portal.update!(portal_params) if params[:portal].present? + @portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present? # @portal.custom_domain = parsed_custom_domain process_attached_logo if params[:blob_id].present? rescue StandardError => e @@ -70,11 +70,21 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def portal_params params.require(:portal).permit( - :account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived, { config: [:default_locale, - { allowed_locales: [] }] } + :account_id, :color, :custom_domain, :header_text, :homepage_link, + :name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] } ) end + def live_chat_widget_params + permitted_params = params.permit(:inbox_id) + return {} if permitted_params[:inbox_id].blank? + + inbox = Inbox.find(permitted_params[:inbox_id]) + return {} unless inbox.web_widget? + + { channel_web_widget_id: inbox.channel.id } + end + def portal_member_params params.require(:portal).permit(:account_id, member_ids: []) end diff --git a/app/controllers/api/v1/accounts/upload_controller.rb b/app/controllers/api/v1/accounts/upload_controller.rb index 4c54938b7..6530279da 100644 --- a/app/controllers/api/v1/accounts/upload_controller.rb +++ b/app/controllers/api/v1/accounts/upload_controller.rb @@ -1,13 +1,68 @@ class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController def create - file_blob = ActiveStorage::Blob.create_and_upload!( - key: nil, - io: params[:attachment].tempfile, - filename: params[:attachment].original_filename, - content_type: params[:attachment].content_type - ) - file_blob.save! + result = if params[:attachment].present? + create_from_file + elsif params[:external_url].present? + create_from_url + else + render_error('No file or URL provided', :unprocessable_entity) + end + render_success(result) if result.is_a?(ActiveStorage::Blob) + end + + private + + def create_from_file + attachment = params[:attachment] + create_and_save_blob(attachment.tempfile, attachment.original_filename, attachment.content_type) + end + + def create_from_url + uri = parse_uri(params[:external_url]) + return if performed? + + fetch_and_process_file_from_uri(uri) + end + + def parse_uri(url) + uri = URI.parse(url) + validate_uri(uri) + uri + rescue URI::InvalidURIError, SocketError + render_error('Invalid URL provided', :unprocessable_entity) + nil + end + + def validate_uri(uri) + raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) + end + + def fetch_and_process_file_from_uri(uri) + uri.open do |file| + create_and_save_blob(file, File.basename(uri.path), file.content_type) + end + rescue OpenURI::HTTPError => e + render_error("Failed to fetch file from URL: #{e.message}", :unprocessable_entity) + rescue SocketError + render_error('Invalid URL provided', :unprocessable_entity) + rescue StandardError + render_error('An unexpected error occurred', :internal_server_error) + end + + def create_and_save_blob(io, filename, content_type) + ActiveStorage::Blob.create_and_upload!( + io: io, + filename: filename, + content_type: content_type + ) + end + + def render_success(file_blob) render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id } end + + def render_error(message, status) + render json: { error: message }, status: status + end end diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index bd3c2673c..ae1a1fe30 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -17,7 +17,7 @@ class Api::V1::ProfilesController < Api::BaseController def avatar @user.avatar.attachment.destroy! if @user.avatar.attached? - head :ok + @user.reload end def auto_offline diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 7e6d84bd2..fe5facc1a 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -11,6 +11,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController process_update_contact @conversation = create_conversation conversation.messages.create!(message_params) + # TODO: Temporary fix for message type cast issue, since message_type is returning as string instead of integer + conversation.reload end end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index ed5be5518..af3655e59 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -66,7 +66,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController end def check_authorization - raise Pundit::NotAuthorizedError unless Current.account_user.administrator? + return if Current.account_user.administrator? + + raise Pundit::NotAuthorizedError end def common_params @@ -135,3 +137,5 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics end end + +Api::V2::Accounts::ReportsController.prepend_mod_with('Api::V2::Accounts::ReportsController') diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 332f1528f..6ddaab73b 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -72,7 +72,7 @@ class DashboardController < ActionController::Base @application_pack = if request.path.include?('/auth') || request.path.include?('/login') 'v3app' else - 'application' + 'dashboard' end end diff --git a/app/controllers/google/callbacks_controller.rb b/app/controllers/google/callbacks_controller.rb index 391e1de0f..766d984df 100644 --- a/app/controllers/google/callbacks_controller.rb +++ b/app/controllers/google/callbacks_controller.rb @@ -1,6 +1,15 @@ class Google::CallbacksController < OauthCallbackController include GoogleConcern + def find_channel_by_email + # find by imap_login first, and then by email + # this ensures the legacy users can migrate correctly even if inbox email address doesn't match + imap_channel = Channel::Email.find_by(imap_login: users_data['email'], account: account) + return imap_channel if imap_channel + + Channel::Email.find_by(email: users_data['email'], account: account) + end + private def provider_name diff --git a/app/controllers/oauth_callback_controller.rb b/app/controllers/oauth_callback_controller.rb index 309160f1f..4cb02d266 100644 --- a/app/controllers/oauth_callback_controller.rb +++ b/app/controllers/oauth_callback_controller.rb @@ -25,7 +25,7 @@ class OauthCallbackController < ApplicationController end def find_or_create_inbox - channel_email = Channel::Email.find_by(email: users_data['email'], account: account) + channel_email = find_channel_by_email # we need this value to know where to redirect on sucessful processing of the callback channel_exists = channel_email.present? @@ -39,6 +39,10 @@ class OauthCallbackController < ApplicationController [channel_email.inbox, channel_exists] end + def find_channel_by_email + Channel::Email.find_by(email: users_data['email'], account: account) + end + def update_channel(channel_email) channel_email.update!({ imap_login: users_data['email'], imap_address: imap_address, diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index 9e6d53fef..e11cf9d4a 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -4,6 +4,7 @@ class Platform::Api::V1::AccountsController < PlatformController def create @resource = Account.create!(account_params) update_resource_features + @resource.save! @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 46ecf19a7..08c62bada 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -6,7 +6,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B layout 'portal' def index - @articles = @portal.articles + @articles = @portal.articles.published search_articles order_by_sort_param @articles.page(list_params[:page]) if list_params[:page].present? @@ -30,7 +30,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def set_article @article = @portal.articles.find_by(slug: permitted_params[:article_slug]) - @article.increment_view_count + @article.increment_view_count if @article.published? @parsed_content = render_article_content(@article.content) end diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb index de6ab42bb..b566551f4 100644 --- a/app/dashboards/account_dashboard.rb +++ b/app/dashboards/account_dashboard.rb @@ -26,7 +26,8 @@ class AccountDashboard < Administrate::BaseDashboard conversations: CountField, locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }), status: Field::Select.with_options(collection: [%w[Active active], %w[Suspended suspended]]), - account_users: Field::HasMany + account_users: Field::HasMany, + custom_attributes: Field::String }.merge(enterprise_attribute_types).freeze # COLLECTION_ATTRIBUTES @@ -45,7 +46,7 @@ class AccountDashboard < Administrate::BaseDashboard # SHOW_PAGE_ATTRIBUTES # an array of attributes that will be displayed on the model's show page. - enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : [] + enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[custom_attributes limits all_features] : [] SHOW_PAGE_ATTRIBUTES = (%i[ id name diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index da169d61f..c32cb98d2 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -1,6 +1,5 @@ + + diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/LiveChatCampaignDetails.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/LiveChatCampaignDetails.vue new file mode 100644 index 000000000..6a883ce6b --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/LiveChatCampaignDetails.vue @@ -0,0 +1,56 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/SMSCampaignDetails.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/SMSCampaignDetails.vue new file mode 100644 index 000000000..0c54c43eb --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/SMSCampaignDetails.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignLayout.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignLayout.vue new file mode 100644 index 000000000..3169dbc3e --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/CampaignLayout.vue @@ -0,0 +1,52 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/EmptyState/CampaignEmptyStateContent.js b/app/javascript/dashboard/components-next/Campaigns/EmptyState/CampaignEmptyStateContent.js new file mode 100644 index 000000000..4f409196f --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/EmptyState/CampaignEmptyStateContent.js @@ -0,0 +1,212 @@ +export const ONGOING_CAMPAIGN_EMPTY_STATE_CONTENT = [ + { + id: 1, + title: 'Chatbot Assistance', + inbox: { + id: 2, + name: 'PaperLayer Website', + channel_type: 'Channel::WebWidget', + phone_number: '', + }, + sender: { + id: 1, + name: 'Alexa Rivera', + }, + message: 'Hello! 👋 Need help with our chatbot features? Feel free to ask!', + campaign_status: 'active', + enabled: true, + campaign_type: 'ongoing', + trigger_rules: { + url: 'https://www.chatwoot.com/features/chatbot/', + time_on_page: 10, + }, + trigger_only_during_business_hours: true, + created_at: '2024-10-24T13:10:26.636Z', + updated_at: '2024-10-24T13:10:26.636Z', + }, + { + id: 2, + title: 'Pricing Information Support', + inbox: { + id: 2, + name: 'PaperLayer Website', + channel_type: 'Channel::WebWidget', + phone_number: '', + }, + sender: { + id: 1, + name: 'Jamie Lee', + }, + message: 'Hello! 👋 Any questions on pricing? I’m here to help!', + campaign_status: 'active', + enabled: false, + campaign_type: 'ongoing', + trigger_rules: { + url: 'https://www.chatwoot.com/pricings', + time_on_page: 10, + }, + trigger_only_during_business_hours: false, + created_at: '2024-10-24T13:11:08.763Z', + updated_at: '2024-10-24T13:11:08.763Z', + }, + { + id: 3, + title: 'Product Setup Assistance', + inbox: { + id: 2, + name: 'PaperLayer Website', + channel_type: 'Channel::WebWidget', + phone_number: '', + }, + sender: { + id: 1, + name: 'Chatwoot', + }, + message: 'Hi! Chatwoot here. Need help setting up? Let me know!', + campaign_status: 'active', + enabled: false, + campaign_type: 'ongoing', + trigger_rules: { + url: 'https://{*.}?chatwoot.com/apps/account/*/settings/inboxes/new/', + time_on_page: 10, + }, + trigger_only_during_business_hours: false, + created_at: '2024-10-24T13:11:44.285Z', + updated_at: '2024-10-24T13:11:44.285Z', + }, + { + id: 4, + title: 'General Assistance Campaign', + inbox: { + id: 2, + name: 'PaperLayer Website', + channel_type: 'Channel::WebWidget', + phone_number: '', + }, + sender: { + id: 1, + name: 'Chris Barlow', + }, + message: + 'Hi there! 👋 I’m here for any questions you may have. Let’s chat!', + campaign_status: 'active', + enabled: true, + campaign_type: 'ongoing', + trigger_rules: { + url: 'https://siv.com', + time_on_page: 200, + }, + trigger_only_during_business_hours: false, + created_at: '2024-10-29T19:54:33.741Z', + updated_at: '2024-10-29T19:56:26.296Z', + }, +]; + +export const ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT = [ + { + id: 1, + title: 'Customer Feedback Request', + inbox: { + id: 6, + name: 'PaperLayer Mobile', + channel_type: 'Channel::Sms', + phone_number: '+29818373149903', + provider: 'default', + }, + message: + 'Hello! Enjoying our product? Share your feedback on G2 and earn a $25 Amazon coupon: https://chwt.app/g2-review', + campaign_status: 'active', + enabled: true, + campaign_type: 'one_off', + scheduled_at: 1729775588, + audience: [ + { id: 4, type: 'Label' }, + { id: 5, type: 'Label' }, + { id: 6, type: 'Label' }, + ], + trigger_rules: {}, + trigger_only_during_business_hours: false, + created_at: '2024-10-24T13:13:08.496Z', + updated_at: '2024-10-24T13:15:38.698Z', + }, + { + id: 2, + title: 'Welcome New Customer', + inbox: { + id: 6, + name: 'PaperLayer Mobile', + channel_type: 'Channel::Sms', + phone_number: '+29818373149903', + provider: 'default', + }, + message: 'Welcome aboard! 🎉 Let us know if you have any questions.', + campaign_status: 'completed', + enabled: true, + campaign_type: 'one_off', + scheduled_at: 1729732500, + audience: [ + { id: 1, type: 'Label' }, + { id: 6, type: 'Label' }, + { id: 5, type: 'Label' }, + { id: 2, type: 'Label' }, + { id: 4, type: 'Label' }, + ], + trigger_rules: {}, + trigger_only_during_business_hours: false, + created_at: '2024-10-24T13:14:00.168Z', + updated_at: '2024-10-24T13:15:38.707Z', + }, + { + id: 3, + title: 'New Business Welcome', + inbox: { + id: 6, + name: 'PaperLayer Mobile', + channel_type: 'Channel::Sms', + phone_number: '+29818373149903', + provider: 'default', + }, + message: 'Hello! We’re excited to have your business with us!', + campaign_status: 'active', + enabled: true, + campaign_type: 'one_off', + scheduled_at: 1730368440, + audience: [ + { id: 1, type: 'Label' }, + { id: 3, type: 'Label' }, + { id: 6, type: 'Label' }, + { id: 4, type: 'Label' }, + { id: 2, type: 'Label' }, + { id: 5, type: 'Label' }, + ], + trigger_rules: {}, + trigger_only_during_business_hours: false, + created_at: '2024-10-30T07:54:49.915Z', + updated_at: '2024-10-30T07:54:49.915Z', + }, + { + id: 4, + title: 'New Member Onboarding', + inbox: { + id: 6, + name: 'PaperLayer Mobile', + channel_type: 'Channel::Sms', + phone_number: '+29818373149903', + provider: 'default', + }, + message: 'Welcome to the team! Reach out if you have questions.', + campaign_status: 'completed', + enabled: true, + campaign_type: 'one_off', + scheduled_at: 1730304840, + audience: [ + { id: 1, type: 'Label' }, + { id: 3, type: 'Label' }, + { id: 6, type: 'Label' }, + ], + trigger_rules: {}, + trigger_only_during_business_hours: false, + created_at: '2024-10-29T16:14:10.374Z', + updated_at: '2024-10-30T16:15:03.157Z', + }, +]; diff --git a/app/javascript/dashboard/components-next/Campaigns/EmptyState/LiveChatCampaignEmptyState.vue b/app/javascript/dashboard/components-next/Campaigns/EmptyState/LiveChatCampaignEmptyState.vue new file mode 100644 index 000000000..d2086b1d4 --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/EmptyState/LiveChatCampaignEmptyState.vue @@ -0,0 +1,38 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/EmptyState/SMSCampaignEmptyState.vue b/app/javascript/dashboard/components-next/Campaigns/EmptyState/SMSCampaignEmptyState.vue new file mode 100644 index 000000000..ab01acb4a --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/EmptyState/SMSCampaignEmptyState.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue new file mode 100644 index 000000000..183573606 --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue @@ -0,0 +1,38 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue new file mode 100644 index 000000000..f55c88e46 --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/EditLiveChatCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/EditLiveChatCampaignDialog.vue new file mode 100644 index 000000000..9f46a1bde --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/EditLiveChatCampaignDialog.vue @@ -0,0 +1,74 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignDialog.vue new file mode 100644 index 000000000..2c4b9848c --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignDialog.vue @@ -0,0 +1,54 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignForm.vue new file mode 100644 index 000000000..1a9bc8cdf --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignForm.vue @@ -0,0 +1,323 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue new file mode 100644 index 000000000..b190b031b --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignForm.vue new file mode 100644 index 000000000..653e58a9a --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignForm.vue @@ -0,0 +1,189 @@ + + +