Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
670ab2400a | ||
|
|
4a800eb8d1 | ||
|
|
1bf1075427 | ||
|
|
4b4d9f8f7c | ||
|
|
49ee147fe3 | ||
|
|
d9450fde4a | ||
|
|
157ec00994 | ||
|
|
d797fe34fb |
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply antialiased h-full bg-n-background;
|
||||
@apply antialiased h-full;
|
||||
}
|
||||
|
||||
.is-mobile {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user