feat: allow configuring attachment upload limit (#12835)

## Summary
- add a configurable MAXIMUM_FILE_UPLOAD_SIZE installation setting and
surface it through super admin and global config payloads
- apply the configurable limit to attachment validations and shared
upload helpers on dashboard and widget
- introduce a reusable helper with unit tests for parsing the limit and
extend attachment specs for configurability


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912644786b08326bc8dee9401af6d0a)

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
Sojan Jose
2025-11-17 14:03:08 -08:00
committed by GitHub
parent 93374f4327
commit bf806f0c28
16 changed files with 204 additions and 46 deletions

View File

@@ -12,7 +12,11 @@ vi.mock('dashboard/composables', () => ({
}));
vi.mock('vue-i18n');
vi.mock('activestorage');
vi.mock('shared/helpers/FileHelper');
vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: vi.fn(),
resolveMaximumFileUploadSize: vi.fn(value => Number(value) || 40),
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE: 40,
}));
vi.mock('@chatwoot/utils');
describe('useFileUpload', () => {
@@ -36,7 +40,9 @@ describe('useFileUpload', () => {
getCurrentAccountId: { value: '123' },
getCurrentUser: { value: { access_token: 'test-token' } },
getSelectedChat: { value: { id: '456' } },
'globalConfig/get': { value: { directUploadsEnabled: true } },
'globalConfig/get': {
value: { directUploadsEnabled: true, maximumFileUploadSize: 40 },
},
};
return getterMap[getter];
});
@@ -86,7 +92,9 @@ describe('useFileUpload', () => {
getCurrentAccountId: { value: '123' },
getCurrentUser: { value: { access_token: 'test-token' } },
getSelectedChat: { value: { id: '456' } },
'globalConfig/get': { value: { directUploadsEnabled: false } },
'globalConfig/get': {
value: { directUploadsEnabled: false, maximumFileUploadSize: 40 },
},
};
return getterMap[getter];
});

View File

@@ -4,7 +4,11 @@ import { useI18n } from 'vue-i18n';
import { DirectUpload } from 'activestorage';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import {
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
resolveMaximumFileUploadSize,
} from 'shared/helpers/FileHelper';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
/**
* Composable for handling file uploads in conversations
@@ -21,18 +25,34 @@ export const useFileUpload = ({ inbox, attachFile, isPrivateNote = false }) => {
const currentChat = useMapGetter('getSelectedChat');
const globalConfig = useMapGetter('globalConfig/get');
const installationLimit = resolveMaximumFileUploadSize(
globalConfig.value?.maximumFileUploadSize
);
// helper: compute max upload size for a given file's mime
const maxSizeFor = mime => {
// Use default file size limit for private notes
// Use default/installation limit for private notes
if (isPrivateNote) {
return MAXIMUM_FILE_UPLOAD_SIZE;
return installationLimit;
}
return getMaxUploadSizeByChannel({
channelType: inbox?.channel_type,
const channelType = inbox?.channel_type;
if (!channelType || channelType === INBOX_TYPES.WEB) {
return installationLimit;
}
const channelLimit = getMaxUploadSizeByChannel({
channelType,
medium: inbox?.medium, // e.g. 'sms' | 'whatsapp' | etc.
mime, // e.g. 'image/png'
});
if (channelLimit === DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE) {
return installationLimit;
}
return Math.min(channelLimit, installationLimit);
};
const alertOverLimit = maxSizeMB =>

View File

@@ -3,27 +3,48 @@ import { useAlert } from 'dashboard/composables';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
import { DirectUpload } from 'activestorage';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import {
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
resolveMaximumFileUploadSize,
} from 'shared/helpers/FileHelper';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
}),
installationLimit() {
return resolveMaximumFileUploadSize(
this.globalConfig.maximumFileUploadSize
);
},
},
methods: {
maxSizeFor(mime) {
// Use default file size limit for private notes
// Use default/installation limit for private notes
if (this.isOnPrivateNote) {
return MAXIMUM_FILE_UPLOAD_SIZE;
return this.installationLimit;
}
return getMaxUploadSizeByChannel({
channelType: this.inbox?.channel_type,
const channelType = this.inbox?.channel_type;
if (!channelType || channelType === INBOX_TYPES.WEB) {
return this.installationLimit;
}
const channelLimit = getMaxUploadSizeByChannel({
channelType,
medium: this.inbox?.medium, // e.g. 'sms' | 'whatsapp'
mime, // e.g. 'image/png'
});
if (channelLimit === DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE) {
return this.installationLimit;
}
return Math.min(channelLimit, this.installationLimit);
},
alertOverLimit(maxSizeMB) {
useAlert(

View File

@@ -6,6 +6,8 @@ import { reactive } from 'vue';
vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: vi.fn(),
resolveMaximumFileUploadSize: vi.fn(value => Number(value) || 40),
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE: 40,
}));
vi.mock('activestorage', () => ({
@@ -27,6 +29,7 @@ describe('FileUploadMixin', () => {
beforeEach(() => {
mockGlobalConfig = reactive({
directUploadsEnabled: true,
maximumFileUploadSize: 40,
});
mockCurrentChat = reactive({