diff --git a/.circleci/config.yml b/.circleci/config.yml
index b8a628677..bc7053130 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,13 +19,19 @@ jobs:
steps:
- checkout
- node/install:
- node-version: '20.12'
+ node-version: '23.7'
- node/install-pnpm
- node/install-packages:
pkg-manager: pnpm
override-ci-command: pnpm i
- run: node --version
- run: pnpm --version
+ - run:
+ name: Add PostgreSQL repository and update
+ command: |
+ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
+ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
+ sudo apt-get update -y
- run:
name: Install System Dependencies
@@ -34,7 +40,9 @@ jobs:
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
libpq-dev \
redis-server \
- postgresql \
+ postgresql-common \
+ postgresql-16 \
+ postgresql-16-pgvector \
build-essential \
git \
curl \
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 213f7d172..c68251f32 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -51,6 +51,7 @@ exclude_patterns:
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
+ - 'app/javascript/dashboard/store/captain/storeFactory.js'
- 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js'
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index a804bb15c..c5530ac17 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -5,30 +5,30 @@
version: '3'
services:
- base:
+ base:
build:
context: ..
dockerfile: .devcontainer/Dockerfile.base
args:
- VARIANT: "ubuntu-22.04"
- NODE_VERSION: "20.9.0"
- RUBY_VERSION: "3.3.3"
+ VARIANT: 'ubuntu-22.04'
+ NODE_VERSION: '23.7.0'
+ RUBY_VERSION: '3.3.3'
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
- USER_UID: "1000"
- USER_GID: "1000"
+ USER_UID: '1000'
+ USER_GID: '1000'
image: base:latest
-
+
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
args:
- VARIANT: "ubuntu-22.04"
- NODE_VERSION: "20.9.0"
- RUBY_VERSION: "3.3.3"
+ VARIANT: 'ubuntu-22.04'
+ NODE_VERSION: '23.7.0'
+ RUBY_VERSION: '3.3.3'
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
- USER_UID: "1000"
- USER_GID: "1000"
+ USER_UID: '1000'
+ USER_GID: '1000'
volumes:
- ..:/workspace:cached
@@ -40,7 +40,7 @@ services:
network_mode: service:db
db:
- image: postgres:latest
+ image: pgvector/pgvector:pg16
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
diff --git a/.eslintrc.js b/.eslintrc.js
index a2932b2a9..6c867f557 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,6 +4,8 @@ module.exports = {
'prettier',
'plugin:vue/vue3-recommended',
'plugin:vitest-globals/recommended',
+ // use recommended-legacy when upgrading the plugin to v4
+ 'plugin:@intlify/vue-i18n/recommended',
],
overrides: [
{
@@ -229,6 +231,18 @@ module.exports = {
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off'],
'no-console': 'error',
+ '@intlify/vue-i18n/no-dynamic-keys': 'warn',
+ '@intlify/vue-i18n/no-unused-keys': [
+ 'warn',
+ {
+ extensions: ['.js', '.vue'],
+ },
+ ],
+ },
+ settings: {
+ 'vue-i18n': {
+ localeDir: './app/javascript/*/i18n/**.json',
+ },
},
env: {
browser: true,
diff --git a/.github/workflows/frontend-fe.yml b/.github/workflows/frontend-fe.yml
index 15bb6f5e9..5af4857e0 100644
--- a/.github/workflows/frontend-fe.yml
+++ b/.github/workflows/frontend-fe.yml
@@ -23,12 +23,10 @@ jobs:
bundler-cache: true
- uses: pnpm/action-setup@v4
- with:
- version: 9.3.0
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 23
cache: 'pnpm'
- name: Install pnpm dependencies
diff --git a/.github/workflows/nightly_installer.yml b/.github/workflows/nightly_installer.yml
index d11fe6401..a01ba1093 100644
--- a/.github/workflows/nightly_installer.yml
+++ b/.github/workflows/nightly_installer.yml
@@ -16,7 +16,7 @@ on:
jobs:
nightly:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- name: get installer
diff --git a/.github/workflows/publish_ee_docker.yml b/.github/workflows/publish_ee_docker.yml
new file mode 100644
index 000000000..8e2c22481
--- /dev/null
+++ b/.github/workflows/publish_ee_docker.yml
@@ -0,0 +1,140 @@
+# #
+# # This action will publish Chatwoot EE docker image.
+# # This is set to run against merges to develop, master
+# # and when tags are created.
+# #
+
+name: Publish Chatwoot EE docker images
+
+on:
+ push:
+ branches:
+ - develop
+ - master
+ tags:
+ - v*
+ workflow_dispatch:
+
+env:
+ DOCKER_REPO: chatwoot/chatwoot
+
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-22.04-arm
+ runs-on: ${{ matrix.runner }}
+ env:
+ GIT_REF: ${{ github.head_ref || github.ref_name }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Prepare
+ run: |
+ platform=${{ matrix.platform }}
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
+
+ - name: Set Chatwoot edition
+ run: |
+ echo -en '\nENV CW_EDITION="ee"' >> docker/Dockerfile
+
+ - name: Set Docker Tags
+ run: |
+ SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
+ if [ "${{ github.ref_name }}" = "master" ]; then
+ echo "DOCKER_TAG=${DOCKER_REPO}:latest" >> $GITHUB_ENV
+ else
+ echo "DOCKER_TAG=${DOCKER_REPO}:${SANITIZED_REF}" >> $GITHUB_ENV
+ fi
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to DockerHub
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Build and push by digest
+ id: build
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/Dockerfile
+ platforms: ${{ matrix.platform }}
+ push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
+ outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
+
+ - name: Export digest
+ run: |
+ mkdir -p ${{ runner.temp }}/digests
+ digest="${{ steps.build.outputs.digest }}"
+ touch "${{ runner.temp }}/digests/${digest#sha256:}"
+
+ - name: Upload digest
+ uses: actions/upload-artifact@v4
+ with:
+ name: digests-${{ env.PLATFORM_PAIR }}
+ path: ${{ runner.temp }}/digests/*
+ if-no-files-found: error
+ retention-days: 1
+
+ merge:
+ runs-on: ubuntu-latest
+ needs:
+ - build
+ steps:
+ - name: Download digests
+ uses: actions/download-artifact@v4
+ with:
+ path: ${{ runner.temp }}/digests
+ pattern: digests-*
+ merge-multiple: true
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Create manifest list and push
+ working-directory: ${{ runner.temp }}/digests
+ env:
+ GIT_REF: ${{ github.head_ref || github.ref_name }}
+ run: |
+ SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
+ if [ "${{ github.ref_name }}" = "master" ]; then
+ TAG="${DOCKER_REPO}:latest"
+ else
+ TAG="${DOCKER_REPO}:${SANITIZED_REF}"
+ fi
+
+ docker buildx imagetools create -t $TAG \
+ $(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
+
+ - name: Inspect image
+ env:
+ GIT_REF: ${{ github.head_ref || github.ref_name }}
+ run: |
+ SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
+ if [ "${{ github.ref_name }}" = "master" ]; then
+ TAG="${DOCKER_REPO}:latest"
+ else
+ TAG="${DOCKER_REPO}:${SANITIZED_REF}"
+ fi
+
+ docker buildx imagetools inspect $TAG
diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml
index 732b22a28..3075a7f3d 100644
--- a/.github/workflows/publish_foss_docker.yml
+++ b/.github/workflows/publish_foss_docker.yml
@@ -5,6 +5,7 @@
# #
name: Publish Chatwoot CE docker images
+
on:
push:
branches:
@@ -12,23 +13,32 @@ on:
- master
tags:
- v*
-# pull_request:
workflow_dispatch:
+env:
+ DOCKER_REPO: chatwoot/chatwoot
+
jobs:
build:
- runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-22.04-arm
+ runs-on: ${{ matrix.runner }}
env:
- GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
+ GIT_REF: ${{ github.head_ref || github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v1
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v1
+ - name: Prepare
+ run: |
+ platform=${{ matrix.platform }}
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Strip enterprise code
run: |
@@ -39,29 +49,97 @@ jobs:
run: |
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
- - name: set docker tag
+ - name: Set Docker Tags
run: |
- # Replace forward slashes with hyphens in the ref name
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
- echo "DOCKER_TAG=chatwoot/chatwoot:$SANITIZED_REF-ce" >> $GITHUB_ENV
+ if [ "${{ github.ref_name }}" = "master" ]; then
+ echo "DOCKER_TAG=${DOCKER_REPO}:latest-ce" >> $GITHUB_ENV
+ else
+ echo "DOCKER_TAG=${DOCKER_REPO}:${SANITIZED_REF}-ce" >> $GITHUB_ENV
+ fi
- - name: replace docker tag if master
- if: github.ref_name == 'master'
- run: |
- echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
- uses: docker/login-action@v1
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Build and push
- uses: docker/build-push-action@v2
+ - name: Build and push by digest
+ id: build
+ uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
- platforms: linux/amd64, linux/arm64
+ platforms: ${{ matrix.platform }}
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
- tags: ${{ env.DOCKER_TAG }}
+ outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
+
+ - name: Export digest
+ run: |
+ mkdir -p ${{ runner.temp }}/digests
+ digest="${{ steps.build.outputs.digest }}"
+ touch "${{ runner.temp }}/digests/${digest#sha256:}"
+
+ - name: Upload digest
+ uses: actions/upload-artifact@v4
+ with:
+ name: digests-${{ env.PLATFORM_PAIR }}
+ path: ${{ runner.temp }}/digests/*
+ if-no-files-found: error
+ retention-days: 1
+
+ merge:
+ runs-on: ubuntu-latest
+ needs:
+ - build
+ steps:
+ - name: Download digests
+ uses: actions/download-artifact@v4
+ with:
+ path: ${{ runner.temp }}/digests
+ pattern: digests-*
+ merge-multiple: true
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Create manifest list and push
+ working-directory: ${{ runner.temp }}/digests
+ env:
+ GIT_REF: ${{ github.head_ref || github.ref_name }}
+ run: |
+ SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
+ if [ "${{ github.ref_name }}" = "master" ]; then
+ TAG="${DOCKER_REPO}:latest-ce"
+ else
+ TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce"
+ fi
+
+ docker buildx imagetools create -t $TAG \
+ $(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
+
+ - name: Inspect image
+ env:
+ GIT_REF: ${{ github.head_ref || github.ref_name }}
+ run: |
+ SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
+ if [ "${{ github.ref_name }}" = "master" ]; then
+ TAG="${DOCKER_REPO}:latest-ce"
+ else
+ TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce"
+ fi
+
+ docker buildx imagetools inspect $TAG
diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml
index 6a018b314..5a9d35d0b 100644
--- a/.github/workflows/run_foss_spec.yml
+++ b/.github/workflows/run_foss_spec.yml
@@ -1,9 +1,3 @@
-# #
-# # This action will strip the enterprise folder
-# # and run the spec.
-# # This is set to run against every PR.
-# #
-
name: Run Chatwoot CE spec
on:
push:
@@ -18,7 +12,7 @@ jobs:
runs-on: ubuntu-20.04
services:
postgres:
- image: postgres:15.3
+ image: pgvector/pgvector:pg15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ''
@@ -44,7 +38,6 @@ jobs:
- 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 }}
@@ -54,7 +47,7 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 23
cache: 'pnpm'
- name: Install pnpm dependencies
diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml
deleted file mode 100644
index c788a0400..000000000
--- a/.github/workflows/run_response_bot_spec.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-# #
-# # 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@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 # 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: pnpm
-
- - name: pnpm
- run: pnpm 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/services/enterprise/message_templates/response_bot_service_spec.rb \
- spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
- spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \
- --profile=10 \
- --format documentation
-
- - name: Upload rails log folder
- uses: actions/upload-artifact@v4
- if: always()
- with:
- name: rails-log-folder
- path: log
diff --git a/.github/workflows/size-limit.yml b/.github/workflows/size-limit.yml
index 0758ca7d0..724f69ec3 100644
--- a/.github/workflows/size-limit.yml
+++ b/.github/workflows/size-limit.yml
@@ -19,13 +19,11 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- - uses: pnpm/action-setup@v2
- with:
- version: 9.3.0
+ - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 23
cache: 'pnpm'
- name: pnpm
@@ -39,7 +37,7 @@ jobs:
- name: setup env
run: |
cp .env.example .env
-
+
- name: Run asset compile
run: bundle exec rake assets:precompile
env:
@@ -47,5 +45,3 @@ jobs:
- name: Size Check
run: pnpm run size
-
-
diff --git a/.github/workflows/test_docker_build.yml b/.github/workflows/test_docker_build.yml
new file mode 100644
index 000000000..460c4ba1f
--- /dev/null
+++ b/.github/workflows/test_docker_build.yml
@@ -0,0 +1,40 @@
+name: Test Docker Build
+
+on:
+ pull_request:
+ branches:
+ - develop
+ - master
+ workflow_dispatch:
+
+jobs:
+ test-build:
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-22.04-arm
+ runs-on: ${{ matrix.runner }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/Dockerfile
+ platforms: ${{ matrix.platform }}
+ push: false
+ load: false
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.gitignore b/.gitignore
index 5eb883db0..77c4a4740 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,6 +73,7 @@ test/cypress/videos/*
#ignore files under .vscode directory
.vscode
+.cursor
# yalc for local testing
.yalc
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 2525dde4e..adda426ad 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/Gemfile b/Gemfile
index 439b0f63e..937aef4af 100644
--- a/Gemfile
+++ b/Gemfile
@@ -94,7 +94,7 @@ gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
-gem 'slack-ruby-client', '~> 2.2.0'
+gem 'slack-ruby-client', '~> 2.5.2'
# for dialogflow integrations
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
gem 'grpc'
@@ -138,9 +138,7 @@ gem 'procore-sift'
# parse email
gem 'email_reply_trimmer'
-# TODO: we might have to fork this gem since 0.3.1 has hard depency on nokogir 1.10.
-# and this gem hasn't been updated for a while.
-gem 'html2text', git: 'https://github.com/chatwoot/html2text_ruby', branch: 'chatwoot'
+gem 'html2text'
# to calculate working hours
gem 'working_hours'
@@ -175,6 +173,8 @@ gem 'pgvector'
# Convert Website HTML to Markdown
gem 'reverse_markdown'
+gem 'ruby-openai'
+
### Gems required only in specific deployment environments ###
##############################################################
diff --git a/Gemfile.lock b/Gemfile.lock
index 74cdbeea8..e9a573816 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -22,14 +22,6 @@ GIT
devise (>= 4.0.0, < 5.0.0)
railties (>= 5.0.0, < 8.0.0)
-GIT
- remote: https://github.com/chatwoot/html2text_ruby
- revision: cdbdbbbf898d846d0136d69d688a003c6b26074b
- branch: chatwoot
- specs:
- html2text (0.3.1)
- nokogiri (>= 1.13.6)
-
GEM
remote: https://rubygems.org/
specs:
@@ -186,7 +178,7 @@ GEM
database_cleaner-core (2.0.1)
datadog-ci (0.8.3)
msgpack
- date (3.3.4)
+ date (3.4.1)
ddtrace (1.23.2)
datadog-ci (~> 0.8.1)
debase-ruby_core_source (= 3.3.1)
@@ -231,6 +223,7 @@ GEM
erubi (1.13.0)
et-orbi (1.2.11)
tzinfo
+ event_stream_parser (1.0.0)
execjs (2.8.1)
facebook-messenger (2.0.1)
httparty (~> 0.13, >= 0.13.7)
@@ -279,7 +272,8 @@ GEM
googleauth (~> 1.0)
grpc (~> 1.36)
geocoder (1.8.1)
- gli (2.21.1)
+ gli (2.22.2)
+ ostruct
globalid (1.2.1)
activesupport (>= 6.1)
gmail_xoauth (0.4.3)
@@ -360,6 +354,8 @@ GEM
hana (1.3.7)
hashdiff (1.1.0)
hashie (5.0.0)
+ html2text (0.4.0)
+ nokogiri (>= 1.0, < 2.0)
http (5.1.1)
addressable (~> 2.8)
http-cookie (~> 1.0)
@@ -486,7 +482,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
- net-imap (0.4.17)
+ net-imap (0.4.19)
date
net-protocol
net-pop (0.1.2)
@@ -502,14 +498,14 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
- nokogiri (1.17.1)
+ nokogiri (1.18.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- nokogiri (1.17.1-arm64-darwin)
+ nokogiri (1.18.3-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.17.1-x86_64-darwin)
+ nokogiri (1.18.3-x86_64-darwin)
racc (~> 1.4)
- nokogiri (1.17.1-x86_64-linux)
+ nokogiri (1.18.3-x86_64-linux-gnu)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@@ -542,6 +538,7 @@ GEM
openssl (3.2.0)
orm_adapter (0.5.0)
os (1.1.4)
+ ostruct (0.6.1)
parallel (1.23.0)
parser (3.2.2.1)
ast (~> 2.4.1)
@@ -564,7 +561,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
- rack (2.2.10)
+ rack (2.2.11)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
@@ -684,6 +681,10 @@ GEM
rubocop-rspec (2.21.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
+ ruby-openai (7.3.1)
+ event_stream_parser (>= 0.3.0, < 2.0.0)
+ faraday (>= 1)
+ faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
@@ -746,12 +747,13 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
- slack-ruby-client (2.2.0)
+ slack-ruby-client (2.5.2)
faraday (>= 2.0)
faraday-mashify
faraday-multipart
gli
hashie
+ logger
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
@@ -777,7 +779,7 @@ GEM
time_diff (0.3.0)
activesupport
i18n
- timeout (0.4.1)
+ timeout (0.4.3)
trailblazer-option (0.1.2)
twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0)
@@ -892,7 +894,7 @@ DEPENDENCIES
haikunator
hairtrigger
hashie
- html2text!
+ html2text
image_processing
jbuilder
json_refs
@@ -941,6 +943,7 @@ DEPENDENCIES
rubocop-performance
rubocop-rails
rubocop-rspec
+ ruby-openai
scout_apm
scss_lint
seed_dump
@@ -951,7 +954,7 @@ DEPENDENCIES
sidekiq (>= 7.3.1)
sidekiq-cron (>= 1.12.0)
simplecov (= 0.17.1)
- slack-ruby-client (~> 2.2.0)
+ slack-ruby-client (~> 2.5.2)
spring
spring-watcher-listen
squasher
diff --git a/README.md b/README.md
index e5f73eb97..f09bd5698 100644
--- a/README.md
+++ b/README.md
@@ -120,4 +120,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
-*Chatwoot* © 2017-2024, Chatwoot Inc - Released under the MIT License.
+*Chatwoot* © 2017-2025, Chatwoot Inc - Released under the MIT License.
diff --git a/SECURITY.md b/SECURITY.md
index 0722f9217..ee9eba0b8 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,13 +2,13 @@ Chatwoot is looking forward to working with security researchers worldwide to ke
## Reporting a Vulnerability
-We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose). This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
+We use Github to track the security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://github.com/chatwoot/chatwoot/security/advisories/new). This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
If you have any questions about the process, contact security@chatwoot.com.
-Please try your best to describe a clear and realistic impact for your report, and please don't open any public issues on GitHub or social media; we're doing our best to respond through Huntr as quickly as possible.
+Please try your best to describe a clear and realistic impact for your report, and please don't open any public issues on GitHub or social media; we're doing our best to respond through Github as quickly as possible.
-> Note: Please use the email for questions related to the process. Disclosures should be done via [huntr.dev](https://huntr.dev/)
+> Note: Please use the email for questions related to the process. Disclosures should be done via [Github](https://github.com/chatwoot/chatwoot/security/advisories/new)
## Supported versions
| Version | Supported |
@@ -48,7 +48,7 @@ We consider the following out of scope, though there may be exceptions.
- Brute force attacks
- DNSSEC
-If you are unsure about the scope, please create a [report](https://huntr.dev/repos/chatwoot/chatwoot/).
+If you are unsure about the scope, please create a [report](https://github.com/chatwoot/chatwoot/security/advisories/new).
## Thanks
diff --git a/VERSION_CWCTL b/VERSION_CWCTL
index 4a36342fc..944880fa1 100644
--- a/VERSION_CWCTL
+++ b/VERSION_CWCTL
@@ -1 +1 @@
-3.0.0
+3.2.0
diff --git a/app/builders/notification_builder.rb b/app/builders/notification_builder.rb
index 3d8ce9674..d5461eb4e 100644
--- a/app/builders/notification_builder.rb
+++ b/app/builders/notification_builder.rb
@@ -25,6 +25,8 @@ class NotificationBuilder
def build_notification
# Create conversation_creation notification only if user is subscribed to it
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
+ # skip notifications for blocked conversations except for user mentions
+ return if primary_actor.contact.blocked? && notification_type != 'conversation_mention'
user.notifications.create!(
notification_type: notification_type,
diff --git a/app/builders/v2/reports/timeseries/average_report_builder.rb b/app/builders/v2/reports/timeseries/average_report_builder.rb
index 3e30557e1..5df718b6a 100644
--- a/app/builders/v2/reports/timeseries/average_report_builder.rb
+++ b/app/builders/v2/reports/timeseries/average_report_builder.rb
@@ -28,7 +28,7 @@ class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::B
end
def object_scope
- scope.reporting_events.where(name: event_name, created_at: range)
+ scope.reporting_events.where(name: event_name, created_at: range, account_id: account.id)
end
def reporting_events
diff --git a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb
index 5e0a0e55e..de0ac4db9 100644
--- a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb
@@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
def index
@conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings
- ).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20)
+ ).where(inbox_id: inbox_ids, contact_id: @contact.id).order(last_activity_at: :desc).limit(20)
end
private
diff --git a/app/controllers/api/v1/accounts/inbox_members_controller.rb b/app/controllers/api/v1/accounts/inbox_members_controller.rb
index 0cb1a0c85..bf8822c27 100644
--- a/app/controllers/api/v1/accounts/inbox_members_controller.rb
+++ b/app/controllers/api/v1/accounts/inbox_members_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
def create
authorize @inbox, :create?
ActiveRecord::Base.transaction do
- agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) }
+ @inbox.add_members(agents_to_be_added_ids)
end
fetch_updated_agents
end
@@ -24,7 +24,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
def destroy
authorize @inbox, :destroy?
ActiveRecord::Base.transaction do
- params[:user_ids].map { |user_id| @inbox.remove_member(user_id) }
+ @inbox.remove_members(params[:user_ids])
end
head :ok
end
@@ -41,8 +41,8 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
# the missing ones are the agents which are to be deleted from the inbox
# the new ones are the agents which are to be added to the inbox
ActiveRecord::Base.transaction do
- agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
- agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
+ @inbox.add_members(agents_to_be_added_ids)
+ @inbox.remove_members(agents_to_be_removed_ids)
end
end
diff --git a/app/controllers/api/v1/accounts/integrations/captain_controller.rb b/app/controllers/api/v1/accounts/integrations/captain_controller.rb
deleted file mode 100644
index 6813d14b5..000000000
--- a/app/controllers/api/v1/accounts/integrations/captain_controller.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::BaseController
- before_action :hook
-
- def proxy
- request_url = build_request_url(request_path)
- response = HTTParty.send(request_method, request_url, body: permitted_params[:body].to_json, headers: headers)
- render plain: response.body, status: response.code
- end
-
- def copilot
- request_url = build_request_url(build_request_path("/assistants/#{hook.settings['assistant_id']}/copilot"))
- params = {
- previous_messages: copilot_params[:previous_messages],
- conversation_history: conversation_history,
- message: copilot_params[:message]
- }
- response = HTTParty.send(:post, request_url, body: params.to_json, headers: headers)
- render plain: response.body, status: response.code
- end
-
- private
-
- def headers
- {
- 'X-User-Email' => hook.settings['account_email'],
- 'X-User-Token' => hook.settings['access_token'],
- 'Content-Type' => 'application/json',
- 'Accept' => '*/*'
- }
- end
-
- def build_request_path(route)
- "api/accounts/#{hook.settings['account_id']}#{route}"
- end
-
- def request_path
- request_route = with_leading_hash_on_route(params[:route])
-
- return 'api/sessions/profile' if request_route == '/sessions/profile'
-
- build_request_path(request_route)
- end
-
- def build_request_url(request_path)
- 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 conversation_history
- conversation = Current.account.conversations.find_by!(display_id: copilot_params[:conversation_id])
- conversation.to_llm_text
- end
-
- def copilot_params
- params.permit(:previous_messages, :conversation_id, :message)
- end
-
- def permitted_params
- params.permit(:method, :route, body: {})
- end
-end
diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb
index 814373c7e..4e5348e88 100644
--- a/app/controllers/api/v1/accounts/integrations/linear_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb
@@ -1,5 +1,11 @@
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:link_issue, :linked_issues]
+ before_action :fetch_hook, only: [:destroy]
+
+ def destroy
+ @hook.destroy!
+ head :ok
+ end
def teams
teams = linear_processor_service.teams
@@ -90,4 +96,8 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
def permitted_params
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
end
+
+ def fetch_hook
+ @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear')
+ end
end
diff --git a/app/controllers/api/v1/accounts/team_members_controller.rb b/app/controllers/api/v1/accounts/team_members_controller.rb
index 0243a915a..ffb115a34 100644
--- a/app/controllers/api/v1/accounts/team_members_controller.rb
+++ b/app/controllers/api/v1/accounts/team_members_controller.rb
@@ -9,14 +9,14 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
def create
ActiveRecord::Base.transaction do
- @team_members = members_to_be_added_ids.map { |user_id| @team.add_member(user_id) }
+ @team_members = @team.add_members(members_to_be_added_ids)
end
end
def update
ActiveRecord::Base.transaction do
- members_to_be_added_ids.each { |user_id| @team.add_member(user_id) }
- members_to_be_removed_ids.each { |user_id| @team.remove_member(user_id) }
+ @team.add_members(members_to_be_added_ids)
+ @team.remove_members(members_to_be_removed_ids)
end
@team_members = @team.members
render action: 'create'
@@ -24,7 +24,7 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
def destroy
ActiveRecord::Base.transaction do
- params[:user_ids].map { |user_id| @team.remove_member(user_id) }
+ @team.remove_members(params[:user_ids])
end
head :ok
end
diff --git a/app/controllers/api/v2/accounts/live_reports_controller.rb b/app/controllers/api/v2/accounts/live_reports_controller.rb
new file mode 100644
index 000000000..1c703764b
--- /dev/null
+++ b/app/controllers/api/v2/accounts/live_reports_controller.rb
@@ -0,0 +1,64 @@
+class Api::V2::Accounts::LiveReportsController < Api::V1::Accounts::BaseController
+ before_action :load_conversations, only: [:conversation_metrics, :grouped_conversation_metrics]
+ before_action :set_group_scope, only: [:grouped_conversation_metrics]
+
+ before_action :check_authorization
+
+ def conversation_metrics
+ render json: {
+ open: @conversations.open.count,
+ unattended: @conversations.open.unattended.count,
+ unassigned: @conversations.open.unassigned.count,
+ pending: @conversations.pending.count
+ }
+ end
+
+ def grouped_conversation_metrics
+ count_by_group = @conversations.open.group(@group_scope).count
+ unattended_by_group = @conversations.open.unattended.group(@group_scope).count
+ unassigned_by_group = @conversations.open.unassigned.group(@group_scope).count
+
+ group_metrics = count_by_group.map do |group_id, count|
+ metric = {
+ open: count,
+ unattended: unattended_by_group[group_id] || 0,
+ unassigned: unassigned_by_group[group_id] || 0
+ }
+ metric[@group_scope] = group_id
+ metric
+ end
+
+ render json: group_metrics
+ end
+
+ private
+
+ def check_authorization
+ authorize :report, :view?
+ end
+
+ def set_group_scope
+ render json: { error: 'invalid group_by' }, status: :unprocessable_entity and return unless %w[
+ team_id
+ assignee_id
+ ].include?(permitted_params[:group_by])
+
+ @group_scope = permitted_params[:group_by]
+ end
+
+ def team
+ return unless permitted_params[:team_id]
+
+ @team ||= Current.account.teams.find(permitted_params[:team_id])
+ end
+
+ def load_conversations
+ scope = Current.account.conversations
+ scope = scope.where(team_id: team.id) if team.present?
+ @conversations = scope
+ end
+
+ def permitted_params
+ params.permit(:team_id, :group_by)
+ end
+end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 6ddaab73b..0f915ab8a 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -7,13 +7,17 @@ class DashboardController < ActionController::Base
around_action :switch_locale
before_action :ensure_installation_onboarding, only: [:index]
before_action :render_hc_if_custom_domain, only: [:index]
-
+ before_action :ensure_html_format
layout 'vueapp'
def index; end
private
+ def ensure_html_format
+ head :not_acceptable unless request.format.html?
+ end
+
def set_global_config
@global_config = GlobalConfig.get(
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
@@ -32,7 +36,7 @@ class DashboardController < ActionController::Base
'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE',
'DEPLOYMENT_ENV',
- 'CSML_EDITOR_HOST'
+ 'CSML_EDITOR_HOST', 'INSTALLATION_PRICING_PLAN'
).merge(app_config)
end
diff --git a/app/controllers/linear/callbacks_controller.rb b/app/controllers/linear/callbacks_controller.rb
new file mode 100644
index 000000000..2eea49333
--- /dev/null
+++ b/app/controllers/linear/callbacks_controller.rb
@@ -0,0 +1,73 @@
+class Linear::CallbacksController < ApplicationController
+ include Linear::IntegrationHelper
+
+ def show
+ @response = oauth_client.auth_code.get_token(
+ params[:code],
+ redirect_uri: "#{base_url}/linear/callback"
+ )
+
+ handle_response
+ rescue StandardError => e
+ Rails.logger.error("Linear callback error: #{e.message}")
+ redirect_to linear_redirect_uri
+ end
+
+ private
+
+ def oauth_client
+ app_id = GlobalConfigService.load('LINEAR_CLIENT_ID', nil)
+ app_secret = GlobalConfigService.load('LINEAR_CLIENT_SECRET', nil)
+
+ OAuth2::Client.new(
+ app_id,
+ app_secret,
+ {
+ site: 'https://api.linear.app',
+ token_url: '/oauth/token',
+ authorize_url: '/oauth/authorize'
+ }
+ )
+ end
+
+ def handle_response
+ hook = account.hooks.new(
+ access_token: parsed_body['access_token'],
+ status: 'enabled',
+ app_id: 'linear',
+ settings: {
+ token_type: parsed_body['token_type'],
+ expires_in: parsed_body['expires_in'],
+ scope: parsed_body['scope']
+ }
+ )
+ # You may wonder why we're not handling the refresh token update, since the token will expire only after 10 years, https://github.com/linear/linear/issues/251
+ hook.save!
+ redirect_to linear_redirect_uri
+ rescue StandardError => e
+ Rails.logger.error("Linear callback error: #{e.message}")
+ redirect_to linear_redirect_uri
+ end
+
+ def account
+ @account ||= Account.find(account_id)
+ end
+
+ def account_id
+ return unless params[:state]
+
+ verify_linear_token(params[:state])
+ end
+
+ def linear_redirect_uri
+ "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/linear"
+ end
+
+ def parsed_body
+ @parsed_body ||= @response.response.parsed
+ end
+
+ def base_url
+ ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
+ end
+end
diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb
index b8f3bd9a9..43157fa0e 100644
--- a/app/controllers/super_admin/app_configs_controller.rb
+++ b/app/controllers/super_admin/app_configs_controller.rb
@@ -39,6 +39,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
%w[AZURE_APP_ID AZURE_APP_SECRET]
when 'email'
['MAILER_INBOUND_EMAIL_DOMAIN']
+ when 'linear'
+ %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET]
else
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]
end
diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb
index b582bc5dd..7416b7861 100644
--- a/app/dispatchers/async_dispatcher.rb
+++ b/app/dispatchers/async_dispatcher.rb
@@ -22,3 +22,5 @@ class AsyncDispatcher < BaseDispatcher
]
end
end
+
+AsyncDispatcher.prepend_mod_with('AsyncDispatcher')
diff --git a/app/fields/enterprise/account_limits_field.rb b/app/fields/enterprise/account_limits_field.rb
index 5833952ad..b014435c6 100644
--- a/app/fields/enterprise/account_limits_field.rb
+++ b/app/fields/enterprise/account_limits_field.rb
@@ -2,6 +2,6 @@ require 'administrate/field/base'
class Enterprise::AccountLimitsField < Administrate::Field::Base
def to_s
- data.present? ? data.to_json : { agents: nil, inboxes: nil }.to_json
+ data.present? ? data.to_json : { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil }.to_json
end
end
diff --git a/app/helpers/api/v2/accounts/heatmap_helper.rb b/app/helpers/api/v2/accounts/heatmap_helper.rb
index 58dade28d..a4977f43a 100644
--- a/app/helpers/api/v2/accounts/heatmap_helper.rb
+++ b/app/helpers/api/v2/accounts/heatmap_helper.rb
@@ -94,7 +94,8 @@ module Api::V2::Accounts::HeatmapHelper
end
def since_timestamp(date)
- (date - 6.days).to_i.to_s
+ number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
+ (date - number_of_days).to_i.to_s
end
def until_timestamp(date)
diff --git a/app/helpers/api/v2/accounts/reports_helper.rb b/app/helpers/api/v2/accounts/reports_helper.rb
index 4d5a6f115..22c51b6ef 100644
--- a/app/helpers/api/v2/accounts/reports_helper.rb
+++ b/app/helpers/api/v2/accounts/reports_helper.rb
@@ -1,58 +1,70 @@
module Api::V2::Accounts::ReportsHelper
def generate_agents_report
+ reports = V2::Reports::AgentSummaryBuilder.new(
+ account: Current.account,
+ params: build_params(type: :agent)
+ ).build
+
Current.account.users.map do |agent|
- agent_report = report_builder({ type: :agent, id: agent.id }).summary
- [agent.name] + generate_readable_report_metrics(agent_report)
+ report = reports.find { |r| r[:id] == agent.id }
+ [agent.name] + generate_readable_report_metrics(report)
end
end
def generate_inboxes_report
+ reports = V2::Reports::InboxSummaryBuilder.new(
+ account: Current.account,
+ params: build_params(type: :inbox)
+ ).build
+
Current.account.inboxes.map do |inbox|
- inbox_report = generate_report({ type: :inbox, id: inbox.id })
- [inbox.name, inbox.channel&.name] + generate_readable_report_metrics(inbox_report)
+ report = reports.find { |r| r[:id] == inbox.id }
+ [inbox.name, inbox.channel&.name] + generate_readable_report_metrics(report)
end
end
def generate_teams_report
+ reports = V2::Reports::TeamSummaryBuilder.new(
+ account: Current.account,
+ params: build_params(type: :team)
+ ).build
+
Current.account.teams.map do |team|
- team_report = report_builder({ type: :team, id: team.id }).summary
- [team.name] + generate_readable_report_metrics(team_report)
+ report = reports.find { |r| r[:id] == team.id }
+ [team.name] + generate_readable_report_metrics(report)
end
end
def generate_labels_report
Current.account.labels.map do |label|
- label_report = generate_report({ type: :label, id: label.id })
+ label_report = report_builder({ type: :label, id: label.id }).short_summary
[label.title] + generate_readable_report_metrics(label_report)
end
end
- def report_builder(report_params)
- V2::ReportBuilder.new(
- Current.account,
- report_params.merge(
- {
- since: params[:since],
- until: params[:until],
- business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
- }
- )
+ private
+
+ def build_params(base_params)
+ base_params.merge(
+ {
+ since: params[:since],
+ until: params[:until],
+ business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
+ }
)
end
- def generate_report(report_params)
- report_builder(report_params).short_summary
+ def report_builder(report_params)
+ V2::ReportBuilder.new(Current.account, build_params(report_params))
end
- private
-
- def generate_readable_report_metrics(report_metric)
+ def generate_readable_report_metrics(report)
[
- report_metric[:conversations_count],
- Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format,
- Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format,
- Reports::TimeFormatPresenter.new(report_metric[:reply_time]).format,
- report_metric[:resolutions_count]
+ report[:conversations_count],
+ Reports::TimeFormatPresenter.new(report[:avg_first_response_time]).format,
+ Reports::TimeFormatPresenter.new(report[:avg_resolution_time]).format,
+ Reports::TimeFormatPresenter.new(report[:avg_reply_time]).format,
+ report[:resolved_conversations_count]
]
end
end
diff --git a/app/helpers/filter_helper.rb b/app/helpers/filters/filter_helper.rb
similarity index 90%
rename from app/helpers/filter_helper.rb
rename to app/helpers/filters/filter_helper.rb
index 9b5cac684..fe03dae28 100644
--- a/app/helpers/filter_helper.rb
+++ b/app/helpers/filters/filter_helper.rb
@@ -1,4 +1,4 @@
-module FilterHelper
+module Filters::FilterHelper
def build_condition_query(model_filters, query_hash, current_index)
current_filter = model_filters[query_hash['attribute_key']]
@@ -89,4 +89,18 @@ module FilterHelper
operator = condition['query_operator'].upcase
raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) unless %w[AND OR].include?(operator)
end
+
+ def conversation_status_values(values)
+ return Conversation.statuses.values if values.include?('all')
+
+ values.map { |x| Conversation.statuses[x.to_sym] }
+ end
+
+ def conversation_priority_values(values)
+ values.map { |x| Conversation.priorities[x.to_sym] }
+ end
+
+ def message_type_values(values)
+ values.map { |x| Message.message_types[x.to_sym] }
+ end
end
diff --git a/app/helpers/linear/integration_helper.rb b/app/helpers/linear/integration_helper.rb
new file mode 100644
index 000000000..67df836ce
--- /dev/null
+++ b/app/helpers/linear/integration_helper.rb
@@ -0,0 +1,47 @@
+module Linear::IntegrationHelper
+ # Generates a signed JWT token for Linear integration
+ #
+ # @param account_id [Integer] The account ID to encode in the token
+ # @return [String, nil] The encoded JWT token or nil if client secret is missing
+ def generate_linear_token(account_id)
+ return if client_secret.blank?
+
+ JWT.encode(token_payload(account_id), client_secret, 'HS256')
+ rescue StandardError => e
+ Rails.logger.error("Failed to generate Linear token: #{e.message}")
+ nil
+ end
+
+ def token_payload(account_id)
+ {
+ sub: account_id,
+ iat: Time.current.to_i
+ }
+ end
+
+ # Verifies and decodes a Linear JWT token
+ #
+ # @param token [String] The JWT token to verify
+ # @return [Integer, nil] The account ID from the token or nil if invalid
+ def verify_linear_token(token)
+ return if token.blank? || client_secret.blank?
+
+ decode_token(token, client_secret)
+ end
+
+ private
+
+ def client_secret
+ @client_secret ||= GlobalConfigService.load('LINEAR_CLIENT_SECRET', nil)
+ end
+
+ def decode_token(token, secret)
+ JWT.decode(token, secret, true, {
+ algorithm: 'HS256',
+ verify_expiration: true
+ }).first['sub']
+ rescue StandardError => e
+ Rails.logger.error("Unexpected error verifying Linear token: #{e.message}")
+ nil
+ end
+end
diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb
index 342bb62aa..2b1ab7b21 100644
--- a/app/helpers/portal_helper.rb
+++ b/app/helpers/portal_helper.rb
@@ -5,8 +5,7 @@ module PortalHelper
end
def generate_portal_bg(portal_color, theme)
- bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
- "url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
+ generate_portal_bg_color(portal_color, theme)
end
def generate_gradient_to_bottom(theme)
diff --git a/app/helpers/super_admin/account_features_helper.rb b/app/helpers/super_admin/account_features_helper.rb
index 759aec134..c24e65e68 100644
--- a/app/helpers/super_admin/account_features_helper.rb
+++ b/app/helpers/super_admin/account_features_helper.rb
@@ -6,4 +6,47 @@ module SuperAdmin::AccountFeaturesHelper
def self.account_premium_features
account_features.filter { |feature| feature['premium'] }.pluck('name')
end
+
+ # Returns a hash mapping feature names to their display names
+ def self.feature_display_names
+ account_features.each_with_object({}) do |feature, hash|
+ hash[feature['name']] = feature['display_name']
+ end
+ end
+
+ def self.filter_internal_features(features)
+ return features if GlobalConfig.get_value('DEPLOYMENT_ENV') == 'cloud'
+
+ internal_features = account_features.select { |f| f['chatwoot_internal'] }.pluck('name')
+ features.except(*internal_features)
+ end
+
+ def self.filter_deprecated_features(features)
+ deprecated_features = account_features.select { |f| f['deprecated'] }.pluck('name')
+ features.except(*deprecated_features)
+ end
+
+ def self.sort_and_transform_features(features, display_names)
+ features.sort_by { |key, _| display_names[key] || key }
+ .to_h
+ .transform_keys { |key| [key, display_names[key]] }
+ end
+
+ def self.partition_features(features)
+ filtered = filter_internal_features(features)
+ filtered = filter_deprecated_features(filtered)
+ display_names = feature_display_names
+
+ regular, premium = filtered.partition { |key, _value| account_premium_features.exclude?(key) }
+
+ [
+ sort_and_transform_features(regular, display_names),
+ sort_and_transform_features(premium, display_names)
+ ]
+ end
+
+ def self.filtered_features(features)
+ regular, premium = partition_features(features)
+ regular.merge(premium)
+ end
end
diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index c32cb98d2..ae50055fb 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -14,6 +14,7 @@ import WootSnackbarBox from './components/SnackbarContainer.vue';
import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
import { useAccount } from 'dashboard/composables/useAccount';
+import { useFontSize } from 'dashboard/composables/useFontSize';
import {
registerSubscription,
verifyServiceWorkerExistence,
@@ -37,8 +38,15 @@ export default {
const router = useRouter();
const store = useStore();
const { accountId } = useAccount();
+ // Use the font size composable (it automatically sets up the watcher)
+ const { currentFontSize } = useFontSize();
- return { router, store, currentAccountId: accountId };
+ return {
+ router,
+ store,
+ currentAccountId: accountId,
+ currentFontSize,
+ };
},
data() {
return {
diff --git a/app/javascript/dashboard/api/captain/assistant.js b/app/javascript/dashboard/api/captain/assistant.js
new file mode 100644
index 000000000..ce636e526
--- /dev/null
+++ b/app/javascript/dashboard/api/captain/assistant.js
@@ -0,0 +1,19 @@
+/* global axios */
+import ApiClient from '../ApiClient';
+
+class CaptainAssistant extends ApiClient {
+ constructor() {
+ super('captain/assistants', { accountScoped: true });
+ }
+
+ get({ page = 1, searchKey } = {}) {
+ return axios.get(this.url, {
+ params: {
+ page,
+ searchKey,
+ },
+ });
+ }
+}
+
+export default new CaptainAssistant();
diff --git a/app/javascript/dashboard/api/captain/bulkActions.js b/app/javascript/dashboard/api/captain/bulkActions.js
new file mode 100644
index 000000000..fd69a1108
--- /dev/null
+++ b/app/javascript/dashboard/api/captain/bulkActions.js
@@ -0,0 +1,9 @@
+import ApiClient from '../ApiClient';
+
+class CaptainBulkActionsAPI extends ApiClient {
+ constructor() {
+ super('captain/bulk_actions', { accountScoped: true });
+ }
+}
+
+export default new CaptainBulkActionsAPI();
diff --git a/app/javascript/dashboard/api/captain/document.js b/app/javascript/dashboard/api/captain/document.js
new file mode 100644
index 000000000..dc22b0c32
--- /dev/null
+++ b/app/javascript/dashboard/api/captain/document.js
@@ -0,0 +1,20 @@
+/* global axios */
+import ApiClient from '../ApiClient';
+
+class CaptainDocument extends ApiClient {
+ constructor() {
+ super('captain/documents', { accountScoped: true });
+ }
+
+ get({ page = 1, searchKey, assistantId } = {}) {
+ return axios.get(this.url, {
+ params: {
+ page,
+ searchKey,
+ assistant_id: assistantId,
+ },
+ });
+ }
+}
+
+export default new CaptainDocument();
diff --git a/app/javascript/dashboard/api/captain/inboxes.js b/app/javascript/dashboard/api/captain/inboxes.js
new file mode 100644
index 000000000..e0a1efdfe
--- /dev/null
+++ b/app/javascript/dashboard/api/captain/inboxes.js
@@ -0,0 +1,26 @@
+/* global axios */
+import ApiClient from '../ApiClient';
+
+class CaptainInboxes extends ApiClient {
+ constructor() {
+ super('captain/assistants', { accountScoped: true });
+ }
+
+ get({ assistantId } = {}) {
+ return axios.get(`${this.url}/${assistantId}/inboxes`);
+ }
+
+ create(params = {}) {
+ const { assistantId, inboxId } = params;
+ return axios.post(`${this.url}/${assistantId}/inboxes`, {
+ inbox: { inbox_id: inboxId },
+ });
+ }
+
+ delete(params = {}) {
+ const { assistantId, inboxId } = params;
+ return axios.delete(`${this.url}/${assistantId}/inboxes/${inboxId}`);
+ }
+}
+
+export default new CaptainInboxes();
diff --git a/app/javascript/dashboard/api/captain/response.js b/app/javascript/dashboard/api/captain/response.js
new file mode 100644
index 000000000..e3c42757a
--- /dev/null
+++ b/app/javascript/dashboard/api/captain/response.js
@@ -0,0 +1,22 @@
+/* global axios */
+import ApiClient from '../ApiClient';
+
+class CaptainResponses extends ApiClient {
+ constructor() {
+ super('captain/assistant_responses', { accountScoped: true });
+ }
+
+ get({ page = 1, searchKey, assistantId, documentId, status } = {}) {
+ return axios.get(this.url, {
+ params: {
+ page,
+ searchKey,
+ assistant_id: assistantId,
+ document_id: documentId,
+ status,
+ },
+ });
+ }
+}
+
+export default new CaptainResponses();
diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js
index ebc6176e7..39546096f 100644
--- a/app/javascript/dashboard/api/inbox/conversation.js
+++ b/app/javascript/dashboard/api/inbox/conversation.js
@@ -133,6 +133,14 @@ class ConversationApi extends ApiClient {
getAllAttachments(conversationId) {
return axios.get(`${this.url}/${conversationId}/attachments`);
}
+
+ requestCopilot(conversationId, body) {
+ return axios.post(`${this.url}/${conversationId}/copilot`, body);
+ }
+
+ getInboxAssistant(conversationId) {
+ return axios.get(`${this.url}/${conversationId}/inbox_assistant`);
+ }
}
export default new ConversationApi();
diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js
index b78a2ea7e..2b816e603 100644
--- a/app/javascript/dashboard/api/integrations.js
+++ b/app/javascript/dashboard/api/integrations.js
@@ -32,14 +32,6 @@ class IntegrationsAPI extends ApiClient {
deleteHook(hookId) {
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
}
-
- requestCaptain(body) {
- return axios.post(`${this.baseUrl()}/integrations/captain/proxy`, body);
- }
-
- requestCaptainCopilot(body) {
- return axios.post(`${this.baseUrl()}/integrations/captain/copilot`, body);
- }
}
export default new IntegrationsAPI();
diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js
index 52fa7f444..c87dfc82b 100644
--- a/app/javascript/dashboard/api/reports.js
+++ b/app/javascript/dashboard/api/reports.js
@@ -61,9 +61,9 @@ class ReportsAPI extends ApiClient {
});
}
- getConversationTrafficCSV() {
+ getConversationTrafficCSV({ daysBefore = 6 } = {}) {
return axios.get(`${this.url}/conversation_traffic`, {
- params: { timezone_offset: getTimeOffset() },
+ params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
});
}
diff --git a/app/javascript/dashboard/api/search.js b/app/javascript/dashboard/api/search.js
index 7dc98dcf2..7abb584c0 100644
--- a/app/javascript/dashboard/api/search.js
+++ b/app/javascript/dashboard/api/search.js
@@ -14,26 +14,29 @@ class SearchAPI extends ApiClient {
});
}
- contacts({ q }) {
+ contacts({ q, page = 1 }) {
return axios.get(`${this.url}/contacts`, {
params: {
q,
+ page: page,
},
});
}
- conversations({ q }) {
+ conversations({ q, page = 1 }) {
return axios.get(`${this.url}/conversations`, {
params: {
q,
+ page: page,
},
});
}
- messages({ q }) {
+ messages({ q, page = 1 }) {
return axios.get(`${this.url}/messages`, {
params: {
q,
+ page: page,
},
});
}
diff --git a/app/javascript/dashboard/api/summaryReports.js b/app/javascript/dashboard/api/summaryReports.js
new file mode 100644
index 000000000..f772ef86f
--- /dev/null
+++ b/app/javascript/dashboard/api/summaryReports.js
@@ -0,0 +1,40 @@
+/* global axios */
+import ApiClient from './ApiClient';
+
+class SummaryReportsAPI extends ApiClient {
+ constructor() {
+ super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
+ }
+
+ getTeamReports({ since, until, businessHours } = {}) {
+ return axios.get(`${this.url}/team`, {
+ params: {
+ since,
+ until,
+ business_hours: businessHours,
+ },
+ });
+ }
+
+ getAgentReports({ since, until, businessHours } = {}) {
+ return axios.get(`${this.url}/agent`, {
+ params: {
+ since,
+ until,
+ business_hours: businessHours,
+ },
+ });
+ }
+
+ getInboxReports({ since, until, businessHours } = {}) {
+ return axios.get(`${this.url}/inbox`, {
+ params: {
+ since,
+ until,
+ business_hours: businessHours,
+ },
+ });
+ }
+}
+
+export default new SummaryReportsAPI();
diff --git a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss
index e08099ae6..9ab51d12a 100644
--- a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss
+++ b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss
@@ -1,7 +1,7 @@
.dropdown-pane {
- @apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999];
+ @apply border rounded-lg hidden relative invisible shadow-lg border-n-strong dark:border-n-strong box-content p-2 w-fit z-[9999];
&.dropdown-pane--open {
- @apply bg-white absolute dark:bg-slate-800 block visible;
+ @apply bg-n-alpha-3 backdrop-blur-[100px] absolute block visible;
}
}
diff --git a/app/javascript/dashboard/assets/scss/super_admin/index.scss b/app/javascript/dashboard/assets/scss/super_admin/index.scss
index 91f7835d0..ddac5f7c8 100644
--- a/app/javascript/dashboard/assets/scss/super_admin/index.scss
+++ b/app/javascript/dashboard/assets/scss/super_admin/index.scss
@@ -6,3 +6,209 @@
body {
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
}
+
+@layer base {
+ // FIXME: Use a common color file for all packs
+ // scss-lint:disable PropertySortOrder
+ :root {
+ --slate-1: 252 252 253;
+ --slate-2: 249 249 251;
+ --slate-3: 240 240 243;
+ --slate-4: 232 232 236;
+ --slate-5: 224 225 230;
+ --slate-6: 217 217 224;
+ --slate-7: 205 206 214;
+ --slate-8: 185 187 198;
+ --slate-9: 139 141 152;
+ --slate-10: 128 131 141;
+ --slate-11: 96 100 108;
+ --slate-12: 28 32 36;
+
+ --iris-1: 253 253 255;
+ --iris-2: 248 248 255;
+ --iris-3: 240 241 254;
+ --iris-4: 230 231 255;
+ --iris-5: 218 220 255;
+ --iris-6: 203 205 255;
+ --iris-7: 184 186 248;
+ --iris-8: 155 158 240;
+ --iris-9: 91 91 214;
+ --iris-10: 81 81 205;
+ --iris-11: 87 83 198;
+ --iris-12: 39 41 98;
+
+ --ruby-1: 255 252 253;
+ --ruby-2: 255 247 248;
+ --ruby-3: 254 234 237;
+ --ruby-4: 255 220 225;
+ --ruby-5: 255 206 214;
+ --ruby-6: 248 191 200;
+ --ruby-7: 239 172 184;
+ --ruby-8: 229 146 163;
+ --ruby-9: 229 70 102;
+ --ruby-10: 220 59 93;
+ --ruby-11: 202 36 77;
+ --ruby-12: 100 23 43;
+
+ --amber-1: 254 253 251;
+ --amber-2: 254 251 233;
+ --amber-3: 255 247 194;
+ --amber-4: 255 238 156;
+ --amber-5: 251 229 119;
+ --amber-6: 243 214 115;
+ --amber-7: 233 193 98;
+ --amber-8: 226 163 54;
+ --amber-9: 255 197 61;
+ --amber-10: 255 186 24;
+ --amber-11: 171 100 0;
+ --amber-12: 79 52 34;
+
+ --teal-1: 250 254 253;
+ --teal-2: 243 251 249;
+ --teal-3: 224 248 243;
+ --teal-4: 204 243 234;
+ --teal-5: 184 234 224;
+ --teal-6: 161 222 210;
+ --teal-7: 131 205 193;
+ --teal-8: 83 185 171;
+ --teal-9: 18 165 148;
+ --teal-10: 13 155 138;
+ --teal-11: 0 133 115;
+ --teal-12: 13 61 56;
+
+ --gray-1: 252 252 252;
+ --gray-2: 249 249 249;
+ --gray-3: 240 240 240;
+ --gray-4: 232 232 232;
+ --gray-5: 224 224 224;
+ --gray-6: 217 217 217;
+ --gray-7: 206 206 206;
+ --gray-8: 187 187 187;
+ --gray-9: 141 141 141;
+ --gray-10: 131 131 131;
+ --gray-11: 100 100 100;
+ --gray-12: 32 32 32;
+
+ --background-color: 253 253 253;
+ --text-blue: 8 109 224;
+ --border-container: 236 236 236;
+ --border-strong: 235 235 235;
+ --border-weak: 234 234 234;
+ --solid-1: 255 255 255;
+ --solid-2: 255 255 255;
+ --solid-3: 255 255 255;
+ --solid-active: 255 255 255;
+ --solid-amber: 252 232 193;
+ --solid-blue: 218 236 255;
+ --solid-iris: 230 231 255;
+
+ --alpha-1: 67, 67, 67, 0.06;
+ --alpha-2: 201, 202, 207, 0.15;
+ --alpha-3: 255, 255, 255, 0.96;
+ --black-alpha-1: 0, 0, 0, 0.12;
+ --black-alpha-2: 0, 0, 0, 0.04;
+ --border-blue: 39, 129, 246, 0.5;
+ --white-alpha: 255, 255, 255, 0.8;
+ }
+
+ .dark {
+ --slate-1: 17 17 19;
+ --slate-2: 24 25 27;
+ --slate-3: 33 34 37;
+ --slate-4: 39 42 45;
+ --slate-5: 46 49 53;
+ --slate-6: 54 58 63;
+ --slate-7: 67 72 78;
+ --slate-8: 90 97 105;
+ --slate-9: 105 110 119;
+ --slate-10: 119 123 132;
+ --slate-11: 176 180 186;
+ --slate-12: 237 238 240;
+
+ --iris-1: 19 19 30;
+ --iris-2: 23 22 37;
+ --iris-3: 32 34 72;
+ --iris-4: 38 42 101;
+ --iris-5: 48 51 116;
+ --iris-6: 61 62 130;
+ --iris-7: 74 74 149;
+ --iris-8: 89 88 177;
+ --iris-9: 91 91 214;
+ --iris-10: 84 114 228;
+ --iris-11: 158 177 255;
+ --iris-12: 224 223 254;
+
+ --ruby-1: 25 17 19;
+ --ruby-2: 30 21 23;
+ --ruby-3: 58 20 30;
+ --ruby-4: 78 19 37;
+ --ruby-5: 94 26 46;
+ --ruby-6: 111 37 57;
+ --ruby-7: 136 52 71;
+ --ruby-8: 179 68 90;
+ --ruby-9: 229 70 102;
+ --ruby-10: 236 90 114;
+ --ruby-11: 255 148 157;
+ --ruby-12: 254 210 225;
+
+ --amber-1: 22 18 12;
+ --amber-2: 29 24 15;
+ --amber-3: 48 32 8;
+ --amber-4: 63 39 0;
+ --amber-5: 77 48 0;
+ --amber-6: 92 61 5;
+ --amber-7: 113 79 25;
+ --amber-8: 143 100 36;
+ --amber-9: 255 197 61;
+ --amber-10: 255 214 10;
+ --amber-11: 255 202 22;
+ --amber-12: 255 231 179;
+
+ --teal-1: 13 21 20;
+ --teal-2: 17 28 27;
+ --teal-3: 13 45 42;
+ --teal-4: 2 59 55;
+ --teal-5: 8 72 67;
+ --teal-6: 20 87 80;
+ --teal-7: 28 105 97;
+ --teal-8: 32 126 115;
+ --teal-9: 18 165 148;
+ --teal-10: 14 179 158;
+ --teal-11: 11 216 182;
+ --teal-12: 173 240 221;
+
+ --gray-1: 17 17 17;
+ --gray-2: 25 25 25;
+ --gray-3: 34 34 34;
+ --gray-4: 42 42 42;
+ --gray-5: 49 49 49;
+ --gray-6: 58 58 58;
+ --gray-7: 72 72 72;
+ --gray-8: 96 96 96;
+ --gray-9: 110 110 110;
+ --gray-10: 123 123 123;
+ --gray-11: 180 180 180;
+ --gray-12: 238 238 238;
+
+ --background-color: 18 18 19;
+ --border-strong: 52 52 52;
+ --border-weak: 38 38 42;
+ --solid-1: 23 23 26;
+ --solid-2: 29 30 36;
+ --solid-3: 44 45 54;
+ --solid-active: 53 57 66;
+ --solid-amber: 42 37 30;
+ --solid-blue: 16 49 91;
+ --solid-iris: 38 42 101;
+ --text-blue: 126 182 255;
+
+ --alpha-1: 36, 36, 36, 0.8;
+ --alpha-2: 139, 147, 182, 0.15;
+ --alpha-3: 36, 38, 45, 0.9;
+ --black-alpha-1: 0, 0, 0, 0.3;
+ --black-alpha-2: 0, 0, 0, 0.2;
+ --border-blue: 39, 129, 246, 0.5;
+ --border-container: 236, 236, 236, 0;
+ --white-alpha: 255, 255, 255, 0.1;
+ }
+}
diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss
index d35c1cfc9..9367e8b2d 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_base.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss
@@ -82,7 +82,7 @@ input[type='url']:not(.reset-base) {
}
input[type='file'] {
- @apply bg-white dark:bg-slate-800 leading-[1.15] mb-4;
+ @apply bg-white dark:bg-n-solid-1 leading-[1.15] mb-4;
}
// Select
@@ -141,11 +141,16 @@ code {
@apply text-xs border-0;
&.hljs {
- @apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50 rounded-lg p-5;
+ @apply bg-n-slate-3 dark:bg-n-solid-3 text-slate-800 dark:text-slate-50 rounded-lg p-5;
.hljs-number,
.hljs-string {
@apply text-red-800 dark:text-red-400;
}
+
+ .hljs-name,
+ .hljs-tag {
+ @apply text-n-slate-11;
+ }
}
}
diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss
index 640e1395f..1213e7c26 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss
@@ -31,7 +31,7 @@ button {
}
.button {
- @apply items-center bg-woot-500 dark:bg-woot-500 px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium;
+ @apply items-center bg-n-brand px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium;
.button__content {
@apply w-full whitespace-nowrap overflow-hidden text-ellipsis;
@@ -42,8 +42,10 @@ button {
}
}
- &:hover {
- @apply bg-woot-600 dark:bg-woot-600;
+ &:hover:not(.secondary):not(.success):not(.alert):not(.warning):not(
+ .clear
+ ):not(.smooth):not(.hollow) {
+ @apply bg-n-brand/80 dark:bg-n-brand/80;
}
&:disabled,
@@ -52,23 +54,23 @@ button {
}
&.success {
- @apply bg-[#44ce4b] dark:bg-[#44ce4b] text-white dark:text-white;
+ @apply bg-n-teal-9 text-white dark:text-white;
}
&.secondary {
- @apply bg-slate-700 dark:bg-slate-600 text-white dark:text-white;
+ @apply bg-n-solid-3 text-white dark:text-white;
}
&.primary {
- @apply bg-woot-500 dark:bg-woot-500 text-white dark:text-white;
+ @apply bg-n-brand text-white dark:text-white;
}
&.clear {
- @apply text-woot-500 dark:text-woot-500 bg-transparent dark:bg-transparent;
+ @apply text-n-blue-text dark:text-n-blue-text bg-transparent dark:bg-transparent;
}
&.alert {
- @apply bg-red-500 dark:bg-red-500 text-white dark:text-white;
+ @apply bg-n-ruby-9 text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
@@ -76,7 +78,7 @@ button {
}
&.warning {
- @apply bg-[#ffc532] dark:bg-[#ffc532] text-white dark:text-white;
+ @apply bg-n-amber-9 text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
@@ -115,114 +117,74 @@ button {
}
&.hollow {
- @apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
+ @apply border border-n-brand/40 bg-transparent text-n-blue-text hover:bg-n-brand/20;
&.secondary {
- @apply text-slate-700 border-slate-100 dark:border-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
+ @apply text-n-slate-12 border-n-slate-5 hover:bg-n-slate-5;
}
&.success {
- @apply text-green-700 dark:text-green-400 border-green-100 dark:border-green-600 hover:bg-green-50 dark:hover:bg-green-800;
+ @apply text-n-teal-9 border-n-teal-8 hover:bg-n-teal-5;
}
&.alert {
- @apply text-red-700 dark:text-red-400 border-red-100 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-800;
+ @apply text-n-ruby-9 border-n-ruby-8 hover:bg-n-ruby-5;
}
&.warning {
- @apply text-yellow-600 dark:text-yellow-600 border-yellow-600 dark:border-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-800;
- }
-
- &:hover {
- @apply bg-woot-75 dark:bg-woot-800 border-slate-100 dark:border-woot-600 dark:text-woot-400;
-
- &.secondary {
- @apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
- }
-
- &.success {
- @apply border-slate-100 dark:border-slate-700 text-green-800 dark:text-green-100;
- }
-
- &.alert {
- @apply border-slate-100 dark:border-slate-700 text-red-700 dark:text-red-100;
- }
-
- &.warning {
- @apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-500;
- }
+ @apply text-n-amber-9 border-n-amber-8 hover:bg-n-amber-5;
}
}
// Smooth style
&.smooth {
- @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;
+ @apply bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-text hover:bg-n-brand/20 dark:hover:bg-n-brand/40;
&.secondary {
- @apply bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800;
+ @apply bg-n-slate-4 text-n-slate-11 hover:text-n-slate-11 hover:bg-n-slate-5;
}
&.success {
- @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;
+ @apply bg-n-teal-4 text-n-teal-11 hover:text-n-teal-11 hover:bg-n-teal-5;
}
&.alert {
- @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;
+ @apply bg-n-ruby-4 text-n-ruby-11 hover:text-n-ruby-11 hover:bg-n-ruby-5;
}
&.warning {
- @apply bg-yellow-100 dark:bg-yellow-100 text-yellow-700 dark:text-yellow-700 hover:bg-yellow-200 dark:hover:bg-yellow-200;
+ @apply bg-n-amber-4 text-n-amber-11 hover:text-n-amber-11 hover:bg-n-amber-5;
}
}
&.clear {
- @apply text-woot-500 dark:text-woot-500;
+ @apply text-n-blue-text hover:bg-n-brand/10 dark:hover:bg-n-brand/30;
&.secondary {
- @apply text-slate-700 dark:text-slate-100;
+ @apply text-n-slate-12 hover:bg-n-slate-4;
}
&.success {
- @apply text-green-700 dark:text-green-100;
+ @apply text-n-teal-10 hover:bg-n-teal-4;
}
&.alert {
- @apply text-red-700 dark:text-red-100;
+ @apply text-n-ruby-11 hover:bg-n-ruby-4;
}
&.warning {
- @apply text-yellow-700 dark:text-yellow-600;
- }
-
- &:hover {
- @apply hover:bg-woot-50 dark:hover:bg-woot-900/50 hover:text-woot-500 dark:hover:text-woot-100;
-
- &.secondary {
- @apply hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-800 dark:hover:text-slate-100;
- }
-
- &.success {
- @apply hover:bg-green-50 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100;
- }
-
- &.alert {
- @apply hover:bg-red-50 dark:hover:bg-red-800 hover:text-red-700 dark:hover:text-red-100;
- }
-
- &.warning {
- @apply hover:bg-yellow-100 dark:hover:bg-yellow-800 hover:text-yellow-700 dark:hover:text-yellow-600;
- }
+ @apply text-n-amber-11 hover:bg-n-amber-4;
}
&:active {
&.secondary {
- @apply active:bg-slate-100 dark:active:bg-slate-900;
+ @apply active:bg-n-slate-3 dark:active:bg-n-slate-7;
}
}
&:focus {
&.secondary {
- @apply focus:bg-slate-50 dark:focus:bg-slate-700;
+ @apply focus:bg-n-slate-4 dark:focus:bg-n-slate-6;
}
}
}
diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss
index 01ccaf671..72a2e6be8 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss
@@ -3,7 +3,7 @@
}
.tabs--container--with-border {
- @apply border-b border-slate-50 dark:border-slate-800/50;
+ @apply border-b border-n-weak;
}
.tabs--container--compact.tab--chat-type {
@@ -42,7 +42,7 @@
@apply flex-shrink-0 my-0 mx-2;
.badge {
- @apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
+ @apply bg-n-alpha-black2 dark:bg-n-solid-3 rounded-md text-n-slate-11 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
}
&:first-child {
@@ -56,22 +56,22 @@
&:hover,
&:focus {
a {
- @apply text-slate-800 dark:text-slate-100;
+ @apply text-n-slate-12;
}
}
a {
- @apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
+ @apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-n-slate-11 text-sm top-[1px] relative;
transition: border-color 0.15s $swift-ease-out-function;
}
&.is-active {
a {
- @apply border-b border-woot-500 text-woot-500 dark:text-woot-500;
+ @apply border-b border-n-brand text-n-blue-text;
}
.badge {
- @apply bg-woot-50 dark:bg-woot-500 text-woot-500 dark:text-woot-50 dark:bg-opacity-40;
+ @apply bg-n-brand/10 dark:bg-n-brand/20 text-n-blue-text;
}
}
}
diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
index 229553253..304e55347 100644
--- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
@@ -98,7 +98,7 @@ const inboxIcon = computed(() => {