Merge branch 'develop' into feat/ui-lib

This commit is contained in:
Shivam Mishra
2025-12-01 14:11:07 +05:30
committed by GitHub
927 changed files with 41817 additions and 9044 deletions
@@ -0,0 +1,57 @@
import { ref } from 'vue';
export const useLoadWithRetry = (config = {}) => {
const maxRetry = config.max_retry || 3;
const backoff = config.backoff || 1000;
const isLoaded = ref(false);
const hasError = ref(false);
const loadWithRetry = async url => {
const attemptLoad = () => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
isLoaded.value = true;
hasError.value = false;
resolve();
};
img.onerror = () => {
reject(new Error('Failed to load image'));
};
img.src = url;
});
};
const sleep = ms => {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
};
const retry = async (attempt = 0) => {
try {
await attemptLoad();
} catch (error) {
if (attempt + 1 >= maxRetry) {
hasError.value = true;
isLoaded.value = false;
return;
}
await sleep(backoff * (attempt + 1));
await retry(attempt + 1);
}
};
await retry();
};
return {
isLoaded,
hasError,
loadWithRetry,
};
};
@@ -12,7 +12,11 @@ vi.mock('dashboard/composables', () => ({
}));
vi.mock('vue-i18n');
vi.mock('activestorage');
vi.mock('shared/helpers/FileHelper');
vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: vi.fn(),
resolveMaximumFileUploadSize: vi.fn(value => Number(value) || 40),
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE: 40,
}));
vi.mock('@chatwoot/utils');
describe('useFileUpload', () => {
@@ -36,7 +40,9 @@ describe('useFileUpload', () => {
getCurrentAccountId: { value: '123' },
getCurrentUser: { value: { access_token: 'test-token' } },
getSelectedChat: { value: { id: '456' } },
'globalConfig/get': { value: { directUploadsEnabled: true } },
'globalConfig/get': {
value: { directUploadsEnabled: true, maximumFileUploadSize: 40 },
},
};
return getterMap[getter];
});
@@ -86,7 +92,9 @@ describe('useFileUpload', () => {
getCurrentAccountId: { value: '123' },
getCurrentUser: { value: { access_token: 'test-token' } },
getSelectedChat: { value: { id: '456' } },
'globalConfig/get': { value: { directUploadsEnabled: false } },
'globalConfig/get': {
value: { directUploadsEnabled: false, maximumFileUploadSize: 40 },
},
};
return getterMap[getter];
});
@@ -14,7 +14,7 @@ import {
} from 'dashboard/constants/automation';
/**
* This is a shared composables that holds utilites used to build dropdown and file options
* This is a shared composables that holds utilities used to build dropdown and file options
* @returns {Object} An object containing various automation-related functions and computed properties.
*/
export default function useAutomationValues() {
@@ -4,7 +4,11 @@ import { useI18n } from 'vue-i18n';
import { DirectUpload } from 'activestorage';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import {
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
resolveMaximumFileUploadSize,
} from 'shared/helpers/FileHelper';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
/**
* Composable for handling file uploads in conversations
@@ -21,18 +25,34 @@ export const useFileUpload = ({ inbox, attachFile, isPrivateNote = false }) => {
const currentChat = useMapGetter('getSelectedChat');
const globalConfig = useMapGetter('globalConfig/get');
const installationLimit = resolveMaximumFileUploadSize(
globalConfig.value?.maximumFileUploadSize
);
// helper: compute max upload size for a given file's mime
const maxSizeFor = mime => {
// Use default file size limit for private notes
// Use default/installation limit for private notes
if (isPrivateNote) {
return MAXIMUM_FILE_UPLOAD_SIZE;
return installationLimit;
}
return getMaxUploadSizeByChannel({
channelType: inbox?.channel_type,
const channelType = inbox?.channel_type;
if (!channelType || channelType === INBOX_TYPES.WEB) {
return installationLimit;
}
const channelLimit = getMaxUploadSizeByChannel({
channelType,
medium: inbox?.medium, // e.g. 'sms' | 'whatsapp' | etc.
mime, // e.g. 'image/png'
});
if (channelLimit === DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE) {
return installationLimit;
}
return Math.min(channelLimit, installationLimit);
};
const alertOverLimit = maxSizeMB =>
@@ -1,161 +0,0 @@
import { computed, unref } from 'vue';
const CALL_STATUSES = {
IN_PROGRESS: 'in-progress',
RINGING: 'ringing',
NO_ANSWER: 'no-answer',
BUSY: 'busy',
FAILED: 'failed',
COMPLETED: 'completed',
CANCELED: 'canceled',
};
const CALL_DIRECTIONS = {
INBOUND: 'inbound',
OUTBOUND: 'outbound',
};
/**
* Composable for handling voice call status display logic
* @param {Ref|string} statusRef - Call status (ringing, in-progress, etc.)
* @param {Ref|string} directionRef - Call direction (inbound, outbound)
* @returns {Object} UI properties for displaying call status
*/
export function useVoiceCallStatus(statusRef, directionRef) {
const status = computed(() => unref(statusRef)?.toString());
const direction = computed(() => unref(directionRef)?.toString());
// Status group helpers
const isFailedStatus = computed(() =>
[
CALL_STATUSES.NO_ANSWER,
CALL_STATUSES.BUSY,
CALL_STATUSES.FAILED,
].includes(status.value)
);
const isEndedStatus = computed(() =>
[CALL_STATUSES.COMPLETED, CALL_STATUSES.CANCELED].includes(status.value)
);
const isOutbound = computed(
() => direction.value === CALL_DIRECTIONS.OUTBOUND
);
const labelKey = computed(() => {
const s = status.value;
if (s === CALL_STATUSES.IN_PROGRESS) {
return isOutbound.value
? 'CONVERSATION.VOICE_CALL.OUTGOING_CALL'
: 'CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS';
}
if (s === CALL_STATUSES.RINGING) {
return isOutbound.value
? 'CONVERSATION.VOICE_CALL.OUTGOING_CALL'
: 'CONVERSATION.VOICE_CALL.INCOMING_CALL';
}
if (s === CALL_STATUSES.NO_ANSWER) {
return 'CONVERSATION.VOICE_CALL.MISSED_CALL';
}
if (isFailedStatus.value) {
return 'CONVERSATION.VOICE_CALL.NO_ANSWER';
}
if (isEndedStatus.value) {
return 'CONVERSATION.VOICE_CALL.CALL_ENDED';
}
return 'CONVERSATION.VOICE_CALL.INCOMING_CALL';
});
const subtextKey = computed(() => {
const s = status.value;
if (s === CALL_STATUSES.RINGING) {
return 'CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET';
}
if (s === CALL_STATUSES.IN_PROGRESS) {
return isOutbound.value
? 'CONVERSATION.VOICE_CALL.THEY_ANSWERED'
: 'CONVERSATION.VOICE_CALL.YOU_ANSWERED';
}
if (isFailedStatus.value) {
return 'CONVERSATION.VOICE_CALL.NO_ANSWER';
}
if (isEndedStatus.value) {
return 'CONVERSATION.VOICE_CALL.CALL_ENDED';
}
return 'CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET';
});
const bubbleIconName = computed(() => {
const s = status.value;
if (s === CALL_STATUSES.IN_PROGRESS) {
return isOutbound.value
? 'i-ph-phone-outgoing-fill'
: 'i-ph-phone-incoming-fill';
}
if (isFailedStatus.value) {
return 'i-ph-phone-x-fill';
}
// For ringing/completed/canceled show direction when possible
return isOutbound.value
? 'i-ph-phone-outgoing-fill'
: 'i-ph-phone-incoming-fill';
});
const bubbleIconBg = computed(() => {
const s = status.value;
if (s === CALL_STATUSES.IN_PROGRESS) {
return 'bg-n-teal-9';
}
if (isFailedStatus.value) {
return 'bg-n-ruby-9';
}
if (isEndedStatus.value) {
return 'bg-n-slate-11';
}
// default (e.g., ringing)
return 'bg-n-teal-9 animate-pulse';
});
const listIconColor = computed(() => {
const s = status.value;
if (s === CALL_STATUSES.IN_PROGRESS || s === CALL_STATUSES.RINGING) {
return 'text-n-teal-9';
}
if (isFailedStatus.value) {
return 'text-n-ruby-9';
}
if (isEndedStatus.value) {
return 'text-n-slate-11';
}
return 'text-n-teal-9';
});
return {
status,
labelKey,
subtextKey,
bubbleIconName,
bubbleIconBg,
listIconColor,
};
}