From 116ed54c7ee38541c35a0ff46c8cd991309f653e Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 17 Dec 2025 19:03:54 +0530 Subject: [PATCH] fix: Prioritize SDK `enableFileUpload` flag when explicitly set (#13091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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> --- app/javascript/entrypoints/sdk.js | 2 +- .../widget/components/ChatAttachment.vue | 8 +- .../widget/components/ChatInputWrap.vue | 22 +- .../composables/specs/useAttachments.spec.js | 224 ++++++++++++++++++ .../widget/composables/useAttachments.js | 42 ++++ .../widget/store/modules/appConfig.js | 4 +- 6 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 app/javascript/widget/composables/specs/useAttachments.spec.js create mode 100644 app/javascript/widget/composables/useAttachments.js diff --git a/app/javascript/entrypoints/sdk.js b/app/javascript/entrypoints/sdk.js index 2a919f42c..6e1639b1c 100755 --- a/app/javascript/entrypoints/sdk.js +++ b/app/javascript/entrypoints/sdk.js @@ -76,7 +76,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { welcomeDescription: chatwootSettings.welcomeDescription || '', availableMessage: chatwootSettings.availableMessage || '', unavailableMessage: chatwootSettings.unavailableMessage || '', - enableFileUpload: chatwootSettings.enableFileUpload ?? true, + enableFileUpload: chatwootSettings.enableFileUpload, enableEmojiPicker: chatwootSettings.enableEmojiPicker ?? true, enableEndConversation: chatwootSettings.enableEndConversation ?? true, diff --git a/app/javascript/widget/components/ChatAttachment.vue b/app/javascript/widget/components/ChatAttachment.vue index 169749527..64c17e9c6 100755 --- a/app/javascript/widget/components/ChatAttachment.vue +++ b/app/javascript/widget/components/ChatAttachment.vue @@ -11,6 +11,7 @@ import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import { DirectUpload } from 'activestorage'; import { mapGetters } from 'vuex'; import { emitter } from 'shared/helpers/mitt'; +import { useAttachments } from '../composables/useAttachments'; export default { components: { FluentIcon, FileUpload, Spinner }, @@ -20,13 +21,16 @@ export default { default: () => {}, }, }, + setup() { + const { canHandleAttachments } = useAttachments(); + return { canHandleAttachments }; + }, data() { return { isUploading: false }; }, computed: { ...mapGetters({ globalConfig: 'globalConfig/get', - shouldShowFilePicker: 'appConfig/getShouldShowFilePicker', }), fileUploadSizeLimit() { return resolveMaximumFileUploadSize( @@ -46,7 +50,7 @@ export default { methods: { handleClipboardPaste(e) { // 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; // items is a DataTransferItemList object which does not have forEach method diff --git a/app/javascript/widget/components/ChatInputWrap.vue b/app/javascript/widget/components/ChatInputWrap.vue index c423ab220..ce8d17455 100755 --- a/app/javascript/widget/components/ChatInputWrap.vue +++ b/app/javascript/widget/components/ChatInputWrap.vue @@ -3,7 +3,7 @@ import { mapGetters } from 'vuex'; import ChatAttachmentButton from 'widget/components/ChatAttachment.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 ResizableTextArea from 'shared/components/ResizableTextArea.vue'; @@ -18,7 +18,6 @@ export default { FluentIcon, ResizableTextArea, }, - mixins: [configMixin], props: { onSendMessage: { type: Function, @@ -29,6 +28,18 @@ export default { default: () => {}, }, }, + setup() { + const { + canHandleAttachments, + shouldShowEmojiPicker, + hasEmojiPickerEnabled, + } = useAttachments(); + return { + canHandleAttachments, + shouldShowEmojiPicker, + hasEmojiPickerEnabled, + }; + }, data() { return { userInput: '', @@ -41,15 +52,10 @@ export default { ...mapGetters({ widgetColor: 'appConfig/getWidgetColor', isWidgetOpen: 'appConfig/getIsWidgetOpen', - shouldShowFilePicker: 'appConfig/getShouldShowFilePicker', shouldShowEmojiPicker: 'appConfig/getShouldShowEmojiPicker', }), showAttachment() { - return ( - this.shouldShowFilePicker && - this.hasAttachmentsEnabled && - this.userInput.length === 0 - ); + return this.canHandleAttachments && this.userInput.length === 0; }, showSendButton() { return this.userInput.length > 0; diff --git a/app/javascript/widget/composables/specs/useAttachments.spec.js b/app/javascript/widget/composables/specs/useAttachments.spec.js new file mode 100644 index 000000000..578fd3ae8 --- /dev/null +++ b/app/javascript/widget/composables/specs/useAttachments.spec.js @@ -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); + }); + }); +}); diff --git a/app/javascript/widget/composables/useAttachments.js b/app/javascript/widget/composables/useAttachments.js new file mode 100644 index 000000000..5ffab60a6 --- /dev/null +++ b/app/javascript/widget/composables/useAttachments.js @@ -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, + }; +} diff --git a/app/javascript/widget/store/modules/appConfig.js b/app/javascript/widget/store/modules/appConfig.js index 3ad5078b8..5b720d907 100644 --- a/app/javascript/widget/store/modules/appConfig.js +++ b/app/javascript/widget/store/modules/appConfig.js @@ -25,7 +25,7 @@ const state = { welcomeDescription: '', availableMessage: '', unavailableMessage: '', - enableFileUpload: true, + enableFileUpload: undefined, enableEmojiPicker: true, enableEndConversation: true, }; @@ -64,7 +64,7 @@ export const actions = { welcomeDescription = '', availableMessage = '', unavailableMessage = '', - enableFileUpload = true, + enableFileUpload = undefined, enableEmojiPicker = true, enableEndConversation = true, }