diff --git a/.circleci/config.yml b/.circleci/config.yml
index db7f87d5c..bc7053130 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- checkout
- node/install:
- node-version: '20.12'
+ node-version: '23.7'
- node/install-pnpm
- node/install-packages:
pkg-manager: pnpm
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 8b0704bfa..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
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/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 0af172849..5a9d35d0b 100644
--- a/.github/workflows/run_foss_spec.yml
+++ b/.github/workflows/run_foss_spec.yml
@@ -38,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 }}
@@ -48,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/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/.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 45e194e13..e8639a5bd 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.1'
# 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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 45c54bd46..abb007178 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)
@@ -280,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)
@@ -361,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)
@@ -487,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)
@@ -503,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)
@@ -543,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)
@@ -565,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)
@@ -751,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.1)
faraday (>= 2.0)
faraday-mashify
faraday-multipart
gli
hashie
+ logger
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
@@ -782,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)
@@ -897,7 +894,7 @@ DEPENDENCIES
haikunator
hairtrigger
hashie
- html2text!
+ html2text
image_processing
jbuilder
json_refs
@@ -957,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.1)
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/VERSION_CWCTL b/VERSION_CWCTL
index fd2a01863..944880fa1 100644
--- a/VERSION_CWCTL
+++ b/VERSION_CWCTL
@@ -1 +1 @@
-3.1.0
+3.2.0
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/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/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/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/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/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/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/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(() => {