Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { ref } from 'vue';
|
||||
import { useAvailability } from '../useAvailability';
|
||||
|
||||
const mockIsOnline = vi.fn();
|
||||
const mockIsInWorkingHours = vi.fn();
|
||||
const mockUseCamelCase = vi.fn(obj => obj);
|
||||
|
||||
vi.mock('widget/helpers/availabilityHelpers', () => ({
|
||||
isOnline: (...args) => mockIsOnline(...args),
|
||||
isInWorkingHours: (...args) => mockIsInWorkingHours(...args),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables/useTransformKeys', () => ({
|
||||
useCamelCase: obj => mockUseCamelCase(obj),
|
||||
}));
|
||||
|
||||
describe('useAvailability', () => {
|
||||
const originalWindow = window.chatwootWebChannel;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Reset mocks to return true by default
|
||||
mockIsOnline.mockReturnValue(true);
|
||||
mockIsInWorkingHours.mockReturnValue(true);
|
||||
mockUseCamelCase.mockImplementation(obj => obj);
|
||||
|
||||
window.chatwootWebChannel = {
|
||||
workingHours: [],
|
||||
workingHoursEnabled: false,
|
||||
timezone: 'UTC',
|
||||
utcOffset: 'UTC',
|
||||
replyTime: 'in_a_few_minutes',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.chatwootWebChannel = originalWindow;
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const { availableAgents, hasOnlineAgents, isInWorkingHours, isOnline } =
|
||||
useAvailability();
|
||||
|
||||
expect(availableAgents.value).toEqual([]);
|
||||
expect(hasOnlineAgents.value).toBe(false);
|
||||
expect(isInWorkingHours.value).toBe(true);
|
||||
expect(isOnline.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with agents', () => {
|
||||
it('should handle agents array', () => {
|
||||
const agents = [{ id: 1 }, { id: 2 }];
|
||||
const { availableAgents, hasOnlineAgents } = useAvailability(agents);
|
||||
|
||||
expect(availableAgents.value).toEqual(agents);
|
||||
expect(hasOnlineAgents.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle reactive agents', () => {
|
||||
const agents = ref([{ id: 1 }]);
|
||||
const { hasOnlineAgents } = useAvailability(agents);
|
||||
|
||||
expect(hasOnlineAgents.value).toBe(true);
|
||||
|
||||
agents.value = [];
|
||||
expect(hasOnlineAgents.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('working hours', () => {
|
||||
const workingHours = [{ dayOfWeek: 1, openHour: 9, closeHour: 17 }];
|
||||
|
||||
beforeEach(() => {
|
||||
window.chatwootWebChannel = {
|
||||
workingHours,
|
||||
workingHoursEnabled: true,
|
||||
utcOffset: '+05:30',
|
||||
};
|
||||
});
|
||||
|
||||
it('should check working hours', () => {
|
||||
mockIsInWorkingHours.mockReturnValueOnce(true);
|
||||
const { isInWorkingHours } = useAvailability();
|
||||
const result = isInWorkingHours.value;
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockIsInWorkingHours).toHaveBeenCalledWith(
|
||||
expect.any(Date),
|
||||
'+05:30',
|
||||
workingHours
|
||||
);
|
||||
});
|
||||
|
||||
it('should determine online status based on working hours and agents', () => {
|
||||
mockIsOnline.mockReturnValueOnce(true);
|
||||
const { isOnline } = useAvailability([{ id: 1 }]);
|
||||
const result = isOnline.value;
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockIsOnline).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.any(Date),
|
||||
'+05:30',
|
||||
workingHours,
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config changes', () => {
|
||||
it('should react to window.chatwootWebChannel changes', () => {
|
||||
const { inboxConfig } = useAvailability();
|
||||
|
||||
window.chatwootWebChannel = {
|
||||
...window.chatwootWebChannel,
|
||||
replyTime: 'in_a_day',
|
||||
};
|
||||
|
||||
expect(inboxConfig.value.replyTime).toBe('in_a_day');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useDarkMode } from '../useDarkMode';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
vi.mock('dashboard/composables/store', () => ({
|
||||
useMapGetter: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useDarkMode', () => {
|
||||
let mockDarkMode;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDarkMode = { value: 'light' };
|
||||
vi.mocked(useMapGetter).mockReturnValue(mockDarkMode);
|
||||
});
|
||||
|
||||
it('returns darkMode, prefersDarkMode', () => {
|
||||
const result = useDarkMode();
|
||||
expect(result).toHaveProperty('darkMode');
|
||||
expect(result).toHaveProperty('prefersDarkMode');
|
||||
});
|
||||
|
||||
describe('prefersDarkMode', () => {
|
||||
it('returns false when darkMode is light', () => {
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
expect(prefersDarkMode.value).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when darkMode is dark', () => {
|
||||
mockDarkMode.value = 'dark';
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
expect(prefersDarkMode.value).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when darkMode is auto and OS prefers dark mode', () => {
|
||||
mockDarkMode.value = 'auto';
|
||||
vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: true });
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
expect(prefersDarkMode.value).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when darkMode is auto and OS prefers light mode', () => {
|
||||
mockDarkMode.value = 'auto';
|
||||
vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: false });
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
expect(prefersDarkMode.value).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { computed, unref } from 'vue';
|
||||
import {
|
||||
isOnline as checkIsOnline,
|
||||
isInWorkingHours as checkInWorkingHours,
|
||||
} from 'widget/helpers/availabilityHelpers';
|
||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
|
||||
const DEFAULT_TIMEZONE = 'UTC';
|
||||
const DEFAULT_REPLY_TIME = 'in_a_few_minutes';
|
||||
|
||||
/**
|
||||
* Composable for availability-related logic
|
||||
* @param {Ref|Array} agents - Available agents (can be ref or raw array)
|
||||
* @returns {Object} Availability utilities and computed properties
|
||||
*/
|
||||
export function useAvailability(agents = []) {
|
||||
// Now receives toRef(props, 'agents') from caller, which maintains reactivity.
|
||||
// Use unref() inside computed to unwrap the ref value properly.
|
||||
// This ensures availableAgents updates when the parent's agents prop changes
|
||||
// (e.g., after API response updates the Vuex store).
|
||||
const availableAgents = computed(() => unref(agents));
|
||||
|
||||
const channelConfig = computed(() => window.chatwootWebChannel || {});
|
||||
|
||||
const inboxConfig = computed(() => ({
|
||||
workingHours: channelConfig.value.workingHours?.map(useCamelCase) || [],
|
||||
workingHoursEnabled: channelConfig.value.workingHoursEnabled || false,
|
||||
timezone: channelConfig.value.timezone || DEFAULT_TIMEZONE,
|
||||
utcOffset:
|
||||
channelConfig.value.utcOffset ||
|
||||
channelConfig.value.timezone ||
|
||||
DEFAULT_TIMEZONE,
|
||||
replyTime: channelConfig.value.replyTime || DEFAULT_REPLY_TIME,
|
||||
}));
|
||||
|
||||
const currentTime = computed(() => new Date());
|
||||
|
||||
const hasOnlineAgents = computed(() => {
|
||||
const agentList = availableAgents.value || [];
|
||||
return Array.isArray(agentList) ? agentList.length > 0 : false;
|
||||
});
|
||||
|
||||
const isInWorkingHours = computed(() =>
|
||||
checkInWorkingHours(
|
||||
currentTime.value,
|
||||
inboxConfig.value.utcOffset,
|
||||
inboxConfig.value.workingHours
|
||||
)
|
||||
);
|
||||
|
||||
// Check if online (considering both working hours and agents)
|
||||
const isOnline = computed(() =>
|
||||
checkIsOnline(
|
||||
inboxConfig.value.workingHoursEnabled,
|
||||
currentTime.value,
|
||||
inboxConfig.value.utcOffset,
|
||||
inboxConfig.value.workingHours,
|
||||
hasOnlineAgents.value
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
channelConfig,
|
||||
inboxConfig,
|
||||
|
||||
currentTime,
|
||||
availableAgents,
|
||||
hasOnlineAgents,
|
||||
|
||||
isOnline,
|
||||
isInWorkingHours,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const isDarkModeAuto = mode => mode === 'auto';
|
||||
const isDarkMode = mode => mode === 'dark';
|
||||
|
||||
const getSystemPreference = () =>
|
||||
window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
||||
|
||||
const calculatePrefersDarkMode = (mode, systemPreference) =>
|
||||
isDarkModeAuto(mode) ? systemPreference : isDarkMode(mode);
|
||||
|
||||
/**
|
||||
* Composable for handling dark mode.
|
||||
* @returns {Object} An object containing computed properties and methods for dark mode.
|
||||
*/
|
||||
export function useDarkMode() {
|
||||
const darkMode = useMapGetter('appConfig/darkMode');
|
||||
|
||||
const systemPreference = computed(getSystemPreference);
|
||||
|
||||
const prefersDarkMode = computed(() =>
|
||||
calculatePrefersDarkMode(darkMode.value, systemPreference.value)
|
||||
);
|
||||
|
||||
return {
|
||||
darkMode,
|
||||
prefersDarkMode,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user