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:
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
224
app/javascript/widget/composables/specs/useAttachments.spec.js
Normal file
224
app/javascript/widget/composables/specs/useAttachments.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
42
app/javascript/widget/composables/useAttachments.js
Normal file
42
app/javascript/widget/composables/useAttachments.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user