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, }