fix: Prioritize SDK enableFileUpload flag when explicitly set (#13091)

### Problem
Currently, the attachment button visibility in the widget uses both the
SDK's `enableFileUpload` flag AND the inbox's attachment settings with
an AND condition. This creates an issue for users who want to control
attachments solely through inbox settings, since the SDK flag defaults
to `true` even when not explicitly provided.

  **Before:**
- SDK not set → `enableFileUpload: true` (default) + inbox setting =
attachment shown only if both true
- SDK set to false → `enableFileUpload: false` + inbox setting =
attachment always hidden
- SDK set to true → `enableFileUpload: true` + inbox setting =
attachment shown only if both true
  
This meant users couldn't rely solely on inbox settings when the SDK
flag wasn't explicitly provided.

  ### Solution

Changed the logic to prioritize explicit SDK configuration when
provided, and fall back to inbox settings when not provided:

  **After:**
  - SDK not set → `enableFileUpload: undefined` → use inbox setting only
- SDK set to false → `enableFileUpload: false` → hide attachment (SDK
controls)
- SDK set to true → `enableFileUpload: true` → show attachment (SDK
controls)

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Muhsin Keloth
2025-12-17 19:03:54 +05:30
committed by GitHub
parent cfa0bb953b
commit 116ed54c7e
6 changed files with 289 additions and 13 deletions

View File

@@ -76,7 +76,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
welcomeDescription: chatwootSettings.welcomeDescription || '', welcomeDescription: chatwootSettings.welcomeDescription || '',
availableMessage: chatwootSettings.availableMessage || '', availableMessage: chatwootSettings.availableMessage || '',
unavailableMessage: chatwootSettings.unavailableMessage || '', unavailableMessage: chatwootSettings.unavailableMessage || '',
enableFileUpload: chatwootSettings.enableFileUpload ?? true, enableFileUpload: chatwootSettings.enableFileUpload,
enableEmojiPicker: chatwootSettings.enableEmojiPicker ?? true, enableEmojiPicker: chatwootSettings.enableEmojiPicker ?? true,
enableEndConversation: chatwootSettings.enableEndConversation ?? true, enableEndConversation: chatwootSettings.enableEndConversation ?? true,

View File

@@ -11,6 +11,7 @@ import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import { DirectUpload } from 'activestorage'; import { DirectUpload } from 'activestorage';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import { useAttachments } from '../composables/useAttachments';
export default { export default {
components: { FluentIcon, FileUpload, Spinner }, components: { FluentIcon, FileUpload, Spinner },
@@ -20,13 +21,16 @@ export default {
default: () => {}, default: () => {},
}, },
}, },
setup() {
const { canHandleAttachments } = useAttachments();
return { canHandleAttachments };
},
data() { data() {
return { isUploading: false }; return { isUploading: false };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
globalConfig: 'globalConfig/get', globalConfig: 'globalConfig/get',
shouldShowFilePicker: 'appConfig/getShouldShowFilePicker',
}), }),
fileUploadSizeLimit() { fileUploadSizeLimit() {
return resolveMaximumFileUploadSize( return resolveMaximumFileUploadSize(
@@ -46,7 +50,7 @@ export default {
methods: { methods: {
handleClipboardPaste(e) { handleClipboardPaste(e) {
// If file picker is not enabled, do not allow paste // If file picker is not enabled, do not allow paste
if (!this.shouldShowFilePicker) return; if (!this.canHandleAttachments) return;
const items = (e.clipboardData || e.originalEvent.clipboardData).items; const items = (e.clipboardData || e.originalEvent.clipboardData).items;
// items is a DataTransferItemList object which does not have forEach method // items is a DataTransferItemList object which does not have forEach method

View File

@@ -3,7 +3,7 @@ import { mapGetters } from 'vuex';
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue'; import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
import ChatSendButton from 'widget/components/ChatSendButton.vue'; import ChatSendButton from 'widget/components/ChatSendButton.vue';
import configMixin from '../mixins/configMixin'; import { useAttachments } from '../composables/useAttachments';
import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue'; import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
@@ -18,7 +18,6 @@ export default {
FluentIcon, FluentIcon,
ResizableTextArea, ResizableTextArea,
}, },
mixins: [configMixin],
props: { props: {
onSendMessage: { onSendMessage: {
type: Function, type: Function,
@@ -29,6 +28,18 @@ export default {
default: () => {}, default: () => {},
}, },
}, },
setup() {
const {
canHandleAttachments,
shouldShowEmojiPicker,
hasEmojiPickerEnabled,
} = useAttachments();
return {
canHandleAttachments,
shouldShowEmojiPicker,
hasEmojiPickerEnabled,
};
},
data() { data() {
return { return {
userInput: '', userInput: '',
@@ -41,15 +52,10 @@ export default {
...mapGetters({ ...mapGetters({
widgetColor: 'appConfig/getWidgetColor', widgetColor: 'appConfig/getWidgetColor',
isWidgetOpen: 'appConfig/getIsWidgetOpen', isWidgetOpen: 'appConfig/getIsWidgetOpen',
shouldShowFilePicker: 'appConfig/getShouldShowFilePicker',
shouldShowEmojiPicker: 'appConfig/getShouldShowEmojiPicker', shouldShowEmojiPicker: 'appConfig/getShouldShowEmojiPicker',
}), }),
showAttachment() { showAttachment() {
return ( return this.canHandleAttachments && this.userInput.length === 0;
this.shouldShowFilePicker &&
this.hasAttachmentsEnabled &&
this.userInput.length === 0
);
}, },
showSendButton() { showSendButton() {
return this.userInput.length > 0; return this.userInput.length > 0;

View File

@@ -0,0 +1,224 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useAttachments } from '../useAttachments';
import { useStore } from 'vuex';
import { computed } from 'vue';
// Mock Vue's useStore
vi.mock('vuex', () => ({
useStore: vi.fn(),
}));
// Mock Vue's computed
vi.mock('vue', () => ({
computed: vi.fn(fn => ({ value: fn() })),
}));
describe('useAttachments', () => {
let mockStore;
let mockGetters;
beforeEach(() => {
// Reset window.chatwootWebChannel
delete window.chatwootWebChannel;
// Create mock store
mockGetters = {};
mockStore = {
getters: mockGetters,
};
vi.mocked(useStore).mockReturnValue(mockStore);
// Mock computed to return a reactive-like object
vi.mocked(computed).mockImplementation(fn => ({ value: fn() }));
});
afterEach(() => {
vi.clearAllMocks();
});
describe('shouldShowFilePicker', () => {
it('returns value from store getter', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = true;
const { shouldShowFilePicker } = useAttachments();
expect(shouldShowFilePicker.value).toBe(true);
});
it('returns undefined when not set in store', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = undefined;
const { shouldShowFilePicker } = useAttachments();
expect(shouldShowFilePicker.value).toBeUndefined();
});
});
describe('hasAttachmentsEnabled', () => {
it('returns true when attachments are enabled in channel config', () => {
window.chatwootWebChannel = {
enabledFeatures: ['attachments', 'emoji'],
};
const { hasAttachmentsEnabled } = useAttachments();
expect(hasAttachmentsEnabled.value).toBe(true);
});
it('returns false when attachments are not enabled in channel config', () => {
window.chatwootWebChannel = {
enabledFeatures: ['emoji'],
};
const { hasAttachmentsEnabled } = useAttachments();
expect(hasAttachmentsEnabled.value).toBe(false);
});
it('returns false when channel config has no enabled features', () => {
window.chatwootWebChannel = {
enabledFeatures: [],
};
const { hasAttachmentsEnabled } = useAttachments();
expect(hasAttachmentsEnabled.value).toBe(false);
});
it('returns false when channel config is missing', () => {
window.chatwootWebChannel = undefined;
const { hasAttachmentsEnabled } = useAttachments();
expect(hasAttachmentsEnabled.value).toBe(false);
});
it('returns false when enabledFeatures is missing', () => {
window.chatwootWebChannel = {};
const { hasAttachmentsEnabled } = useAttachments();
expect(hasAttachmentsEnabled.value).toBe(false);
});
});
describe('canHandleAttachments', () => {
beforeEach(() => {
// Set up a default channel config
window.chatwootWebChannel = {
enabledFeatures: ['attachments'],
};
});
it('prioritizes SDK flag when explicitly set to true', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = true;
const { canHandleAttachments } = useAttachments();
expect(canHandleAttachments.value).toBe(true);
});
it('prioritizes SDK flag when explicitly set to false', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = false;
const { canHandleAttachments } = useAttachments();
expect(canHandleAttachments.value).toBe(false);
});
it('falls back to inbox settings when SDK flag is undefined', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = undefined;
window.chatwootWebChannel = {
enabledFeatures: ['attachments'],
};
const { canHandleAttachments } = useAttachments();
expect(canHandleAttachments.value).toBe(true);
});
it('falls back to inbox settings when SDK flag is undefined and attachments disabled', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = undefined;
window.chatwootWebChannel = {
enabledFeatures: ['emoji'],
};
const { canHandleAttachments } = useAttachments();
expect(canHandleAttachments.value).toBe(false);
});
it('prioritizes SDK false over inbox settings true', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = false;
window.chatwootWebChannel = {
enabledFeatures: ['attachments'],
};
const { canHandleAttachments } = useAttachments();
expect(canHandleAttachments.value).toBe(false);
});
it('prioritizes SDK true over inbox settings false', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = true;
window.chatwootWebChannel = {
enabledFeatures: ['emoji'], // no attachments
};
const { canHandleAttachments } = useAttachments();
expect(canHandleAttachments.value).toBe(true);
});
});
describe('hasEmojiPickerEnabled', () => {
it('returns true when emoji picker is enabled in channel config', () => {
window.chatwootWebChannel = {
enabledFeatures: ['emoji_picker', 'attachments'],
};
const { hasEmojiPickerEnabled } = useAttachments();
expect(hasEmojiPickerEnabled.value).toBe(true);
});
it('returns false when emoji picker is not enabled in channel config', () => {
window.chatwootWebChannel = {
enabledFeatures: ['attachments'],
};
const { hasEmojiPickerEnabled } = useAttachments();
expect(hasEmojiPickerEnabled.value).toBe(false);
});
});
describe('shouldShowEmojiPicker', () => {
it('returns value from store getter', () => {
mockGetters['appConfig/getShouldShowEmojiPicker'] = true;
const { shouldShowEmojiPicker } = useAttachments();
expect(shouldShowEmojiPicker.value).toBe(true);
});
});
describe('integration test', () => {
it('returns all expected properties', () => {
mockGetters['appConfig/getShouldShowFilePicker'] = undefined;
mockGetters['appConfig/getShouldShowEmojiPicker'] = true;
window.chatwootWebChannel = {
enabledFeatures: ['attachments', 'emoji_picker'],
};
const result = useAttachments();
expect(result).toHaveProperty('shouldShowFilePicker');
expect(result).toHaveProperty('shouldShowEmojiPicker');
expect(result).toHaveProperty('hasAttachmentsEnabled');
expect(result).toHaveProperty('hasEmojiPickerEnabled');
expect(result).toHaveProperty('canHandleAttachments');
expect(Object.keys(result)).toHaveLength(5);
});
});
});

View File

@@ -0,0 +1,42 @@
import { computed } from 'vue';
import { useStore } from 'vuex';
export function useAttachments() {
const store = useStore();
const shouldShowFilePicker = computed(
() => store.getters['appConfig/getShouldShowFilePicker']
);
const shouldShowEmojiPicker = computed(
() => store.getters['appConfig/getShouldShowEmojiPicker']
);
const hasAttachmentsEnabled = computed(() => {
const channelConfig = window.chatwootWebChannel;
return channelConfig?.enabledFeatures?.includes('attachments') || false;
});
const hasEmojiPickerEnabled = computed(() => {
const channelConfig = window.chatwootWebChannel;
return channelConfig?.enabledFeatures?.includes('emoji_picker') || false;
});
const canHandleAttachments = computed(() => {
// If enableFileUpload was explicitly set via SDK, prioritize that
if (shouldShowFilePicker.value !== undefined) {
return shouldShowFilePicker.value;
}
// Otherwise, fall back to inbox settings only
return hasAttachmentsEnabled.value;
});
return {
shouldShowFilePicker,
shouldShowEmojiPicker,
hasAttachmentsEnabled,
hasEmojiPickerEnabled,
canHandleAttachments,
};
}

View File

@@ -25,7 +25,7 @@ const state = {
welcomeDescription: '', welcomeDescription: '',
availableMessage: '', availableMessage: '',
unavailableMessage: '', unavailableMessage: '',
enableFileUpload: true, enableFileUpload: undefined,
enableEmojiPicker: true, enableEmojiPicker: true,
enableEndConversation: true, enableEndConversation: true,
}; };
@@ -64,7 +64,7 @@ export const actions = {
welcomeDescription = '', welcomeDescription = '',
availableMessage = '', availableMessage = '',
unavailableMessage = '', unavailableMessage = '',
enableFileUpload = true, enableFileUpload = undefined,
enableEmojiPicker = true, enableEmojiPicker = true,
enableEndConversation = true, enableEndConversation = true,
} }