Compare commits

...
Author SHA1 Message Date
Shivam Mishra 670ab2400a feat: disable matches filter 2025-03-27 16:31:52 +05:30
Shivam Mishra 4a800eb8d1 chore: get changes from dbc1b492 2025-03-27 16:23:12 +05:30
Shivam Mishra 1bf1075427 feat: disable get filtered conversations 2025-03-27 16:16:22 +05:30
PranavandGitHub 4b4d9f8f7c fix: Update throttle for /meta endpoints, will call every 2s for small account, 10s for large accounts (#11190)
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
2025-03-26 14:59:39 -07:00
PranavandGitHub 49ee147fe3 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.
2025-03-26 11:11:01 -07:00
Muhsin KelothandGitHub d9450fde4a feat: Added Instagram channel migration (#11181)
This PR is part of https://github.com/chatwoot/chatwoot/pull/11054 to
make the review cycle easier.
2025-03-26 11:12:32 +05:30
PettersonandGitHub 157ec00994 fix(i18n): correct settings name translation in pt-BR (#11172)
# Pull Request Template

## Description

In pt_BR language, some strings are being mispelled, the correct word
would be "Configurações" but instead it is being showed as
"Confirgurações", which is wrong.
Fixes: https://github.com/chatwoot/chatwoot/issues/11149

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)


## How Has This Been Tested?

I've compiled the new translations and I compared the problematic
strings in PT_BR to check if it was still with the "Confirgurações" word
instead of "Configurações"

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
2025-03-25 20:27:41 -07:00
Sivin VargheseandGitHub d797fe34fb fix: Support Business hours when downloading the agent reports.
# Pull Request Template

## Description

This PR fixes an issue where downloaded agent conversation reports from
the Conversations page under Reports do not respect business hours.

Fixes
https://linear.app/chatwoot/issue/CW-4139/downloaded-agent-reports-do-not-respect-business-hours
https://github.com/chatwoot/chatwoot/issues/11057

## 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/43b94494647b48c3855476a227b02acb?sid=d6072725-11e5-487c-8aa5-8ecfae6dc818


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] 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
2025-03-25 20:26:18 -07:00
19 changed files with 232 additions and 31 deletions
+3 -1
View File
@@ -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
@@ -61,7 +61,7 @@ import {
getUserPermissions,
filterItemsByPermission,
} from 'dashboard/helper/permissionsHelper.js';
import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
// import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
@@ -106,7 +106,7 @@ const advancedFilterTypes = ref(
);
const currentUser = useMapGetter('getCurrentUser');
const chatLists = useMapGetter('getFilteredConversations');
const chatLists = useMapGetter('getAllConversations');
const mineChatsList = useMapGetter('getMineChats');
const allChatList = useMapGetter('getAllStatusChats');
const unAssignedChatsList = useMapGetter('getUnAssignedChats');
@@ -326,12 +326,12 @@ const conversationList = computed(() => {
localConversationList = [...chatLists.value];
}
if (activeFolder.value) {
const { payload } = activeFolder.value.query;
localConversationList = localConversationList.filter(conversation => {
return matchesFilters(conversation, payload);
});
}
// if (activeFolder.value) {
// const { payload } = activeFolder.value.query;
// localConversationList = localConversationList.filter(conversation => {
// return matchesFilters(conversation, payload);
// });
// }
return localConversationList;
});
@@ -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();
@@ -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);
});
});
});
@@ -100,7 +100,7 @@
"NO": "cancelar"
}
},
"SETTINGS": "Confirgurações",
"SETTINGS": "Configurações",
"FORM": {
"UPDATE": "Atualizar a equipa",
"CREATE": "Criar uma equipa",
@@ -9,7 +9,7 @@
"FILTER": "Filtrar por",
"SORT": "Classificar por",
"LOCALE": "Localidade",
"SETTINGS_BUTTON": "Confirgurações",
"SETTINGS_BUTTON": "Configurações",
"NEW_BUTTON": "Novo artigo",
"DROPDOWN_OPTIONS": {
"PUBLISHED": "Publicado",
@@ -121,7 +121,7 @@
"COUNT_LABEL": "artigos",
"ADD": "Adicionar localidade",
"VISIT": "Visitar site",
"SETTINGS": "Confirgurações",
"SETTINGS": "Configurações",
"DELETE": "Excluir"
},
"PORTAL_CONFIG": {
@@ -473,7 +473,7 @@
"WIDGET_BUILDER": "Construtor de Widget",
"BOT_CONFIGURATION": "Configuração do Bot"
},
"SETTINGS": "Confirgurações",
"SETTINGS": "Configurações",
"FEATURES": {
"LABEL": "Funcionalidades",
"DISPLAY_FILE_PICKER": "Exibir seletor de arquivos no widget",
@@ -333,7 +333,7 @@
"ARTICLES": "Artigos",
"CATEGORIES": "Categorias",
"LOCALES": "Localidades",
"SETTINGS": "Confirgurações"
"SETTINGS": "Configurações"
},
"CHANNELS": "Canais",
"SET_AUTO_OFFLINE": {
@@ -100,7 +100,7 @@
"NO": "Cancelar"
}
},
"SETTINGS": "Confirgurações",
"SETTINGS": "Configurações",
"FORM": {
"UPDATE": "Atualizar equipe",
"CREATE": "Criar nova equipe",
@@ -1,11 +1,10 @@
<script>
import V4Button from 'dashboard/components-next/button/Button.vue';
import { useAlert, useTrack } from 'dashboard/composables';
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
import ReportFilterSelector from './components/FilterSelector.vue';
import { GROUP_BY_FILTER } from './constants';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import { generateFileName } from 'dashboard/helper/downloadHelper';
import ReportContainer from './ReportContainer.vue';
import ReportHeader from './components/ReportHeader.vue';
@@ -79,11 +78,17 @@ export default {
},
downloadAgentReports() {
const { from, to } = this;
const fileName = `agent-report-${format(
fromUnixTime(to),
'dd-MM-yyyy'
)}.csv`;
this.$store.dispatch('downloadAgentReports', { from, to, fileName });
const fileName = generateFileName({
type: 'agent',
to,
businessHours: this.businessHours,
});
this.$store.dispatch('downloadAgentReports', {
from,
to,
fileName,
businessHours: this.businessHours,
});
},
onFilterChange({ from, to, groupBy, businessHours }) {
this.from = from;
@@ -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 = 500;
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);
@@ -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);
});
});
+1 -1
View File
@@ -10,7 +10,7 @@
html,
body {
@apply antialiased h-full bg-n-background;
@apply antialiased h-full;
}
.is-mobile {
+1
View File
@@ -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
+28
View File
@@ -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
@@ -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
+11 -1
View File
@@ -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
@@ -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
+17
View File
@@ -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