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 image --------- 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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.4.12&new-version=5.4.15)](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 @@ + +