From d9450fde4a0a5946d8a5db2bc373137427fc7dc1 Mon Sep 17 00:00:00 2001
From: Muhsin Keloth
Date: Wed, 26 Mar 2025 11:12:32 +0530
Subject: [PATCH 001/554] feat: Added Instagram channel migration (#11181)
This PR is part of https://github.com/chatwoot/chatwoot/pull/11054 to
make the review cycle easier.
---
app/models/account.rb | 1 +
app/models/channel/instagram.rb | 28 +++++++++++++++++++
.../20250326034635_add_instagram_channel.rb | 13 +++++++++
db/schema.rb | 12 +++++++-
spec/factories/channel/channel_instagram.rb | 13 +++++++++
spec/models/channel/instagram_spec.rb | 17 +++++++++++
6 files changed, 83 insertions(+), 1 deletion(-)
create mode 100644 app/models/channel/instagram.rb
create mode 100644 db/migrate/20250326034635_add_instagram_channel.rb
create mode 100644 spec/factories/channel/channel_instagram.rb
create mode 100644 spec/models/channel/instagram_spec.rb
diff --git a/app/models/account.rb b/app/models/account.rb
index e6216873c..eb95194c5 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -56,6 +56,7 @@ class Account < ApplicationRecord
has_many :data_imports, dependent: :destroy_async
has_many :email_channels, dependent: :destroy_async, class_name: '::Channel::Email'
has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage'
+ has_many :instagram_channels, dependent: :destroy_async, class_name: '::Channel::Instagram'
has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook'
has_many :inboxes, dependent: :destroy_async
has_many :labels, dependent: :destroy_async
diff --git a/app/models/channel/instagram.rb b/app/models/channel/instagram.rb
new file mode 100644
index 000000000..fcfcb852e
--- /dev/null
+++ b/app/models/channel/instagram.rb
@@ -0,0 +1,28 @@
+# == Schema Information
+#
+# Table name: channel_instagram
+#
+# id :bigint not null, primary key
+# access_token :string not null
+# expires_at :datetime not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :integer not null
+# instagram_id :string not null
+#
+# Indexes
+#
+# index_channel_instagram_on_instagram_id (instagram_id) UNIQUE
+#
+class Channel::Instagram < ApplicationRecord
+ include Channelable
+
+ self.table_name = 'channel_instagram'
+
+ validates :access_token, presence: true
+ validates :instagram_id, uniqueness: true, presence: true
+
+ def name
+ 'Instagram'
+ end
+end
diff --git a/db/migrate/20250326034635_add_instagram_channel.rb b/db/migrate/20250326034635_add_instagram_channel.rb
new file mode 100644
index 000000000..6882caf6a
--- /dev/null
+++ b/db/migrate/20250326034635_add_instagram_channel.rb
@@ -0,0 +1,13 @@
+class AddInstagramChannel < ActiveRecord::Migration[7.0]
+ def change
+ create_table :channel_instagram do |t|
+ t.string :access_token, null: false
+ t.datetime :expires_at, null: false
+ t.integer :account_id, null: false
+ t.string :instagram_id, null: false
+ t.timestamps
+ end
+
+ add_index :channel_instagram, :instagram_id, unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0818d1117..1f7217cd8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2025_03_15_202035) do
+ActiveRecord::Schema[7.0].define(version: 2025_03_26_034635) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -377,6 +377,16 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_15_202035) do
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
end
+ create_table "channel_instagram", force: :cascade do |t|
+ t.string "access_token", null: false
+ t.datetime "expires_at", null: false
+ t.integer "account_id", null: false
+ t.string "instagram_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["instagram_id"], name: "index_channel_instagram_on_instagram_id", unique: true
+ end
+
create_table "channel_line", force: :cascade do |t|
t.integer "account_id", null: false
t.string "line_channel_id", null: false
diff --git a/spec/factories/channel/channel_instagram.rb b/spec/factories/channel/channel_instagram.rb
new file mode 100644
index 000000000..9a0d33bb5
--- /dev/null
+++ b/spec/factories/channel/channel_instagram.rb
@@ -0,0 +1,13 @@
+FactoryBot.define do
+ factory :channel_instagram, class: 'Channel::Instagram' do
+ account
+ access_token { SecureRandom.hex(32) }
+ instagram_id { SecureRandom.hex(16) }
+ expires_at { 60.days.from_now }
+ updated_at { 25.hours.ago }
+
+ after(:create) do |channel|
+ create(:inbox, channel: channel, account: channel.account)
+ end
+ end
+end
diff --git a/spec/models/channel/instagram_spec.rb b/spec/models/channel/instagram_spec.rb
new file mode 100644
index 000000000..901fe392e
--- /dev/null
+++ b/spec/models/channel/instagram_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Channel::Instagram do
+ let(:channel) { create(:channel_instagram) }
+
+ it { is_expected.to validate_presence_of(:account_id) }
+ it { is_expected.to validate_presence_of(:access_token) }
+ it { is_expected.to validate_presence_of(:instagram_id) }
+ it { is_expected.to belong_to(:account) }
+ it { is_expected.to have_one(:inbox).dependent(:destroy_async) }
+
+ it 'has a valid name' do
+ expect(channel.name).to eq('Instagram')
+ end
+end
From 49ee147fe3215b165f42d84135f53e8592424e8f Mon Sep 17 00:00:00 2001
From: Pranav
Date: Wed, 26 Mar 2025 11:11:01 -0700
Subject: [PATCH 002/554] fix: Remove where query if admin (#11183)
When finding conversation if it is an admin, we don't need to filter it
by inbox ids.
---
app/finders/conversation_finder.rb | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb
index 44592c201..0ef00cba6 100644
--- a/app/finders/conversation_finder.rb
+++ b/app/finders/conversation_finder.rb
@@ -32,6 +32,7 @@ class ConversationFinder
def initialize(current_user, params)
@current_user = current_user
@current_account = current_user.account
+ @is_admin = current_account.account_users.find_by(user_id: current_user.id)&.administrator?
@params = params
end
@@ -86,7 +87,8 @@ class ConversationFinder
end
def find_all_conversations
- @conversations = current_account.conversations.where(inbox_id: @inbox_ids)
+ @conversations = current_account.conversations
+ @conversations = @conversations.where(inbox_id: @inbox_ids) unless @is_admin
filter_by_conversation_type if params[:conversation_type]
@conversations
end
From 4b4d9f8f7ceb0cdf66c8058397c07b7e457f037f Mon Sep 17 00:00:00 2001
From: Pranav
Date: Wed, 26 Mar 2025 14:59:39 -0700
Subject: [PATCH 003/554] fix: Update throttle for /meta endpoints, will call
every 2s for small account, 10s for large accounts (#11190)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This update improves the throttling mechanism for conversation meta
requests to optimize server load and enhance performance. The changes
implement differentiated thresholds based on account size - a 2-second
throttle for small accounts (≤100 conversations) and a 10-second
throttle for large accounts (>100 conversations).
Fixes #11178
---
.../helper/ConversationMetaThrottleManager.js | 29 +++++++++++++++
.../ConversationMetaThrottleManager.spec.js | 34 +++++++++++++++++
.../store/modules/conversationStats.js | 26 +++++++++----
.../specs/conversationStats/helper.spec.js | 37 +++++++++++++++++++
app/javascript/widget/assets/scss/woot.scss | 2 +-
5 files changed, 120 insertions(+), 8 deletions(-)
create mode 100644 app/javascript/dashboard/helper/ConversationMetaThrottleManager.js
create mode 100644 app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js
create mode 100644 app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js
diff --git a/app/javascript/dashboard/helper/ConversationMetaThrottleManager.js b/app/javascript/dashboard/helper/ConversationMetaThrottleManager.js
new file mode 100644
index 000000000..4f7c7a610
--- /dev/null
+++ b/app/javascript/dashboard/helper/ConversationMetaThrottleManager.js
@@ -0,0 +1,29 @@
+class ConversationMetaThrottleManager {
+ constructor() {
+ this.lastUpdatedTime = null;
+ }
+
+ shouldThrottle(threshold = 10000) {
+ if (!this.lastUpdatedTime) {
+ return false;
+ }
+
+ const currentTime = new Date().getTime();
+ const lastUpdatedTime = new Date(this.lastUpdatedTime).getTime();
+
+ if (currentTime - lastUpdatedTime < threshold) {
+ return true;
+ }
+ return false;
+ }
+
+ markUpdate() {
+ this.lastUpdatedTime = new Date();
+ }
+
+ reset() {
+ this.lastUpdatedTime = null;
+ }
+}
+
+export default new ConversationMetaThrottleManager();
diff --git a/app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js b/app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js
new file mode 100644
index 000000000..b0c67ffaa
--- /dev/null
+++ b/app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js
@@ -0,0 +1,34 @@
+import ConversationMetaThrottleManager from '../ConversationMetaThrottleManager';
+
+describe('ConversationMetaThrottleManager', () => {
+ beforeEach(() => {
+ // Reset the lastUpdatedTime before each test
+ ConversationMetaThrottleManager.lastUpdatedTime = null;
+ });
+
+ describe('shouldThrottle', () => {
+ it('returns false when lastUpdatedTime is not set', () => {
+ expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(false);
+ });
+
+ it('returns true when time difference is less than threshold', () => {
+ ConversationMetaThrottleManager.markUpdate();
+ expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(true);
+ });
+
+ it('returns false when time difference is more than threshold', () => {
+ ConversationMetaThrottleManager.lastUpdatedTime = new Date(
+ Date.now() - 11000
+ );
+ expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(false);
+ });
+
+ it('respects custom threshold value', () => {
+ ConversationMetaThrottleManager.lastUpdatedTime = new Date(
+ Date.now() - 5000
+ );
+ expect(ConversationMetaThrottleManager.shouldThrottle(3000)).toBe(false);
+ expect(ConversationMetaThrottleManager.shouldThrottle(6000)).toBe(true);
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/conversationStats.js b/app/javascript/dashboard/store/modules/conversationStats.js
index 35fc1c6ab..8de20370b 100644
--- a/app/javascript/dashboard/store/modules/conversationStats.js
+++ b/app/javascript/dashboard/store/modules/conversationStats.js
@@ -1,28 +1,40 @@
import types from '../mutation-types';
import ConversationApi from '../../api/inbox/conversation';
+import ConversationMetaThrottleManager from 'dashboard/helper/ConversationMetaThrottleManager';
+
const state = {
mineCount: 0,
unAssignedCount: 0,
allCount: 0,
- updatedOn: null,
};
export const getters = {
getStats: $state => $state,
};
+export const shouldThrottle = conversationCount => {
+ // The threshold for throttling is different for normal users and large accounts
+ // Normal users: 2 seconds
+ // Large accounts: 10 seconds
+ // We would only update the conversation stats based on the threshold above.
+ // This is done to reduce the number of /meta request made to the server.
+ const NORMAL_USER_THRESHOLD = 2000;
+ const LARGE_ACCOUNT_THRESHOLD = 10000;
+
+ const threshold =
+ conversationCount > 100 ? LARGE_ACCOUNT_THRESHOLD : NORMAL_USER_THRESHOLD;
+ return ConversationMetaThrottleManager.shouldThrottle(threshold);
+};
+
export const actions = {
get: async ({ commit, state: $state }, params) => {
- const currentTime = new Date();
- const lastUpdatedTime = new Date($state.updatedOn);
-
- // Skip large accounts from making too many requests
- if (currentTime - lastUpdatedTime < 10000 && $state.allCount > 100) {
+ if (shouldThrottle($state.allCount)) {
// eslint-disable-next-line no-console
- console.warn('Skipping conversation meta fetch');
+ console.warn('Throttle /meta fetch, will resume after threshold');
return;
}
+ ConversationMetaThrottleManager.markUpdate();
try {
const response = await ConversationApi.meta(params);
diff --git a/app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js b/app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js
new file mode 100644
index 000000000..eb07fd1c8
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js
@@ -0,0 +1,37 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import ConversationMetaThrottleManager from 'dashboard/helper/ConversationMetaThrottleManager';
+import { shouldThrottle } from '../../conversationStats';
+
+vi.mock('dashboard/helper/ConversationMetaThrottleManager', () => ({
+ default: {
+ shouldThrottle: vi.fn(),
+ },
+}));
+
+describe('shouldThrottle', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('uses normal threshold for accounts with 100 or fewer conversations', () => {
+ shouldThrottle(100);
+ expect(ConversationMetaThrottleManager.shouldThrottle).toHaveBeenCalledWith(
+ 2000
+ );
+ });
+
+ it('uses large account threshold for accounts with more than 100 conversations', () => {
+ shouldThrottle(101);
+ expect(ConversationMetaThrottleManager.shouldThrottle).toHaveBeenCalledWith(
+ 10000
+ );
+ });
+
+ it('returns the throttle value from ConversationMetaThrottleManager', () => {
+ ConversationMetaThrottleManager.shouldThrottle.mockReturnValue(true);
+ expect(shouldThrottle(50)).toBe(true);
+
+ ConversationMetaThrottleManager.shouldThrottle.mockReturnValue(false);
+ expect(shouldThrottle(150)).toBe(false);
+ });
+});
diff --git a/app/javascript/widget/assets/scss/woot.scss b/app/javascript/widget/assets/scss/woot.scss
index 61f432ab5..3a8496d70 100755
--- a/app/javascript/widget/assets/scss/woot.scss
+++ b/app/javascript/widget/assets/scss/woot.scss
@@ -10,7 +10,7 @@
html,
body {
- @apply antialiased h-full bg-n-background;
+ @apply antialiased h-full;
}
.is-mobile {
From 5951c4b985c56b34e5c6798fa2c3898eb2035b89 Mon Sep 17 00:00:00 2001
From: Pranav
Date: Thu, 27 Mar 2025 17:05:48 -0700
Subject: [PATCH 004/554] fix: Apply filter for inbox when the user is an admin
(#11197)
Optimization #11183 missed a condition where the inbox_id filter is
manually passed. Due to the previous change, the inbox filter was being
discarded for admins, although it continued to work correctly for
agents.
This PR includes a fix for that specific case and adds a spec to
explicitly test it.
---
app/finders/conversation_finder.rb | 8 ++++++--
spec/finders/conversation_finder_spec.rb | 10 ++++++++++
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb
index 0ef00cba6..31f98e384 100644
--- a/app/finders/conversation_finder.rb
+++ b/app/finders/conversation_finder.rb
@@ -86,9 +86,13 @@ class ConversationFinder
@team = current_account.teams.find(params[:team_id]) if params[:team_id]
end
- def find_all_conversations
+ def find_conversation_by_inbox
@conversations = current_account.conversations
- @conversations = @conversations.where(inbox_id: @inbox_ids) unless @is_admin
+ @conversations = @conversations.where(inbox_id: @inbox_ids) unless params[:inbox_id].blank? && @is_admin
+ end
+
+ def find_all_conversations
+ find_conversation_by_inbox
filter_by_conversation_type if params[:conversation_type]
@conversations
end
diff --git a/spec/finders/conversation_finder_spec.rb b/spec/finders/conversation_finder_spec.rb
index 4d6e9ed40..0174ccfb0 100644
--- a/spec/finders/conversation_finder_spec.rb
+++ b/spec/finders/conversation_finder_spec.rb
@@ -57,6 +57,16 @@ describe ConversationFinder do
expect(result[:conversations].map(&:id)).not_to include(restricted_conversation.id)
end
+
+ it 'returns only the conversations from the inbox if inbox_id filter is passed' do
+ conversation = create(:conversation, account: account, inbox_id: inbox.id)
+ params = { inbox_id: restricted_inbox.id }
+ result = described_class.new(admin, params).perform
+
+ conversation_ids = result[:conversations].map(&:id)
+ expect(conversation_ids).not_to include(conversation.id)
+ expect(conversation_ids).to include(restricted_conversation.id)
+ end
end
context 'with assignee_type all' do
From 97612148600d207bf28294f074336b88a86c0657 Mon Sep 17 00:00:00 2001
From: Shivam Mishra
Date: Fri, 28 Mar 2025 08:11:02 +0530
Subject: [PATCH 005/554] feat: Add debounce for meta query (#11195)
This PR combines the approaches in
https://github.com/chatwoot/chatwoot/pull/11190 and
https://github.com/chatwoot/chatwoot/pull/11187 to debounce the meta
request with a max wait time of 2.5 seconds With 500 concurrent users,
the theoretical limit with this is 720K requests per minute, if all of
them continuously receive websocket events.
The max wait of 2.5 seconds is still very generous, and we can easily
make it 2 seconds for smaller accounts and 5 seconds for larger
accounts.
```js
const debouncedFetchMetaData = debounce(fetchMetaData, 500, false, 200);
const longDebouncedFetchMetaData = debounce(fetchMetaData, 500, false, 5000);
export const actions = {
get: async ({ commit, state: $state }, params) => {
if ($state.allCount > 100) {
longDebouncedFetchMetaData(commit, params);
} else {
debouncedFetchMetaData(commit, params);
}
},
set({ commit }, meta) {
commit(types.SET_CONV_TAB_META, meta);
},
};
```
Related Utils PR: https://github.com/chatwoot/utils/pull/49
Here's the debounce in action
---------
Co-authored-by: Pranav
---
.../helper/ConversationMetaThrottleManager.js | 29 ------------
.../ConversationMetaThrottleManager.spec.js | 34 --------------
.../store/modules/conversationStats.js | 47 +++++++------------
.../specs/conversationStats/actions.spec.js | 27 +++++++++--
.../specs/conversationStats/helper.spec.js | 37 ---------------
package.json | 2 +-
pnpm-lock.yaml | 10 ++--
7 files changed, 45 insertions(+), 141 deletions(-)
delete mode 100644 app/javascript/dashboard/helper/ConversationMetaThrottleManager.js
delete mode 100644 app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js
delete mode 100644 app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js
diff --git a/app/javascript/dashboard/helper/ConversationMetaThrottleManager.js b/app/javascript/dashboard/helper/ConversationMetaThrottleManager.js
deleted file mode 100644
index 4f7c7a610..000000000
--- a/app/javascript/dashboard/helper/ConversationMetaThrottleManager.js
+++ /dev/null
@@ -1,29 +0,0 @@
-class ConversationMetaThrottleManager {
- constructor() {
- this.lastUpdatedTime = null;
- }
-
- shouldThrottle(threshold = 10000) {
- if (!this.lastUpdatedTime) {
- return false;
- }
-
- const currentTime = new Date().getTime();
- const lastUpdatedTime = new Date(this.lastUpdatedTime).getTime();
-
- if (currentTime - lastUpdatedTime < threshold) {
- return true;
- }
- return false;
- }
-
- markUpdate() {
- this.lastUpdatedTime = new Date();
- }
-
- reset() {
- this.lastUpdatedTime = null;
- }
-}
-
-export default new ConversationMetaThrottleManager();
diff --git a/app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js b/app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js
deleted file mode 100644
index b0c67ffaa..000000000
--- a/app/javascript/dashboard/helper/specs/ConversationMetaThrottleManager.spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import ConversationMetaThrottleManager from '../ConversationMetaThrottleManager';
-
-describe('ConversationMetaThrottleManager', () => {
- beforeEach(() => {
- // Reset the lastUpdatedTime before each test
- ConversationMetaThrottleManager.lastUpdatedTime = null;
- });
-
- describe('shouldThrottle', () => {
- it('returns false when lastUpdatedTime is not set', () => {
- expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(false);
- });
-
- it('returns true when time difference is less than threshold', () => {
- ConversationMetaThrottleManager.markUpdate();
- expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(true);
- });
-
- it('returns false when time difference is more than threshold', () => {
- ConversationMetaThrottleManager.lastUpdatedTime = new Date(
- Date.now() - 11000
- );
- expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(false);
- });
-
- it('respects custom threshold value', () => {
- ConversationMetaThrottleManager.lastUpdatedTime = new Date(
- Date.now() - 5000
- );
- expect(ConversationMetaThrottleManager.shouldThrottle(3000)).toBe(false);
- expect(ConversationMetaThrottleManager.shouldThrottle(6000)).toBe(true);
- });
- });
-});
diff --git a/app/javascript/dashboard/store/modules/conversationStats.js b/app/javascript/dashboard/store/modules/conversationStats.js
index 8de20370b..ef70500ef 100644
--- a/app/javascript/dashboard/store/modules/conversationStats.js
+++ b/app/javascript/dashboard/store/modules/conversationStats.js
@@ -1,7 +1,6 @@
import types from '../mutation-types';
import ConversationApi from '../../api/inbox/conversation';
-
-import ConversationMetaThrottleManager from 'dashboard/helper/ConversationMetaThrottleManager';
+import { debounce } from '@chatwoot/utils';
const state = {
mineCount: 0,
@@ -13,38 +12,24 @@ export const getters = {
getStats: $state => $state,
};
-export const shouldThrottle = conversationCount => {
- // The threshold for throttling is different for normal users and large accounts
- // Normal users: 2 seconds
- // Large accounts: 10 seconds
- // We would only update the conversation stats based on the threshold above.
- // This is done to reduce the number of /meta request made to the server.
- const NORMAL_USER_THRESHOLD = 2000;
- const LARGE_ACCOUNT_THRESHOLD = 10000;
-
- const threshold =
- conversationCount > 100 ? LARGE_ACCOUNT_THRESHOLD : NORMAL_USER_THRESHOLD;
- return ConversationMetaThrottleManager.shouldThrottle(threshold);
+// Create a debounced version of the actual API call function
+const fetchMetaData = async (commit, params) => {
+ try {
+ const response = await ConversationApi.meta(params);
+ const {
+ data: { meta },
+ } = response;
+ commit(types.SET_CONV_TAB_META, meta);
+ } catch (error) {
+ // ignore
+ }
};
-export const actions = {
- get: async ({ commit, state: $state }, params) => {
- if (shouldThrottle($state.allCount)) {
- // eslint-disable-next-line no-console
- console.warn('Throttle /meta fetch, will resume after threshold');
- return;
- }
- ConversationMetaThrottleManager.markUpdate();
+const debouncedFetchMetaData = debounce(fetchMetaData, 500, false, 2500);
- try {
- const response = await ConversationApi.meta(params);
- const {
- data: { meta },
- } = response;
- commit(types.SET_CONV_TAB_META, meta);
- } catch (error) {
- // Ignore error
- }
+export const actions = {
+ get: async ({ commit }, params) => {
+ debouncedFetchMetaData(commit, params);
},
set({ commit }, meta) {
commit(types.SET_CONV_TAB_META, meta);
diff --git a/app/javascript/dashboard/store/modules/specs/conversationStats/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversationStats/actions.spec.js
index 4572387ac..43f0efb3a 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationStats/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationStats/actions.spec.js
@@ -6,22 +6,41 @@ const commit = vi.fn();
global.axios = axios;
vi.mock('axios');
+vi.mock('@chatwoot/utils', () => ({
+ debounce: vi.fn(fn => {
+ return fn;
+ }),
+}));
+
describe('#actions', () => {
+ beforeEach(() => {
+ vi.useFakeTimers(); // Set up fake timers
+ commit.mockClear();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers(); // Reset to real timers after each test
+ });
+
describe('#get', () => {
it('sends correct mutations if API is success', async () => {
axios.get.mockResolvedValue({ data: { meta: { mine_count: 1 } } });
- await actions.get(
- { commit, state: { updatedOn: null } },
+ actions.get(
+ { commit, state: { allCount: 0 } },
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
);
+
+ await vi.runAllTimersAsync();
+ await vi.waitFor(() => expect(commit).toHaveBeenCalled());
+
expect(commit.mock.calls).toEqual([
[types.default.SET_CONV_TAB_META, { mine_count: 1 }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
- await actions.get(
- { commit, state: { updatedOn: null } },
+ actions.get(
+ { commit, state: { allCount: 0 } },
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
);
expect(commit.mock.calls).toEqual([]);
diff --git a/app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js b/app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js
deleted file mode 100644
index eb07fd1c8..000000000
--- a/app/javascript/dashboard/store/modules/specs/conversationStats/helper.spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import ConversationMetaThrottleManager from 'dashboard/helper/ConversationMetaThrottleManager';
-import { shouldThrottle } from '../../conversationStats';
-
-vi.mock('dashboard/helper/ConversationMetaThrottleManager', () => ({
- default: {
- shouldThrottle: vi.fn(),
- },
-}));
-
-describe('shouldThrottle', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it('uses normal threshold for accounts with 100 or fewer conversations', () => {
- shouldThrottle(100);
- expect(ConversationMetaThrottleManager.shouldThrottle).toHaveBeenCalledWith(
- 2000
- );
- });
-
- it('uses large account threshold for accounts with more than 100 conversations', () => {
- shouldThrottle(101);
- expect(ConversationMetaThrottleManager.shouldThrottle).toHaveBeenCalledWith(
- 10000
- );
- });
-
- it('returns the throttle value from ConversationMetaThrottleManager', () => {
- ConversationMetaThrottleManager.shouldThrottle.mockReturnValue(true);
- expect(shouldThrottle(50)).toBe(true);
-
- ConversationMetaThrottleManager.shouldThrottle.mockReturnValue(false);
- expect(shouldThrottle(150)).toBe(false);
- });
-});
diff --git a/package.json b/package.json
index 9e5bab631..6d7300667 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"@breezystack/lamejs": "^1.2.7",
"@chatwoot/ninja-keys": "1.2.3",
"@chatwoot/prosemirror-schema": "1.1.1-next",
- "@chatwoot/utils": "^0.0.41",
+ "@chatwoot/utils": "^0.0.42",
"@formkit/core": "^1.6.7",
"@formkit/vue": "^1.6.7",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6749103a8..b32dc7521 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,8 +23,8 @@ importers:
specifier: 1.1.1-next
version: 1.1.1-next
'@chatwoot/utils':
- specifier: ^0.0.41
- version: 0.0.41
+ specifier: ^0.0.42
+ version: 0.0.42
'@formkit/core':
specifier: ^1.6.7
version: 1.6.7
@@ -406,8 +406,8 @@ packages:
'@chatwoot/prosemirror-schema@1.1.1-next':
resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==}
- '@chatwoot/utils@0.0.41':
- resolution: {integrity: sha512-f0D+XArVYbc9m9M7KZpCaVJ+EUVzobX+D9P5Vt/h2jUipg706GoBhGwsP8kjfWhUdNdcS+H+OB4ZCKGF1NIkTQ==}
+ '@chatwoot/utils@0.0.42':
+ resolution: {integrity: sha512-TrEywcG1zjgBScVrQla7GMJwXsbLyc5u/verm/LbLrGxizU2NcNoJecRvJOUgL65kYVEtcO9//+gIDswwqnt6g==}
engines: {node: '>=10'}
'@codemirror/commands@6.7.0':
@@ -5250,7 +5250,7 @@ snapshots:
prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3)
prosemirror-view: 1.34.1
- '@chatwoot/utils@0.0.41':
+ '@chatwoot/utils@0.0.42':
dependencies:
date-fns: 2.30.0
From 2fd54b8d9dfd2885c430fe96667ca98f16012b15 Mon Sep 17 00:00:00 2001
From: Shivam Mishra
Date: Fri, 28 Mar 2025 09:41:19 +0530
Subject: [PATCH 006/554] feat: Use long debounce for larger accounts (#11200)
---
.../dashboard/store/modules/conversationStats.js | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/app/javascript/dashboard/store/modules/conversationStats.js b/app/javascript/dashboard/store/modules/conversationStats.js
index ef70500ef..2d53ac573 100644
--- a/app/javascript/dashboard/store/modules/conversationStats.js
+++ b/app/javascript/dashboard/store/modules/conversationStats.js
@@ -25,11 +25,16 @@ const fetchMetaData = async (commit, params) => {
}
};
-const debouncedFetchMetaData = debounce(fetchMetaData, 500, false, 2500);
+const debouncedFetchMetaData = debounce(fetchMetaData, 500, false, 1000);
+const longDebouncedFetchMetaData = debounce(fetchMetaData, 500, false, 5000);
export const actions = {
- get: async ({ commit }, params) => {
- debouncedFetchMetaData(commit, params);
+ get: async ({ commit, store: $store }, params) => {
+ if ($store.allCount > 100) {
+ longDebouncedFetchMetaData(commit, params);
+ } else {
+ debouncedFetchMetaData(commit, params);
+ }
},
set({ commit }, meta) {
commit(types.SET_CONV_TAB_META, meta);
From 21fbed32eb532d47e30b3899a10a448c3902db61 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Mar 2025 21:11:43 -0700
Subject: [PATCH 007/554] chore(deps-dev): Bump vite from 5.4.12 to 5.4.15
(#11199)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 5.4.12 to 5.4.15.
Release notes
Sourced from vite's
releases.
v5.4.15
Please refer to CHANGELOG.md
for details.
v5.4.14
Please refer to CHANGELOG.md
for details.
v5.4.13
Please refer to CHANGELOG.md
for details.
Changelog
Sourced from vite's
changelog.
5.4.15 (2025-03-24)
5.4.14 (2025-01-21)
5.4.13 (2025-01-20)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package.json | 4 +-
pnpm-lock.yaml | 268 ++++++++++++++++++++++++++-----------------------
2 files changed, 143 insertions(+), 129 deletions(-)
diff --git a/package.json b/package.json
index 6d7300667..4c30829f7 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,7 @@
"prosemirror-model": "^1.22.3",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.13",
- "vite": "^5.4.12",
+ "vite": "^5.4.15",
"vite-plugin-ruby": "^5.0.0",
"vitest": "3.0.5"
},
@@ -154,7 +154,7 @@
"pnpm": {
"overrides": {
"vite-node": "2.0.1",
- "vite": "5.4.12",
+ "vite": "5.4.15",
"vitest": "3.0.5"
}
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b32dc7521..41ef0526a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,7 +6,7 @@ settings:
overrides:
vite-node: 2.0.1
- vite: 5.4.12
+ vite: 5.4.15
vitest: 3.0.5
importers:
@@ -72,7 +72,7 @@ importers:
version: 8.20.5(vue@3.5.12(typescript@5.6.2))
'@vitejs/plugin-vue':
specifier: ^5.1.4
- version: 5.1.4(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
+ version: 5.1.4(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
'@vue/compiler-sfc':
specifier: ^3.5.8
version: 3.5.8
@@ -238,7 +238,7 @@ importers:
version: 1.8.1(tailwindcss@3.4.13)
'@histoire/plugin-vue':
specifier: 0.17.15
- version: 0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
+ version: 0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
'@iconify-json/logos':
specifier: ^1.2.3
version: 1.2.3
@@ -301,7 +301,7 @@ importers:
version: 6.0.0
histoire:
specifier: 0.17.15
- version: 0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ version: 0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
husky:
specifier: ^7.0.0
version: 7.0.4
@@ -330,11 +330,11 @@ importers:
specifier: ^3.4.13
version: 3.4.13
vite:
- specifier: 5.4.12
- version: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ specifier: 5.4.15
+ version: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
vite-plugin-ruby:
specifier: ^5.0.0
- version: 5.0.0(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ version: 5.0.0(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
vitest:
specifier: 3.0.5
version: 3.0.5(@types/node@22.7.0)(jsdom@24.1.3)(sass@1.79.3)(terser@5.33.0)
@@ -860,7 +860,7 @@ packages:
'@histoire/shared@0.17.17':
resolution: {integrity: sha512-ueGtURysonT0MujCObPCR57+mgZluMEXCrbc2FBgKAD/DoAt38tNwSGsmLldk2O6nTr7lr6ClbVSgWrLwgY6Xw==}
peerDependencies:
- vite: 5.4.12
+ vite: 5.4.15
'@histoire/vendors@0.17.17':
resolution: {integrity: sha512-QZvmffdoJlLuYftPIkOU5Q2FPAdG2JjMuQ5jF7NmEl0n1XnmbMqtRkdYTZ4eF6CO1KLZ0Zyf6gBQvoT1uWNcjA==}
@@ -1039,98 +1039,103 @@ packages:
'@rails/ujs@7.1.400':
resolution: {integrity: sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w==}
- '@rollup/rollup-android-arm-eabi@4.31.0':
- resolution: {integrity: sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==}
+ '@rollup/rollup-android-arm-eabi@4.37.0':
+ resolution: {integrity: sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==}
cpu: [arm]
os: [android]
- '@rollup/rollup-android-arm64@4.31.0':
- resolution: {integrity: sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==}
+ '@rollup/rollup-android-arm64@4.37.0':
+ resolution: {integrity: sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==}
cpu: [arm64]
os: [android]
- '@rollup/rollup-darwin-arm64@4.31.0':
- resolution: {integrity: sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==}
+ '@rollup/rollup-darwin-arm64@4.37.0':
+ resolution: {integrity: sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==}
cpu: [arm64]
os: [darwin]
- '@rollup/rollup-darwin-x64@4.31.0':
- resolution: {integrity: sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==}
+ '@rollup/rollup-darwin-x64@4.37.0':
+ resolution: {integrity: sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==}
cpu: [x64]
os: [darwin]
- '@rollup/rollup-freebsd-arm64@4.31.0':
- resolution: {integrity: sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==}
+ '@rollup/rollup-freebsd-arm64@4.37.0':
+ resolution: {integrity: sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==}
cpu: [arm64]
os: [freebsd]
- '@rollup/rollup-freebsd-x64@4.31.0':
- resolution: {integrity: sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==}
+ '@rollup/rollup-freebsd-x64@4.37.0':
+ resolution: {integrity: sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==}
cpu: [x64]
os: [freebsd]
- '@rollup/rollup-linux-arm-gnueabihf@4.31.0':
- resolution: {integrity: sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==}
+ '@rollup/rollup-linux-arm-gnueabihf@4.37.0':
+ resolution: {integrity: sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==}
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm-musleabihf@4.31.0':
- resolution: {integrity: sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==}
+ '@rollup/rollup-linux-arm-musleabihf@4.37.0':
+ resolution: {integrity: sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==}
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm64-gnu@4.31.0':
- resolution: {integrity: sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==}
+ '@rollup/rollup-linux-arm64-gnu@4.37.0':
+ resolution: {integrity: sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==}
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-arm64-musl@4.31.0':
- resolution: {integrity: sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==}
+ '@rollup/rollup-linux-arm64-musl@4.37.0':
+ resolution: {integrity: sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==}
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-loongarch64-gnu@4.31.0':
- resolution: {integrity: sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==}
+ '@rollup/rollup-linux-loongarch64-gnu@4.37.0':
+ resolution: {integrity: sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==}
cpu: [loong64]
os: [linux]
- '@rollup/rollup-linux-powerpc64le-gnu@4.31.0':
- resolution: {integrity: sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==}
+ '@rollup/rollup-linux-powerpc64le-gnu@4.37.0':
+ resolution: {integrity: sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==}
cpu: [ppc64]
os: [linux]
- '@rollup/rollup-linux-riscv64-gnu@4.31.0':
- resolution: {integrity: sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==}
+ '@rollup/rollup-linux-riscv64-gnu@4.37.0':
+ resolution: {integrity: sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==}
cpu: [riscv64]
os: [linux]
- '@rollup/rollup-linux-s390x-gnu@4.31.0':
- resolution: {integrity: sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==}
+ '@rollup/rollup-linux-riscv64-musl@4.37.0':
+ resolution: {integrity: sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.37.0':
+ resolution: {integrity: sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==}
cpu: [s390x]
os: [linux]
- '@rollup/rollup-linux-x64-gnu@4.31.0':
- resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==}
+ '@rollup/rollup-linux-x64-gnu@4.37.0':
+ resolution: {integrity: sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==}
cpu: [x64]
os: [linux]
- '@rollup/rollup-linux-x64-musl@4.31.0':
- resolution: {integrity: sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==}
+ '@rollup/rollup-linux-x64-musl@4.37.0':
+ resolution: {integrity: sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==}
cpu: [x64]
os: [linux]
- '@rollup/rollup-win32-arm64-msvc@4.31.0':
- resolution: {integrity: sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==}
+ '@rollup/rollup-win32-arm64-msvc@4.37.0':
+ resolution: {integrity: sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==}
cpu: [arm64]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.31.0':
- resolution: {integrity: sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==}
+ '@rollup/rollup-win32-ia32-msvc@4.37.0':
+ resolution: {integrity: sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==}
cpu: [ia32]
os: [win32]
- '@rollup/rollup-win32-x64-msvc@4.31.0':
- resolution: {integrity: sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==}
+ '@rollup/rollup-win32-x64-msvc@4.37.0':
+ resolution: {integrity: sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==}
cpu: [x64]
os: [win32]
@@ -1744,6 +1749,9 @@ packages:
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+ '@types/estree@1.0.7':
+ resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
'@types/flexsearch@0.7.6':
resolution: {integrity: sha512-H5IXcRn96/gaDmo+rDl2aJuIJsob8dgOXDqf8K0t8rWZd1AFNaaspmRsElESiU+EWE33qfbFPgI0OC/B1g9FCA==}
@@ -1795,7 +1803,7 @@ packages:
resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
- vite: 5.4.12
+ vite: 5.4.15
vue: ^3.2.25
'@vitest/coverage-v8@3.0.5':
@@ -1814,7 +1822,7 @@ packages:
resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==}
peerDependencies:
msw: ^2.4.9
- vite: 5.4.12
+ vite: 5.4.15
peerDependenciesMeta:
msw:
optional: true
@@ -3095,7 +3103,7 @@ packages:
resolution: {integrity: sha512-DiRMSIgj340z+zikqf0f3Pj0CTv2/xtdBMBIAO1EARat+QXxMwumbfK41Gi7f9IIBr+UVmomNcwFxVY2EM/vrw==}
hasBin: true
peerDependencies:
- vite: 5.4.12
+ vite: 5.4.15
hotkeys-js@3.8.7:
resolution: {integrity: sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg==}
@@ -4179,8 +4187,8 @@ packages:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.1:
- resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
+ postcss@8.5.3:
+ resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
@@ -4339,8 +4347,8 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
- rollup@4.31.0:
- resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==}
+ rollup@4.37.0:
+ resolution: {integrity: sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -4858,10 +4866,10 @@ packages:
vite-plugin-ruby@5.0.0:
resolution: {integrity: sha512-c8PjTp21Ah/ttgnNUyu0qvCXZI08Jr9I24oUKg3TRIRhF5GcOZ++6wtlTCrNFd9COEQbpXHxlRIXd/MEg0iZJw==}
peerDependencies:
- vite: 5.4.12
+ vite: 5.4.15
- vite@5.4.12:
- resolution: {integrity: sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==}
+ vite@5.4.15:
+ resolution: {integrity: sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -5667,10 +5675,10 @@ snapshots:
highlight.js: 11.10.0
vue: 3.5.12(typescript@5.6.2)
- '@histoire/app@0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@histoire/app@0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
dependencies:
- '@histoire/controls': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/shared': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/controls': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/shared': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
'@histoire/vendors': 0.17.17
'@types/flexsearch': 0.7.6
flexsearch: 0.7.21
@@ -5678,7 +5686,7 @@ snapshots:
transitivePeerDependencies:
- vite
- '@histoire/controls@0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@histoire/controls@0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
dependencies:
'@codemirror/commands': 6.7.0
'@codemirror/lang-json': 6.0.1
@@ -5687,26 +5695,26 @@ snapshots:
'@codemirror/state': 6.4.1
'@codemirror/theme-one-dark': 6.1.2
'@codemirror/view': 6.34.1
- '@histoire/shared': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/shared': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
'@histoire/vendors': 0.17.17
transitivePeerDependencies:
- vite
- '@histoire/plugin-vue@0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
+ '@histoire/plugin-vue@0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
dependencies:
- '@histoire/controls': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/shared': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/controls': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/shared': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
'@histoire/vendors': 0.17.17
change-case: 4.1.2
globby: 13.2.2
- histoire: 0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ histoire: 0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
launch-editor: 2.9.1
pathe: 1.1.2
vue: 3.5.12(typescript@5.6.2)
transitivePeerDependencies:
- vite
- '@histoire/shared@0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@histoire/shared@0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
dependencies:
'@histoire/vendors': 0.17.17
'@types/fs-extra': 9.0.13
@@ -5714,7 +5722,7 @@ snapshots:
chokidar: 3.6.0
pathe: 1.1.2
picocolors: 1.1.0
- vite: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
'@histoire/vendors@0.17.17': {}
@@ -5939,61 +5947,64 @@ snapshots:
'@rails/ujs@7.1.400': {}
- '@rollup/rollup-android-arm-eabi@4.31.0':
+ '@rollup/rollup-android-arm-eabi@4.37.0':
optional: true
- '@rollup/rollup-android-arm64@4.31.0':
+ '@rollup/rollup-android-arm64@4.37.0':
optional: true
- '@rollup/rollup-darwin-arm64@4.31.0':
+ '@rollup/rollup-darwin-arm64@4.37.0':
optional: true
- '@rollup/rollup-darwin-x64@4.31.0':
+ '@rollup/rollup-darwin-x64@4.37.0':
optional: true
- '@rollup/rollup-freebsd-arm64@4.31.0':
+ '@rollup/rollup-freebsd-arm64@4.37.0':
optional: true
- '@rollup/rollup-freebsd-x64@4.31.0':
+ '@rollup/rollup-freebsd-x64@4.37.0':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.31.0':
+ '@rollup/rollup-linux-arm-gnueabihf@4.37.0':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.31.0':
+ '@rollup/rollup-linux-arm-musleabihf@4.37.0':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.31.0':
+ '@rollup/rollup-linux-arm64-gnu@4.37.0':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.31.0':
+ '@rollup/rollup-linux-arm64-musl@4.37.0':
optional: true
- '@rollup/rollup-linux-loongarch64-gnu@4.31.0':
+ '@rollup/rollup-linux-loongarch64-gnu@4.37.0':
optional: true
- '@rollup/rollup-linux-powerpc64le-gnu@4.31.0':
+ '@rollup/rollup-linux-powerpc64le-gnu@4.37.0':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.31.0':
+ '@rollup/rollup-linux-riscv64-gnu@4.37.0':
optional: true
- '@rollup/rollup-linux-s390x-gnu@4.31.0':
+ '@rollup/rollup-linux-riscv64-musl@4.37.0':
optional: true
- '@rollup/rollup-linux-x64-gnu@4.31.0':
+ '@rollup/rollup-linux-s390x-gnu@4.37.0':
optional: true
- '@rollup/rollup-linux-x64-musl@4.31.0':
+ '@rollup/rollup-linux-x64-gnu@4.37.0':
optional: true
- '@rollup/rollup-win32-arm64-msvc@4.31.0':
+ '@rollup/rollup-linux-x64-musl@4.37.0':
optional: true
- '@rollup/rollup-win32-ia32-msvc@4.31.0':
+ '@rollup/rollup-win32-arm64-msvc@4.37.0':
optional: true
- '@rollup/rollup-win32-x64-msvc@4.31.0':
+ '@rollup/rollup-win32-ia32-msvc@4.37.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.37.0':
optional: true
'@rtsao/scc@1.1.0': {}
@@ -6738,6 +6749,8 @@ snapshots:
'@types/estree@1.0.6': {}
+ '@types/estree@1.0.7': {}
+
'@types/flexsearch@0.7.6': {}
'@types/fs-extra@9.0.13':
@@ -6794,9 +6807,9 @@ snapshots:
global: 4.4.0
is-function: 1.0.2
- '@vitejs/plugin-vue@5.1.4(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
+ '@vitejs/plugin-vue@5.1.4(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
dependencies:
- vite: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
vue: 3.5.12(typescript@5.6.2)
'@vitest/coverage-v8@3.0.5(vitest@3.0.5(@types/node@22.7.0)(jsdom@24.1.3)(sass@1.79.3)(terser@5.33.0))':
@@ -6824,13 +6837,13 @@ snapshots:
chai: 5.1.2
tinyrainbow: 2.0.0
- '@vitest/mocker@3.0.5(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@vitest/mocker@3.0.5(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
dependencies:
'@vitest/spy': 3.0.5
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
- vite: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
'@vitest/pretty-format@3.0.5':
dependencies:
@@ -6905,7 +6918,7 @@ snapshots:
'@vue/shared': 3.5.12
estree-walker: 2.0.2
magic-string: 0.30.17
- postcss: 8.5.1
+ postcss: 8.5.3
source-map-js: 1.2.1
'@vue/compiler-sfc@3.5.13':
@@ -6917,7 +6930,7 @@ snapshots:
'@vue/shared': 3.5.13
estree-walker: 2.0.2
magic-string: 0.30.17
- postcss: 8.5.1
+ postcss: 8.5.3
source-map-js: 1.2.1
'@vue/compiler-sfc@3.5.8':
@@ -8068,7 +8081,7 @@ snapshots:
estree-walker@3.0.3:
dependencies:
- '@types/estree': 1.0.6
+ '@types/estree': 1.0.7
esutils@2.0.3: {}
@@ -8385,12 +8398,12 @@ snapshots:
highlight.js@11.10.0: {}
- histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
+ histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
dependencies:
'@akryum/tinypool': 0.3.1
- '@histoire/app': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/controls': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/shared': 0.17.17(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/app': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/controls': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/shared': 0.17.17(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
'@histoire/vendors': 0.17.17
'@types/flexsearch': 0.7.6
'@types/markdown-it': 12.2.3
@@ -8417,7 +8430,7 @@ snapshots:
sade: 1.8.1
shiki-es: 0.2.0
sirv: 2.0.4
- vite: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
vite-node: 2.0.1(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
transitivePeerDependencies:
- '@types/node'
@@ -9579,7 +9592,7 @@ snapshots:
picocolors: 1.1.0
source-map-js: 1.2.1
- postcss@8.5.1:
+ postcss@8.5.3:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
@@ -9758,29 +9771,30 @@ snapshots:
dependencies:
glob: 7.2.3
- rollup@4.31.0:
+ rollup@4.37.0:
dependencies:
'@types/estree': 1.0.6
optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.31.0
- '@rollup/rollup-android-arm64': 4.31.0
- '@rollup/rollup-darwin-arm64': 4.31.0
- '@rollup/rollup-darwin-x64': 4.31.0
- '@rollup/rollup-freebsd-arm64': 4.31.0
- '@rollup/rollup-freebsd-x64': 4.31.0
- '@rollup/rollup-linux-arm-gnueabihf': 4.31.0
- '@rollup/rollup-linux-arm-musleabihf': 4.31.0
- '@rollup/rollup-linux-arm64-gnu': 4.31.0
- '@rollup/rollup-linux-arm64-musl': 4.31.0
- '@rollup/rollup-linux-loongarch64-gnu': 4.31.0
- '@rollup/rollup-linux-powerpc64le-gnu': 4.31.0
- '@rollup/rollup-linux-riscv64-gnu': 4.31.0
- '@rollup/rollup-linux-s390x-gnu': 4.31.0
- '@rollup/rollup-linux-x64-gnu': 4.31.0
- '@rollup/rollup-linux-x64-musl': 4.31.0
- '@rollup/rollup-win32-arm64-msvc': 4.31.0
- '@rollup/rollup-win32-ia32-msvc': 4.31.0
- '@rollup/rollup-win32-x64-msvc': 4.31.0
+ '@rollup/rollup-android-arm-eabi': 4.37.0
+ '@rollup/rollup-android-arm64': 4.37.0
+ '@rollup/rollup-darwin-arm64': 4.37.0
+ '@rollup/rollup-darwin-x64': 4.37.0
+ '@rollup/rollup-freebsd-arm64': 4.37.0
+ '@rollup/rollup-freebsd-x64': 4.37.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.37.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.37.0
+ '@rollup/rollup-linux-arm64-gnu': 4.37.0
+ '@rollup/rollup-linux-arm64-musl': 4.37.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.37.0
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.37.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.37.0
+ '@rollup/rollup-linux-riscv64-musl': 4.37.0
+ '@rollup/rollup-linux-s390x-gnu': 4.37.0
+ '@rollup/rollup-linux-x64-gnu': 4.37.0
+ '@rollup/rollup-linux-x64-musl': 4.37.0
+ '@rollup/rollup-win32-arm64-msvc': 4.37.0
+ '@rollup/rollup-win32-ia32-msvc': 4.37.0
+ '@rollup/rollup-win32-x64-msvc': 4.37.0
fsevents: 2.3.3
rope-sequence@1.3.2: {}
@@ -10369,7 +10383,7 @@ snapshots:
debug: 4.4.0
pathe: 1.1.2
picocolors: 1.1.1
- vite: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
transitivePeerDependencies:
- '@types/node'
- less
@@ -10381,19 +10395,19 @@ snapshots:
- supports-color
- terser
- vite-plugin-ruby@5.0.0(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
+ vite-plugin-ruby@5.0.0(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
dependencies:
debug: 4.3.5
fast-glob: 3.3.2
- vite: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
transitivePeerDependencies:
- supports-color
- vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0):
+ vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0):
dependencies:
esbuild: 0.21.5
- postcss: 8.5.1
- rollup: 4.31.0
+ postcss: 8.5.3
+ rollup: 4.37.0
optionalDependencies:
'@types/node': 22.7.0
fsevents: 2.3.3
@@ -10403,7 +10417,7 @@ snapshots:
vitest@3.0.5(@types/node@22.7.0)(jsdom@24.1.3)(sass@1.79.3)(terser@5.33.0):
dependencies:
'@vitest/expect': 3.0.5
- '@vitest/mocker': 3.0.5(vite@5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@vitest/mocker': 3.0.5(vite@5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
'@vitest/pretty-format': 3.0.5
'@vitest/runner': 3.0.5
'@vitest/snapshot': 3.0.5
@@ -10419,7 +10433,7 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 2.0.0
- vite: 5.4.12(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
vite-node: 2.0.1(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
why-is-node-running: 2.3.0
optionalDependencies:
From 91fa68bbb51cf70d91cd3e3fe8561a170a0e40b7 Mon Sep 17 00:00:00 2001
From: Shivam Mishra
Date: Fri, 28 Mar 2025 09:58:22 +0530
Subject: [PATCH 008/554] fix: Fix typo in conversationStats/get (#11201)
---
app/javascript/dashboard/store/modules/conversationStats.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/javascript/dashboard/store/modules/conversationStats.js b/app/javascript/dashboard/store/modules/conversationStats.js
index 2d53ac573..bae365f30 100644
--- a/app/javascript/dashboard/store/modules/conversationStats.js
+++ b/app/javascript/dashboard/store/modules/conversationStats.js
@@ -29,8 +29,8 @@ const debouncedFetchMetaData = debounce(fetchMetaData, 500, false, 1000);
const longDebouncedFetchMetaData = debounce(fetchMetaData, 500, false, 5000);
export const actions = {
- get: async ({ commit, store: $store }, params) => {
- if ($store.allCount > 100) {
+ get: async ({ commit, state: $state }, params) => {
+ if ($state.allCount > 100) {
longDebouncedFetchMetaData(commit, params);
} else {
debouncedFetchMetaData(commit, params);
From 001b25c92aa2e37835a289677c8cea9357ad6150 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Fri, 28 Mar 2025 10:08:22 +0530
Subject: [PATCH 009/554] fix: Reset recorder and attachments when switching
chats (#11174)
# Pull Request Template
## Description
This PR will reset the recorder and clear attachments when switching
chats. It ensures that any ongoing recordings or attached files do not
persist across different conversations
Fixes
https://linear.app/chatwoot/issue/CW-4157/recorded-audio-is-being-preserved-between-conversations
https://github.com/chatwoot/chatwoot/issues/11136
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/6d6361650794426497467d6de1b4900d?sid=d856c540-1032-4ea2-8765-8704b76e8e1d
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---
.../dashboard/components/widgets/conversation/ReplyBox.vue | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index d9b1de5e9..c42b678fd 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -420,6 +420,7 @@ export default {
if (conversationId !== oldConversationId) {
this.setToDraft(oldConversationId, this.replyType);
this.getFromDraft();
+ this.resetRecorderAndClearAttachments();
}
},
message(updatedMessage) {
@@ -529,6 +530,12 @@ export default {
);
}
},
+ resetRecorderAndClearAttachments() {
+ // Reset audio recorder UI state
+ this.resetAudioRecorderInput();
+ // Reset attached files
+ this.attachedFiles = [];
+ },
saveDraft(conversationId, replyType) {
if (this.message || this.message === '') {
const key = `draft-${conversationId}-${replyType}`;
From 0175714d65bbc49680a94d428646504cd50ab9a0 Mon Sep 17 00:00:00 2001
From: Vishnu Narayanan
Date: Fri, 28 Mar 2025 12:18:39 +0530
Subject: [PATCH 010/554] feat: add job to remove stale contacts and
contact_inboxes (#11186)
- Add a job to remove stale contacts and contact_inboxes across all accounts
Stale anonymous contact is defined as
- have no identification (email, phone_number, and identifier are NULL)
- have no conversations
- are older than 30 days
---------
Co-authored-by: Pranav
Co-authored-by: Shivam Mishra
---
.../internal/process_stale_contacts_job.rb | 18 ++++++++
.../internal/remove_stale_contacts_job.rb | 13 ++++++
app/models/contact.rb | 12 ++++++
.../internal/remove_stale_contacts_service.rb | 19 +++++++++
config/schedule.yml | 7 ++++
.../process_stale_contacts_job_spec.rb | 34 +++++++++++++++
.../remove_stale_contacts_job_spec.rb | 20 +++++++++
.../remove_stale_contacts_service_spec.rb | 41 +++++++++++++++++++
8 files changed, 164 insertions(+)
create mode 100644 app/jobs/internal/process_stale_contacts_job.rb
create mode 100644 app/jobs/internal/remove_stale_contacts_job.rb
create mode 100644 app/services/internal/remove_stale_contacts_service.rb
create mode 100644 spec/jobs/internal/process_stale_contacts_job_spec.rb
create mode 100644 spec/jobs/internal/remove_stale_contacts_job_spec.rb
create mode 100644 spec/services/internal/remove_stale_contacts_service_spec.rb
diff --git a/app/jobs/internal/process_stale_contacts_job.rb b/app/jobs/internal/process_stale_contacts_job.rb
new file mode 100644
index 000000000..eaf53be75
--- /dev/null
+++ b/app/jobs/internal/process_stale_contacts_job.rb
@@ -0,0 +1,18 @@
+# housekeeping
+# remove stale contacts for all accounts
+# - have no identification (email, phone_number, and identifier are NULL)
+# - have no conversations
+# - are older than 30 days
+
+class Internal::ProcessStaleContactsJob < ApplicationJob
+ queue_as :scheduled_jobs
+
+ def perform
+ Account.find_in_batches(batch_size: 100) do |accounts|
+ accounts.each do |account|
+ Rails.logger.info "Enqueuing RemoveStaleContactsJob for account #{account.id}"
+ Internal::RemoveStaleContactsJob.perform_later(account)
+ end
+ end
+ end
+end
diff --git a/app/jobs/internal/remove_stale_contacts_job.rb b/app/jobs/internal/remove_stale_contacts_job.rb
new file mode 100644
index 000000000..3c33fd245
--- /dev/null
+++ b/app/jobs/internal/remove_stale_contacts_job.rb
@@ -0,0 +1,13 @@
+# housekeeping
+# remove contacts that:
+# - have no identification (email, phone_number, and identifier are NULL)
+# - have no conversations
+# - are older than 30 days
+
+class Internal::RemoveStaleContactsJob < ApplicationJob
+ queue_as :low
+
+ def perform(account, batch_size = 1000)
+ Internal::RemoveStaleContactsService.new(account: account).perform(batch_size)
+ end
+end
diff --git a/app/models/contact.rb b/app/models/contact.rb
index d9555f5fa..83920d9fc 100644
--- a/app/models/contact.rb
+++ b/app/models/contact.rb
@@ -128,6 +128,18 @@ class Contact < ApplicationRecord
)
}
+ # Find contacts that:
+ # 1. Have no identification (email, phone_number, and identifier are NULL or empty string)
+ # 2. Have no conversations
+ # 3. Are older than the specified time period
+ scope :stale_without_conversations, lambda { |time_period|
+ where('contacts.email IS NULL OR contacts.email = ?', '')
+ .where('contacts.phone_number IS NULL OR contacts.phone_number = ?', '')
+ .where('contacts.identifier IS NULL OR contacts.identifier = ?', '')
+ .where('contacts.created_at < ?', time_period)
+ .where.missing(:conversations)
+ }
+
def get_source_id(inbox_id)
contact_inboxes.find_by!(inbox_id: inbox_id).source_id
end
diff --git a/app/services/internal/remove_stale_contacts_service.rb b/app/services/internal/remove_stale_contacts_service.rb
new file mode 100644
index 000000000..74e189068
--- /dev/null
+++ b/app/services/internal/remove_stale_contacts_service.rb
@@ -0,0 +1,19 @@
+class Internal::RemoveStaleContactsService
+ pattr_initialize [:account!]
+
+ def perform(batch_size = 1000)
+ contacts_to_remove = @account.contacts.stale_without_conversations(30.days.ago)
+ total_deleted = 0
+
+ Rails.logger.info "[Internal::RemoveStaleContactsService] Starting removal of stale contacts for account #{@account.id}"
+
+ contacts_to_remove.find_in_batches(batch_size: batch_size) do |batch|
+ contact_ids = batch.map(&:id)
+
+ ContactInbox.where(contact_id: contact_ids).delete_all
+ Contact.where(id: contact_ids).delete_all
+ total_deleted += batch.size
+ Rails.logger.info "[Internal::RemoveStaleContactsService] Deleted #{batch.size} contacts (#{total_deleted} total) for account #{@account.id}"
+ end
+ end
+end
diff --git a/config/schedule.yml b/config/schedule.yml
index d8d23172b..f86015de6 100644
--- a/config/schedule.yml
+++ b/config/schedule.yml
@@ -32,3 +32,10 @@ remove_stale_redis_keys_job.rb:
cron: '30 22 * * *'
class: 'Internal::RemoveStaleRedisKeysJob'
queue: scheduled_jobs
+
+# executed daily at 2230 UTC
+# which is our lowest traffic time
+# process_stale_contacts_job:
+# cron: '30 22 * * *'
+# class: 'Internal::ProcessStaleContactsJob'
+# queue: scheduled_jobs
diff --git a/spec/jobs/internal/process_stale_contacts_job_spec.rb b/spec/jobs/internal/process_stale_contacts_job_spec.rb
new file mode 100644
index 000000000..30648ce9a
--- /dev/null
+++ b/spec/jobs/internal/process_stale_contacts_job_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+RSpec.describe Internal::ProcessStaleContactsJob do
+ subject(:job) { described_class.perform_later }
+
+ it 'enqueues the job' do
+ expect { job }.to have_enqueued_job(described_class)
+ .on_queue('scheduled_jobs')
+ end
+
+ it 'enqueues RemoveStaleContactsJob for each account' do
+ account1 = create(:account)
+ account2 = create(:account)
+ account3 = create(:account)
+
+ expect { described_class.perform_now }.to have_enqueued_job(Internal::RemoveStaleContactsJob)
+ .with(account1)
+ .on_queue('low')
+ expect { described_class.perform_now }.to have_enqueued_job(Internal::RemoveStaleContactsJob)
+ .with(account2)
+ .on_queue('low')
+ expect { described_class.perform_now }.to have_enqueued_job(Internal::RemoveStaleContactsJob)
+ .with(account3)
+ .on_queue('low')
+ end
+
+ it 'processes accounts in batches' do
+ account = create(:account)
+ allow(Account).to receive(:find_in_batches).with(batch_size: 100).and_yield([account])
+
+ expect(Internal::RemoveStaleContactsJob).to receive(:perform_later).with(account)
+ described_class.perform_now
+ end
+end
diff --git a/spec/jobs/internal/remove_stale_contacts_job_spec.rb b/spec/jobs/internal/remove_stale_contacts_job_spec.rb
new file mode 100644
index 000000000..ea2636114
--- /dev/null
+++ b/spec/jobs/internal/remove_stale_contacts_job_spec.rb
@@ -0,0 +1,20 @@
+require 'rails_helper'
+
+RSpec.describe Internal::RemoveStaleContactsJob do
+ subject(:job) { described_class.perform_later(account) }
+
+ let(:account) { create(:account) }
+
+ it 'enqueues the job' do
+ expect { job }.to have_enqueued_job(described_class)
+ .with(account)
+ .on_queue('low')
+ end
+
+ it 'calls the RemoveStaleContactsService' do
+ service = instance_double(Internal::RemoveStaleContactsService)
+ expect(Internal::RemoveStaleContactsService).to receive(:new).with(account: account).and_return(service)
+ expect(service).to receive(:perform)
+ described_class.perform_now(account)
+ end
+end
diff --git a/spec/services/internal/remove_stale_contacts_service_spec.rb b/spec/services/internal/remove_stale_contacts_service_spec.rb
new file mode 100644
index 000000000..d00c0e3ca
--- /dev/null
+++ b/spec/services/internal/remove_stale_contacts_service_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+
+RSpec.describe Internal::RemoveStaleContactsService do
+ describe '#perform' do
+ let(:account) { create(:account) }
+
+ it 'does not delete contacts with conversations' do
+ # Contact with NULL values and conversation
+ contact1 = create(:contact, account: account, email: nil, phone_number: nil, identifier: nil, created_at: 31.days.ago)
+ create(:conversation, contact: contact1)
+
+ # Contact with empty strings and conversation
+ contact2 = create(:contact, account: account, email: '', phone_number: '', identifier: '', created_at: 31.days.ago)
+ create(:conversation, contact: contact2)
+
+ service = described_class.new(account: account)
+ expect { service.perform }.not_to change(Contact, :count)
+ end
+
+ it 'does not delete contacts with identification' do
+ create(:contact, :with_email, account: account, phone_number: '', identifier: nil, created_at: 31.days.ago)
+ create(:contact, :with_phone_number, account: account, email: nil, identifier: '', created_at: 31.days.ago)
+ create(:contact, account: account, identifier: 'test123', created_at: 31.days.ago)
+
+ create(:contact, :with_email, account: account, phone_number: '', identifier: nil, created_at: 31.days.ago)
+ create(:contact, :with_phone_number, account: account, email: nil, identifier: nil, created_at: 31.days.ago)
+ create(:contact, account: account, email: '', phone_number: nil, identifier: 'test1234', created_at: 31.days.ago)
+
+ service = described_class.new(account: account)
+ expect { service.perform }.not_to change(Contact, :count)
+ end
+
+ it 'deletes stale contacts' do
+ create(:contact, account: account, created_at: 31.days.ago)
+ create(:contact, account: account, created_at: 1.day.ago)
+
+ service = described_class.new(account: account)
+ expect { service.perform }.to change(Contact, :count).by(-1)
+ end
+ end
+end
From 4e58a2a91d68f5259b720db2979430e5c64729a2 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Fri, 28 Mar 2025 14:58:17 +0530
Subject: [PATCH 011/554] feat: Upgrade page instead of banner (#11202)
# Pull Request Template
## Description
This PR will replace the upgrade banner with an upgrade page view.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/0f2b4b09acdd4404bf3211184a470227?sid=7ed60a99-0299-4642-b907-2af8c4dcc643
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav
---
app/helpers/billing_helper.rb | 4 +
app/javascript/dashboard/App.vue | 3 -
.../i18n/locale/en/generalSettings.json | 7 +
.../dashboard/routes/dashboard/Dashboard.vue | 47 ++++--
.../routes/dashboard/upgrade/UpgradePage.vue | 145 ++++++++++++++++++
app/policies/account_policy.rb | 2 +-
.../enterprise/api/v1/accounts_controller.rb | 45 +++---
.../api/v1/accounts_controller_spec.rb | 50 ++++--
8 files changed, 255 insertions(+), 48 deletions(-)
create mode 100644 app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue
diff --git a/app/helpers/billing_helper.rb b/app/helpers/billing_helper.rb
index 26669e38d..e2ada7e86 100644
--- a/app/helpers/billing_helper.rb
+++ b/app/helpers/billing_helper.rb
@@ -18,4 +18,8 @@ module BillingHelper
def non_web_inboxes(account)
account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count
end
+
+ def agents(account)
+ account.users.count
+ end
end
diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index ae50055fb..e51958e9e 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -4,7 +4,6 @@ import AddAccountModal from '../dashboard/components/layout/sidebarComponents/Ad
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue';
-import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import vueActionCable from './helper/actionCable';
@@ -31,7 +30,6 @@ export default {
UpdateBanner,
PaymentPendingBanner,
WootSnackbarBox,
- UpgradeBanner,
PendingEmailVerificationBanner,
},
setup() {
@@ -146,7 +144,6 @@ export default {
-
diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
index 9ad4e4b57..4e28e0b2f 100644
--- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json
+++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
@@ -1,5 +1,11 @@
{
"GENERAL_SETTINGS": {
+ "LIMIT_MESSAGES": {
+ "CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
+ "INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
+ "AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
+ "NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
+ },
"TITLE": "Account settings",
"SUBMIT": "Update settings",
"BACK": "Back",
@@ -51,6 +57,7 @@
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
+ "UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
"OPEN_BILLING": "Open billing"
},
diff --git a/app/javascript/dashboard/routes/dashboard/Dashboard.vue b/app/javascript/dashboard/routes/dashboard/Dashboard.vue
index 4d4c5e8df..7de36102b 100644
--- a/app/javascript/dashboard/routes/dashboard/Dashboard.vue
+++ b/app/javascript/dashboard/routes/dashboard/Dashboard.vue
@@ -1,6 +1,6 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('GENERAL_SETTINGS.UPGRADE') }}
+
+
+
+
+ {{ limitExceededMessage }}
+
+
+ {{ t('GENERAL_SETTINGS.LIMIT_MESSAGES.NON_ADMIN') }}
+
+
+
+
+
+
+
+
+
diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb
index a74a13a66..5eb80c1ab 100644
--- a/app/policies/account_policy.rb
+++ b/app/policies/account_policy.rb
@@ -8,7 +8,7 @@ class AccountPolicy < ApplicationPolicy
end
def limits?
- @account_user.administrator?
+ @account_user.administrator? || @account_user.agent?
end
def update?
diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb
index f449529b8..86ec2fb55 100644
--- a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb
+++ b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb
@@ -13,24 +13,24 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
end
def limits
- limits = {
- 'conversation' => {},
- 'non_web_inboxes' => {},
- 'captain' => @account.usage_limits[:captain]
- }
-
- if default_plan?(@account)
- limits = {
- 'conversation' => {
- 'allowed' => 500,
- 'consumed' => conversations_this_month(@account)
- },
- 'non_web_inboxes' => {
- 'allowed' => 0,
- 'consumed' => non_web_inboxes(@account)
- }
- }
- end
+ limits = if default_plan?(@account)
+ {
+ 'conversation' => {
+ 'allowed' => 500,
+ 'consumed' => conversations_this_month(@account)
+ },
+ 'non_web_inboxes' => {
+ 'allowed' => 0,
+ 'consumed' => non_web_inboxes(@account)
+ },
+ 'agents' => {
+ 'allowed' => 2,
+ 'consumed' => agents(@account)
+ }
+ }
+ else
+ default_limits
+ end
# include id in response to ensure that the store can be updated on the frontend
render json: { id: @account.id, limits: limits }, status: :ok
@@ -49,6 +49,15 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
private
+ def default_limits
+ {
+ 'conversation' => {},
+ 'non_web_inboxes' => {},
+ 'agents' => {},
+ 'captain' => @account.usage_limits[:captain]
+ }
+ end
+
def fetch_account
@account = current_user.accounts.find(params[:id])
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
diff --git a/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb b/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb
index 6373bc842..ac26dc525 100644
--- a/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb
+++ b/spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb
@@ -2,8 +2,8 @@ require 'rails_helper'
RSpec.describe 'Enterprise Billing APIs', type: :request do
let(:account) { create(:account) }
- let(:admin) { create(:user, account: account, role: :administrator) }
- let(:agent) { create(:user, account: account, role: :agent) }
+ let!(:admin) { create(:user, account: account, role: :administrator) }
+ let!(:agent) { create(:user, account: account, role: :agent) }
describe 'POST /enterprise/api/v1/accounts/{account.id}/subscription' do
context 'when it is an unauthenticated user' do
@@ -121,13 +121,36 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
end
context 'when it is an authenticated user' do
+ before do
+ InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'cloud')
+ InstallationConfig.where(name: 'CHATWOOT_CLOUD_PLANS').first_or_create(value: [{ 'name': 'Hacker' }])
+ end
+
context 'when it is an agent' do
it 'returns unauthorized' do
get "/enterprise/api/v1/accounts/#{account.id}/limits",
headers: agent.create_new_auth_token,
as: :json
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+ expect(json_response['id']).to eq(account.id)
+ expect(json_response['limits']).to eq(
+ {
+ 'conversation' => {
+ 'allowed' => 500,
+ 'consumed' => 0
+ },
+ 'non_web_inboxes' => {
+ 'allowed' => 0,
+ 'consumed' => 0
+ },
+ 'agents' => {
+ 'allowed' => 2,
+ 'consumed' => 2
+ }
+ }
+ )
end
end
@@ -155,6 +178,10 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
'non_web_inboxes' => {
'allowed' => 0,
'consumed' => 1
+ },
+ 'agents' => {
+ 'allowed' => 2,
+ 'consumed' => 2
}
}
}
@@ -172,18 +199,11 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
expected_response = {
'id' => account.id,
'limits' => {
+ 'agents' => {},
'conversation' => {},
'captain' => {
- 'documents' => {
- 'consumed' => 0,
- 'current_available' => ChatwootApp.max_limit,
- 'total_count' => ChatwootApp.max_limit
- },
- 'responses' => {
- 'consumed' => 0,
- 'current_available' => ChatwootApp.max_limit,
- 'total_count' => ChatwootApp.max_limit
- }
+ 'documents' => { 'consumed' => 0, 'current_available' => ChatwootApp.max_limit, 'total_count' => ChatwootApp.max_limit },
+ 'responses' => { 'consumed' => 0, 'current_available' => ChatwootApp.max_limit, 'total_count' => ChatwootApp.max_limit }
},
'non_web_inboxes' => {}
}
@@ -208,6 +228,10 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
'non_web_inboxes' => {
'allowed' => 0,
'consumed' => 1
+ },
+ 'agents' => {
+ 'allowed' => 2,
+ 'consumed' => 2
}
}
}
From 9fb3053007220eb3e74246ce18bf5116f1f042b6 Mon Sep 17 00:00:00 2001
From: Pranav
Date: Fri, 28 Mar 2025 14:07:03 -0700
Subject: [PATCH 012/554] fix: Add support for named parameter templates in
WhatsApp (#11198)
The expected payload on WhatsApp Cloud API is the following.
```json
{
"template": {
"name": "TEMPLATE_NAME",
"language": {
"code": "LANGUAGE_AND_LOCALE_CODE"
},
"components": [
"",
""
]
}
}
```
Named templates expect a `parameter_name`
```json
{
"type": "body",
"parameters": [
{
"type": "text",
"parameter_name": "customer_name",
"text": "John"
},
{
"type": "text",
"parameter_name": "order_id",
"text": "9128312831"
}
]
}
```
In this PR, we would check if the template is a name template, then we
would send the `parameter_name` as well.
Reference: https://github.com/chatwoot/chatwoot/issues/10886
---
.../contacts/contactable_inboxes_service.rb | 6 +--
.../whatsapp/send_on_whatsapp_service.rb | 23 +++++++++--
spec/factories/channel/channel_whatsapp.rb | 34 ++++++++++++++-
.../whatsapp/send_on_whatsapp_service_spec.rb | 41 +++++++++++++++++++
4 files changed, 97 insertions(+), 7 deletions(-)
diff --git a/app/services/contacts/contactable_inboxes_service.rb b/app/services/contacts/contactable_inboxes_service.rb
index c5cde516f..92d4160fe 100644
--- a/app/services/contacts/contactable_inboxes_service.rb
+++ b/app/services/contacts/contactable_inboxes_service.rb
@@ -42,20 +42,20 @@ class Contacts::ContactableInboxesService
end
def email_contactable_inbox(inbox)
- return unless @contact.email
+ return if @contact.email.blank?
{ source_id: @contact.email, inbox: inbox }
end
def whatsapp_contactable_inbox(inbox)
- return unless @contact.phone_number
+ return if @contact.phone_number.blank?
# Remove the plus since thats the format 360 dialog uses
{ source_id: @contact.phone_number.delete('+'), inbox: inbox }
end
def sms_contactable_inbox(inbox)
- return unless @contact.phone_number
+ return if @contact.phone_number.blank?
{ source_id: @contact.phone_number, inbox: inbox }
end
diff --git a/app/services/whatsapp/send_on_whatsapp_service.rb b/app/services/whatsapp/send_on_whatsapp_service.rb
index 3d843e64b..186c8b2ae 100644
--- a/app/services/whatsapp/send_on_whatsapp_service.rb
+++ b/app/services/whatsapp/send_on_whatsapp_service.rb
@@ -28,14 +28,13 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
message.update!(source_id: message_id) if message_id.present?
end
- # rubocop:disable Metrics/CyclomaticComplexity
def processable_channel_message_template
if template_params.present?
return [
template_params['name'],
template_params['namespace'],
template_params['language'],
- template_params['processed_params']&.map { |_, value| { type: 'text', text: value } }
+ processed_templates_params(template_params)
]
end
@@ -56,7 +55,6 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
end
[nil, nil, nil, nil]
end
- # rubocop:enable Metrics/CyclomaticComplexity
def template_match_object(template)
body_object = validated_body_object(template)
@@ -82,6 +80,25 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
Regexp.new template_match_string
end
+ def template(template_params)
+ channel.message_templates.find do |t|
+ t['name'] == template_params['name'] && t['language'] == template_params['language']
+ end
+ end
+
+ def processed_templates_params(template_params)
+ template = template(template_params)
+ return if template.blank?
+
+ parameter_format = template['parameter_format']
+
+ if parameter_format == 'NAMED'
+ template_params['processed_params']&.map { |key, value| { type: 'text', parameter_name: key, text: value } }
+ else
+ template_params['processed_params']&.map { |_, value| { type: 'text', text: value } }
+ end
+ end
+
def validated_body_object(template)
# we don't care if its not approved template
return if template['status'] != 'approved'
diff --git a/spec/factories/channel/channel_whatsapp.rb b/spec/factories/channel/channel_whatsapp.rb
index ccbbf7172..d437ed346 100644
--- a/spec/factories/channel/channel_whatsapp.rb
+++ b/spec/factories/channel/channel_whatsapp.rb
@@ -30,7 +30,39 @@ FactoryBot.define do
'components' =>
[{ 'text' => 'Your package has been shipped. It will be delivered in {{1}} business days.', 'type' => 'BODY' },
{ 'text' => 'This message is from an unverified business.', 'type' => 'FOOTER' }],
- 'rejected_reason' => 'NONE' }]
+ 'rejected_reason' => 'NONE' },
+ {
+ 'name' => 'ticket_status_updated',
+ 'status' => 'APPROVED',
+ 'category' => 'UTILITY',
+ 'language' => 'en',
+ 'components' => [
+ { 'text' => "Hello {{name}}, Your support ticket with ID: \#{{ticket_id}} has been updated by the support agent.",
+ 'type' => 'BODY',
+ 'example' => { 'body_text_named_params' => [
+ { 'example' => 'John', 'param_name' => 'name' },
+ { 'example' => '2332', 'param_name' => 'ticket_id' }
+ ] } }
+ ],
+ 'sub_category' => 'CUSTOM',
+ 'parameter_format' => 'NAMED'
+ },
+ {
+ 'name' => 'ticket_status_updated',
+ 'status' => 'APPROVED',
+ 'category' => 'UTILITY',
+ 'language' => 'en_US',
+ 'components' => [
+ { 'text' => "Hello {{last_name}}, Your support ticket with ID: \#{{ticket_id}} has been updated by the support agent.",
+ 'type' => 'BODY',
+ 'example' => { 'body_text_named_params' => [
+ { 'example' => 'Dale', 'param_name' => 'last_name' },
+ { 'example' => '2332', 'param_name' => 'ticket_id' }
+ ] } }
+ ],
+ 'sub_category' => 'CUSTOM',
+ 'parameter_format' => 'NAMED'
+ }]
end
message_templates_last_updated { Time.now.utc }
diff --git a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb
index 88f3f7740..a0e3e893a 100644
--- a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb
+++ b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb
@@ -18,6 +18,7 @@ describe Whatsapp::SendOnWhatsappService do
context 'when a valid message' do
let(:whatsapp_request) { instance_double(HTTParty::Response) }
let!(:whatsapp_channel) { create(:channel_whatsapp, sync_templates: false) }
+
let!(:contact_inbox) { create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '123456789') }
let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: whatsapp_channel.inbox) }
let(:api_key) { 'test_key' }
@@ -35,6 +36,21 @@ describe Whatsapp::SendOnWhatsappService do
}
end
+ let(:named_template_body) do
+ {
+ messaging_product: 'whatsapp',
+ to: '123456789',
+ template: {
+ name: 'ticket_status_updated',
+ language: { 'policy': 'deterministic', 'code': 'en_US' },
+ components: [{ 'type': 'body',
+ 'parameters': [{ 'type': 'text', parameter_name: 'last_name', 'text': 'Dale' },
+ { 'type': 'text', parameter_name: 'ticket_id', 'text': '2332' }] }]
+ },
+ type: 'template'
+ }
+ end
+
let(:success_response) { { 'messages' => [{ 'id' => '123456789' }] }.to_json }
it 'calls channel.send_message when with in 24 hour limit' do
@@ -82,6 +98,31 @@ describe Whatsapp::SendOnWhatsappService do
expect(message.reload.source_id).to eq('123456789')
end
+ it 'calls channel.send_template with named params if template parameter type is NAMED' do
+ whatsapp_cloud_channel = create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false)
+ cloud_contact_inbox = create(:contact_inbox, inbox: whatsapp_cloud_channel.inbox, source_id: '123456789')
+ cloud_conversation = create(:conversation, contact_inbox: cloud_contact_inbox, inbox: whatsapp_cloud_channel.inbox)
+
+ named_template_params = {
+ name: 'ticket_status_updated',
+ language: 'en_US',
+ category: 'UTILITY',
+ processed_params: { 'last_name' => 'Dale', 'ticket_id' => '2332' }
+ }
+
+ stub_request(:post, "https://graph.facebook.com/v13.0/#{whatsapp_cloud_channel.provider_config['phone_number_id']}/messages")
+ .with(
+ :headers => { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{whatsapp_cloud_channel.provider_config['api_key']}" },
+ :body => named_template_body.to_json
+ ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
+ message = create(:message,
+ additional_attributes: { template_params: named_template_params },
+ content: 'Your package will be delivered in 3 business days.', conversation: cloud_conversation, message_type: :outgoing)
+
+ described_class.new(message: message).perform
+ expect(message.reload.source_id).to eq('123456789')
+ end
+
it 'calls channel.send_template when template has regexp characters' do
message = create(
:message,
From cc4d54becf3090dde7258a52f17a3cf60bc82471 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Tue, 1 Apr 2025 05:51:32 +0530
Subject: [PATCH 013/554] chore: Update buttons in dashboard (#11145)
# Pull Request Template
## Changes
* Remove unused component `MaskedText.vue`
* Remove unused component `ContactIntro.vue`
* Remove unused `AddCustomViews.vue` component
* Update buttons in help center upgrade page
* Update SLA view details button in reports page
* Update assign to me conversation action button
* Update button in participants action
* Update the show more attributes button
* Update SLA empty state
* Update create new label button from dropdown
* Update add macro button
* Update copy button
* Update the buttons in banner component
* Update table pagination buttons
* Update filter chip buttons and dropdown colors
---------
Co-authored-by: Pranav
Co-authored-by: Muhsin Keloth
Co-authored-by: Shivam Mishra
---
.../components-next/banner/Banner.vue | 2 +
.../components-next/filter/SaveCustomView.vue | 10 +-
.../dashboard/components/MaskedText.vue | 64 ---------
.../dashboard/components/SidemenuIcon.vue | 16 ++-
.../app/PendingEmailVerificationBanner.vue | 2 +-
app/javascript/dashboard/components/index.js | 2 -
.../components/layout/AvailabilityStatus.vue | 22 +--
.../sidebarComponents/AccountContext.vue | 17 ++-
.../layout/sidebarComponents/AgentDetails.vue | 11 +-
.../layout/sidebarComponents/OptionsMenu.vue | 123 ++++++++++-------
.../sidebarComponents/SecondaryNavItem.vue | 27 ++--
.../specs/AccountSelector.spec.js | 1 -
.../specs/AgentDetails.spec.js | 6 +-
.../layout/specs/AvailabilityStatus.spec.js | 4 +-
.../components/specs/SidemenuIcon.spec.js | 2 +-
.../__snapshots__/SidemenuIcon.spec.js.snap | 10 +-
.../dashboard/components/ui/Banner.vue | 70 ++++------
.../components/ui/Dropdown/DropdownButton.vue | 45 ++----
.../ui/Dropdown/DropdownEmptyState.vue | 4 +-
.../components/ui/Dropdown/DropdownList.vue | 2 +-
.../ui/Dropdown/DropdownListItemButton.vue | 8 +-
.../ui/Dropdown/DropdownLoadingState.vue | 4 +-
.../components/ui/Dropdown/DropdownSearch.vue | 22 +--
.../dashboard/components/ui/WootButton.vue | 129 ------------------
.../widgets/TableFooterPagination.vue | 122 +++++++----------
.../widgets/conversation/ReplyBox.vue | 2 +-
.../widgets/conversation/bubble/Contact.vue | 13 +-
.../conversation/bubble/integrations/Dyte.vue | 32 ++---
.../conversation/LabelSuggestion.vue | 48 +++----
.../widgets/conversation/linear/LinkIssue.vue | 5 +-
.../linear/SearchableDropdown.vue | 5 +-
.../widgets/forms/AvatarUploader.vue | 19 +--
.../contact/components/ContactIntro.vue | 123 -----------------
.../components/MessageContextMenu.vue | 12 +-
.../conversation/ConversationAction.vue | 16 ++-
.../conversation/ConversationParticipant.vue | 35 +++--
.../dashboard/conversation/Macros/List.vue | 11 +-
.../conversation/contact/ContactInfoRow.vue | 16 +--
.../customAttributes/CustomAttributes.vue | 20 +--
.../conversation/search/PopOverSearch.vue | 3 +-
.../dashboard/customviews/AddCustomViews.vue | 114 ----------------
.../helpcenter/components/UpgradePage.vue | 25 ++--
.../components/NotificationPanel.vue | 91 ++++++------
.../components/NotificationPanelItem.vue | 13 +-
.../components/NotificationPanelList.vue | 15 +-
.../Filters/v3/ActiveFilterChip.vue | 3 +-
.../components/Filters/v3/AddFilterChip.vue | 6 +-
.../reports/components/SLA/SLAViewDetails.vue | 15 +-
.../settings/sla/components/SLAEmptyState.vue | 13 +-
.../shared/components/emoji/EmojiInput.vue | 92 +++++--------
.../components/ui/label/LabelDropdown.vue | 42 ++----
.../whatsappTemplates.spec.js | 2 +-
vitest.setup.js | 2 +-
53 files changed, 515 insertions(+), 1003 deletions(-)
delete mode 100644 app/javascript/dashboard/components/MaskedText.vue
delete mode 100644 app/javascript/dashboard/components/ui/WootButton.vue
delete mode 100644 app/javascript/dashboard/modules/contact/components/ContactIntro.vue
delete mode 100644 app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue
diff --git a/app/javascript/dashboard/components-next/banner/Banner.vue b/app/javascript/dashboard/components-next/banner/Banner.vue
index ed038d4bb..466c03be0 100644
--- a/app/javascript/dashboard/components-next/banner/Banner.vue
+++ b/app/javascript/dashboard/components-next/banner/Banner.vue
@@ -1,3 +1,5 @@
+
+
-
-
-
-
- {{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/SidemenuIcon.vue b/app/javascript/dashboard/components/SidemenuIcon.vue
index 59ef4c4c2..50bee3f17 100644
--- a/app/javascript/dashboard/components/SidemenuIcon.vue
+++ b/app/javascript/dashboard/components/SidemenuIcon.vue
@@ -3,12 +3,16 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { mapGetters } from 'vuex';
import { emitter } from 'shared/helpers/mitt';
+import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
+ components: {
+ NextButton,
+ },
props: {
size: {
type: String,
- default: 'small',
+ default: 'sm',
},
},
computed: {
@@ -33,13 +37,13 @@ export default {
-
diff --git a/app/javascript/dashboard/components/app/PendingEmailVerificationBanner.vue b/app/javascript/dashboard/components/app/PendingEmailVerificationBanner.vue
index a349786e6..e09f8368a 100644
--- a/app/javascript/dashboard/components/app/PendingEmailVerificationBanner.vue
+++ b/app/javascript/dashboard/components/app/PendingEmailVerificationBanner.vue
@@ -35,7 +35,7 @@ export default {
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
- action-button-icon="mail"
+ action-button-icon="i-lucide-mail"
has-action-button
@primary-action="resendVerificationEmail"
/>
diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js
index dbd827d57..2f807d2ef 100644
--- a/app/javascript/dashboard/components/index.js
+++ b/app/javascript/dashboard/components/index.js
@@ -1,7 +1,6 @@
// [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file.
/* eslint no-plusplus: 0 */
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
-import Button from './ui/WootButton.vue';
import Code from './Code.vue';
import ColorPicker from './widgets/ColorPicker.vue';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
@@ -26,7 +25,6 @@ import DatePicker from './ui/DatePicker/DatePicker.vue';
const WootUIKit = {
AvatarUploader,
- Button,
Code,
ColorPicker,
ConfirmDeleteModal,
diff --git a/app/javascript/dashboard/components/layout/AvailabilityStatus.vue b/app/javascript/dashboard/components/layout/AvailabilityStatus.vue
index 8bce09250..04190c7a4 100644
--- a/app/javascript/dashboard/components/layout/AvailabilityStatus.vue
+++ b/app/javascript/dashboard/components/layout/AvailabilityStatus.vue
@@ -7,6 +7,7 @@ import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
import wootConstants from 'dashboard/constants/globals';
+import NextButton from 'dashboard/components-next/button/Button.vue';
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
@@ -17,6 +18,7 @@ export default {
WootDropdownMenu,
WootDropdownItem,
AvailabilityStatusBadge,
+ NextButton,
},
data() {
return {
@@ -101,19 +103,21 @@ export default {
:key="status.value"
class="flex items-baseline"
>
-
- {{ status.label }}
-
+
+ {{ status.label }}
+
+
-
+
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue b/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue
index 83b2cc728..dc00ed03d 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue
@@ -1,7 +1,11 @@
-
-
+
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue b/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue
index 4c1367957..03549dad6 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue
@@ -5,12 +5,14 @@ import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
import { FEATURE_FLAGS } from '../../../featureFlags';
+import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
WootDropdownMenu,
WootDropdownItem,
AvailabilityStatus,
+ NextButton,
},
props: {
show: {
@@ -82,37 +84,46 @@ export default {
-
- {{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
-
+
+ {{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
+
+
-
- {{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
-
+
+ {{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
+
+
-
- {{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
-
+
+ {{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
+
+
handleProfileSettingClick(e, navigate)"
>
-
-
- {{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
-
+
+
+ {{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
+
+
-
- {{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
-
+
+ {{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
+
+
-
-
- {{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
-
+
+
+ {{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
+
+
-
- {{ $t('SIDEBAR_ITEMS.LOGOUT') }}
-
+
+ {{ $t('SIDEBAR_ITEMS.LOGOUT') }}
+
+
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
index a38bbd20b..6ca2ef8f8 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
@@ -13,9 +13,10 @@ import {
isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers';
import Policy from '../../policy.vue';
+import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
- components: { SecondaryChildNavItem, Policy },
+ components: { SecondaryChildNavItem, Policy, NextButton },
props: {
menuItem: {
type: Object,
@@ -205,14 +206,7 @@ export default {
{{ $t(`SIDEBAR.${menuItem.label}`) }}
-
+
- newLinkClick(e, navigate)"
- >
- {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
-
+ />
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js
index 2d93d3952..c819702b0 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js
@@ -41,7 +41,6 @@ describe('AccountSelector', () => {
'fluent-icon': FluentIcon,
},
stubs: {
- WootButton: { template: '' },
// override global stub
WootModalHeader: false,
},
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js
index 3ebb1f41f..7dd3e775b 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AgentDetails from '../AgentDetails.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
-import WootButton from 'dashboard/components/ui/WootButton.vue';
+import NextButton from 'dashboard/components-next/button/Button.vue';
describe('AgentDetails', () => {
const currentUser = {
@@ -40,12 +40,12 @@ describe('AgentDetails', () => {
plugins: [store],
components: {
Thumbnail,
- WootButton,
+ NextButton,
},
directives: {
tooltip: mockTooltipDirective, // Mocking the tooltip directive
},
- stubs: { WootButton: { template: '' } },
+ stubs: { NextButton: { template: '' } },
},
});
});
diff --git a/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js b/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js
index 2a9dbaba0..ccff4321a 100644
--- a/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js
+++ b/app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AvailabilityStatus from '../AvailabilityStatus.vue';
-import WootButton from 'dashboard/components/ui/WootButton.vue';
+import NextButton from 'dashboard/components-next/button/Button.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -40,7 +40,7 @@ describe('AvailabilityStatus', () => {
global: {
plugins: [store],
components: {
- WootButton,
+ NextButton,
WootDropdownItem,
WootDropdownMenu,
WootDropdownHeader,
diff --git a/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js b/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js
index 59cf8f4ec..dc946369c 100644
--- a/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js
+++ b/app/javascript/dashboard/components/specs/SidemenuIcon.spec.js
@@ -22,7 +22,7 @@ const store = createStore({
describe('SidemenuIcon', () => {
test('matches snapshot', () => {
const wrapper = shallowMount(SidemenuIcon, {
- stubs: { WootButton: { template: '' } },
+ stubs: { NextButton: { template: '' } },
global: { plugins: [store] },
});
expect(wrapper.vm).toBeTruthy();
diff --git a/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap b/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap
index 7e35f5717..a5ca2af6a 100644
--- a/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap
+++ b/app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap
@@ -2,11 +2,11 @@
exports[`SidemenuIcon > matches snapshot 1`] = `
diff --git a/app/javascript/dashboard/components/ui/Dropdown/DropdownEmptyState.vue b/app/javascript/dashboard/components/ui/Dropdown/DropdownEmptyState.vue
index 9c8e7155a..ee8d9dde9 100644
--- a/app/javascript/dashboard/components/ui/Dropdown/DropdownEmptyState.vue
+++ b/app/javascript/dashboard/components/ui/Dropdown/DropdownEmptyState.vue
@@ -8,9 +8,7 @@ defineProps({
-
+
{{ message }}
diff --git a/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue b/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue
index 8b77d54fb..9acd38728 100644
--- a/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue
+++ b/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue
@@ -78,7 +78,7 @@ const shouldShowEmptyState = computed(() => {
diff --git a/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue b/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue
index b766bc898..3f545835d 100644
--- a/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue
+++ b/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue
@@ -21,7 +21,7 @@ defineProps({
-
+
{{ buttonText }}
diff --git a/app/javascript/dashboard/components/ui/Dropdown/DropdownLoadingState.vue b/app/javascript/dashboard/components/ui/Dropdown/DropdownLoadingState.vue
index 9c8e7155a..ee8d9dde9 100644
--- a/app/javascript/dashboard/components/ui/Dropdown/DropdownLoadingState.vue
+++ b/app/javascript/dashboard/components/ui/Dropdown/DropdownLoadingState.vue
@@ -8,9 +8,7 @@ defineProps({
-
+
{{ message }}
diff --git a/app/javascript/dashboard/components/ui/Dropdown/DropdownSearch.vue b/app/javascript/dashboard/components/ui/Dropdown/DropdownSearch.vue
index 6c663ae6c..7e5bf569e 100644
--- a/app/javascript/dashboard/components/ui/Dropdown/DropdownSearch.vue
+++ b/app/javascript/dashboard/components/ui/Dropdown/DropdownSearch.vue
@@ -1,5 +1,7 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/widgets/TableFooterPagination.vue b/app/javascript/dashboard/components/widgets/TableFooterPagination.vue
index 6c9a38995..d3a8a4559 100644
--- a/app/javascript/dashboard/components/widgets/TableFooterPagination.vue
+++ b/app/javascript/dashboard/components/widgets/TableFooterPagination.vue
@@ -1,7 +1,7 @@
-
-
+
-
-
-
-
+
+
-
-
-
+ />
-
+
{{ currentPage }}
- /
-
+ /
+
{{ totalPages }}
-
-
-
-
-
+
+
-
-
+ />
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index c42b678fd..fd254c357 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -1074,7 +1074,7 @@ export default {
-
- {{ $t('CONVERSATION.SAVE_CONTACT') }}
-
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/integrations/Dyte.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/integrations/Dyte.vue
index b8368e422..0b41d00f0 100644
--- a/app/javascript/dashboard/components/widgets/conversation/bubble/integrations/Dyte.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/bubble/integrations/Dyte.vue
@@ -2,8 +2,12 @@
import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import { useAlert } from 'dashboard/composables';
+import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
+ components: {
+ NextButton,
+ },
props: {
messageId: {
type: Number,
@@ -41,31 +45,25 @@ export default {
-
- {{ $t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN') }}
-
+ />
-
- {{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
-
+ />
diff --git a/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue b/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue
index bb33d1ccc..aafd59f3e 100644
--- a/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/conversation/LabelSuggestion.vue
@@ -1,6 +1,6 @@
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue
index ca8a28f5c..5c051869a 100644
--- a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue
+++ b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue
@@ -13,12 +13,14 @@ import {
} from '../../../helper/AnalyticsHelper/events';
import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue';
import { useTrack } from 'dashboard/composables';
+import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
AddCannedModal,
MenuItem,
ContextMenu,
+ NextButton,
},
props: {
message: {
@@ -175,12 +177,12 @@ export default {
:confirm-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.DELETE')"
:reject-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.CANCEL')"
/>
-
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue
index 6f7c58ec5..b715c000a 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue
@@ -9,12 +9,14 @@ import ConversationLabels from './labels/LabelBox.vue';
import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { useTrack } from 'dashboard/composables';
+import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
ContactDetailsItem,
MultiselectDropdown,
ConversationLabels,
+ NextButton,
},
props: {
conversationId: {
@@ -212,15 +214,15 @@ export default {
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
>
-
- {{ $t('CONVERSATION_SIDEBAR.SELF_ASSIGN') }}
-
+ />
-
@@ -188,15 +191,15 @@ export default {
>
{{ $t('CONVERSATION_PARTICIPANTS.YOU_ARE_WATCHING') }}
-
- {{ $t('CONVERSATION_PARTICIPANTS.WATCH_CONVERSATION') }}
-
+ />
+
{
{{ $t('MACROS.LIST.404') }}
-
- {{ $t('MACROS.HEADER_BTN_TXT') }}
-
+
{{ $t('CONTACT_PANEL.NOT_AVAILABLE') }}
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue
index 4f0cf6340..3225f1bac 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue
@@ -9,6 +9,7 @@ import { useI18n } from 'vue-i18n';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
+import NextButton from 'dashboard/components-next/button/Button.vue';
const props = defineProps({
attributeType: {
@@ -318,17 +319,16 @@ const evenClass = [
{{ emptyStateMessage }}
-
-
+
- {{ toggleButtonText }}
-
+ />
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue b/app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue
index 3adb9910e..1c1627914 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex';
import SwitchLayout from './SwitchLayout.vue';
import { frontendURL } from 'dashboard/helper/URLHelper';
+
export default {
components: {
SwitchLayout,
@@ -37,7 +38,7 @@ export default {
class="flex px-4 pb-1 justify-between items-center flex-row gap-1 pt-2.5 border-b border-transparent"
>
-import { useVuelidate } from '@vuelidate/core';
-import { required, minLength } from '@vuelidate/validators';
-import { useAlert } from 'dashboard/composables';
-import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
-import { useTrack } from 'dashboard/composables';
-
-export default {
- props: {
- filterType: {
- type: Number,
- default: 0,
- },
- customViewsQuery: {
- type: Object,
- default: () => {},
- },
- openLastSavedItem: {
- type: Function,
- default: () => {},
- },
- },
- emits: ['close'],
- setup() {
- return { v$: useVuelidate() };
- },
- data() {
- return {
- show: true,
- name: '',
- };
- },
-
- computed: {
- isButtonDisabled() {
- return this.v$.name.$invalid;
- },
- },
-
- validations: {
- name: {
- required,
- minLength: minLength(1),
- },
- },
-
- methods: {
- onClose() {
- this.$emit('close');
- },
- async saveCustomViews() {
- this.v$.$touch();
- if (this.v$.$invalid) {
- return;
- }
- try {
- await this.$store.dispatch('customViews/create', {
- name: this.name,
- filter_type: this.filterType,
- query: this.customViewsQuery,
- });
- this.alertMessage =
- this.filterType === 0
- ? this.$t('FILTER.CUSTOM_VIEWS.ADD.API_FOLDERS.SUCCESS_MESSAGE')
- : this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.SUCCESS_MESSAGE');
- this.onClose();
-
- useTrack(CONTACTS_EVENTS.SAVE_FILTER, {
- type: this.filterType === 0 ? 'folder' : 'segment',
- });
- } catch (error) {
- const errorMessage = error?.message;
- this.alertMessage =
- errorMessage || this.filterType === 0
- ? errorMessage
- : this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.ERROR_MESSAGE');
- } finally {
- useAlert(this.alertMessage);
- }
- this.openLastSavedItem();
- },
- },
-};
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/UpgradePage.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/UpgradePage.vue
index eb57acf69..b498e4385 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/UpgradePage.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/UpgradePage.vue
@@ -1,7 +1,12 @@
-
+
import SLAPopoverCard from 'dashboard/components/widgets/conversation/components/SLAPopoverCard.vue';
+import NextButton from 'dashboard/components-next/button/Button.vue';
+
export default {
components: {
SLAPopoverCard,
+ NextButton,
},
props: {
slaEvents: {
@@ -34,13 +37,13 @@ export default {
class="flex items-center col-span-2 text-slate-11 justify-end"
>
-
- {{ $t('SLA_REPORTS.TABLE.VIEW_DETAILS') }}
-
+ />
import BaseEmptyState from './BaseEmptyState.vue';
+import NextButton from 'dashboard/components-next/button/Button.vue';
const emit = defineEmits(['primaryAction']);
const primaryAction = () => emit('primaryAction');
@@ -10,13 +11,11 @@ const primaryAction = () => emit('primaryAction');
{{ $t('SLA.LIST.404') }}
-
- {{ $t('SLA.ADD_ACTION_LONG') }}
-
+ />
diff --git a/app/javascript/shared/components/emoji/EmojiInput.vue b/app/javascript/shared/components/emoji/EmojiInput.vue
index 0672a4bb3..37917c6c3 100644
--- a/app/javascript/shared/components/emoji/EmojiInput.vue
+++ b/app/javascript/shared/components/emoji/EmojiInput.vue
@@ -1,11 +1,11 @@
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue b/app/javascript/dashboard/components/buttons/FormSubmitButton.vue
deleted file mode 100644
index 283387e7c..000000000
--- a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
- {{ buttonText }}
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue
index 7c7a9c262..4c537aaac 100644
--- a/app/javascript/dashboard/components/buttons/ResolveAction.vue
+++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue
@@ -134,7 +134,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
-
-
-
-
+
+ $emit('closeAccountCreateModal')"
+ />
+
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue
index 0b75be50b..23ef82b5e 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue
@@ -106,43 +106,3 @@ export default {
/>
-
-
diff --git a/app/javascript/dashboard/components/widgets/conversation/EmailTranscriptModal.vue b/app/javascript/dashboard/components/widgets/conversation/EmailTranscriptModal.vue
index a1c99b69e..5c1c79d23 100644
--- a/app/javascript/dashboard/components/widgets/conversation/EmailTranscriptModal.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/EmailTranscriptModal.vue
@@ -2,8 +2,12 @@
import { useVuelidate } from '@vuelidate/core';
import { required, minLength, email } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
+import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
+ components: {
+ NextButton,
+ },
props: {
show: {
type: Boolean,
@@ -153,13 +157,18 @@ export default {
-
+
-
- {{ $t('EMAIL_TRANSCRIPT.CANCEL') }}
-
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index 7108c1610..10a21245b 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -387,7 +387,8 @@
"LABEL": "Company Name",
"PLACEHOLDER": "Wayne Enterprises"
},
- "SUBMIT": "Submit"
+ "SUBMIT": "Submit",
+ "CANCEL": "Cancel"
}
},
"KEYBOARD_SHORTCUTS": {
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue
index 952d43c8e..810872cac 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue
@@ -1,7 +1,7 @@
-