Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bce6707d2 |
@@ -8,7 +8,6 @@ import {
|
||||
EditorState,
|
||||
Selection,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -56,7 +55,7 @@ export default {
|
||||
return {
|
||||
editorView: null,
|
||||
state: undefined,
|
||||
plugins: [imagePastePlugin(this.handleImageUpload)],
|
||||
plugins: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -77,7 +76,6 @@ export default {
|
||||
this.reloadState();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.state = createState(
|
||||
this.value,
|
||||
@@ -97,24 +95,6 @@ export default {
|
||||
openFileBrowser() {
|
||||
this.$refs.imageUploadInput.click();
|
||||
},
|
||||
async handleImageUpload(url) {
|
||||
try {
|
||||
const fileUrl = await this.$store.dispatch(
|
||||
'articles/uploadExternalImage',
|
||||
{
|
||||
portalSlug: this.$route.params.portalSlug,
|
||||
url,
|
||||
}
|
||||
);
|
||||
|
||||
return fileUrl;
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.UN_AUTHORIZED_ERROR')
|
||||
);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
onFileChange() {
|
||||
const file = this.$refs.imageUploadInput.files[0];
|
||||
|
||||
|
||||
@@ -1,93 +1,52 @@
|
||||
import { uploadFile } from '../uploadHelper';
|
||||
import axios from 'axios';
|
||||
import { uploadExternalImage, uploadFile } from '../uploadHelper';
|
||||
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
|
||||
describe('Upload Helpers', () => {
|
||||
describe('#Upload Helpers', () => {
|
||||
afterEach(() => {
|
||||
// Cleaning up the mock after each test
|
||||
axios.post.mockReset();
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadExternalImage', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadExternalImage(mockUrl, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
{ external_url: mockUrl },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockError = new Error('Failed to upload');
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(uploadExternalImage(mockUrl)).rejects.toThrow(
|
||||
'Failed to upload'
|
||||
);
|
||||
});
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,47 +19,26 @@ const HEADERS = {
|
||||
* The function uses FormData to wrap the file and axios to send the request.
|
||||
*
|
||||
* @param {File} file - The file to be uploaded. It should be a File object (typically coming from a file input element).
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response when the upload is successful, or rejects if there's an error.
|
||||
*/
|
||||
export async function uploadFile(file, accountId) {
|
||||
// Create a new FormData instance.
|
||||
let formData = new FormData();
|
||||
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
// Append the file to the FormData instance under the key 'attachment'.
|
||||
let formData = new FormData();
|
||||
formData.append('attachment', file);
|
||||
|
||||
// Use axios to send a POST request to the upload endpoint.
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
formData,
|
||||
{ headers: HEADERS }
|
||||
);
|
||||
|
||||
return {
|
||||
fileUrl: data.file_url,
|
||||
blobKey: data.blob_key,
|
||||
blobId: data.blob_id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an image from an external URL.
|
||||
*
|
||||
* @param {string} url - The external URL of the image.
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response.
|
||||
*/
|
||||
export async function uploadExternalImage(url, accountId) {
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
{ external_url: url },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
{
|
||||
headers: HEADERS,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"UPLOADING": "Uploading...",
|
||||
"SUCCESS": "Image uploaded successfully",
|
||||
"ERROR": "Error while uploading image",
|
||||
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
|
||||
"ERROR_FILE_SIZE": "Image size should be less than {size}MB",
|
||||
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
|
||||
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import articlesAPI from 'dashboard/api/helpCenter/articles';
|
||||
import { uploadExternalImage, uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
|
||||
import types from '../../mutation-types';
|
||||
@@ -132,11 +132,6 @@ export const actions = {
|
||||
return fileUrl;
|
||||
},
|
||||
|
||||
uploadExternalImage: async (_, { url }) => {
|
||||
const { fileUrl } = await uploadExternalImage(url);
|
||||
return fileUrl;
|
||||
},
|
||||
|
||||
reorder: async (_, { portalSlug, categorySlug, reorderedGroup }) => {
|
||||
try {
|
||||
await articlesAPI.reorderArticles({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { uploadExternalImage, uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { actions } from '../actions';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
|
||||
vi.mock('dashboard/helper/uploadHelper');
|
||||
|
||||
@@ -211,29 +211,4 @@ describe('#actions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadExternalImage', () => {
|
||||
it('should upload the image from external URL and return the fileUrl', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockFileUrl = 'https://uploaded.example.com/image.jpg';
|
||||
uploadExternalImage.mockResolvedValueOnce({ fileUrl: mockFileUrl });
|
||||
|
||||
// When
|
||||
const result = await actions.uploadExternalImage({}, { url: mockUrl });
|
||||
|
||||
// Then
|
||||
expect(uploadExternalImage).toHaveBeenCalledWith(mockUrl);
|
||||
expect(result).toBe(mockFileUrl);
|
||||
});
|
||||
|
||||
it('should throw an error if the upload fails', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockError = new Error('Upload failed');
|
||||
uploadExternalImage.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(
|
||||
actions.uploadExternalImage({}, { url: mockUrl })
|
||||
).rejects.toThrow('Upload failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script>
|
||||
import CardButton from 'shared/components/CardButton.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CardButton,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -24,30 +25,21 @@ export default {
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card-message chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||
>
|
||||
<img class="media" :src="mediaUrl" />
|
||||
<div class="card-body">
|
||||
<h4
|
||||
class="title"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
>
|
||||
<h4 class="title" :class="$dm('text-black-900', 'dark:text-slate-50')">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<p
|
||||
class="body"
|
||||
:class="getThemeClass('text-black-700', 'dark:text-slate-100')"
|
||||
>
|
||||
<p class="body" :class="$dm('text-black-700', 'dark:text-slate-100')">
|
||||
{{ description }}
|
||||
</p>
|
||||
<CardButton v-for="action in actions" :key="action.id" :action="action" />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
|
||||
export default {
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
@@ -18,10 +19,6 @@ export default {
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formValues: {},
|
||||
@@ -36,8 +33,8 @@ export default {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
|
||||
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||
},
|
||||
isFormValid() {
|
||||
return this.items.reduce((acc, { name }) => {
|
||||
@@ -83,7 +80,7 @@ export default {
|
||||
<template>
|
||||
<div
|
||||
class="form chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||
>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div
|
||||
@@ -94,7 +91,7 @@ export default {
|
||||
'has-submitted': hasSubmitted,
|
||||
}"
|
||||
>
|
||||
<label :class="getThemeClass('text-black-900', 'dark:text-slate-50')">{{
|
||||
<label :class="$dm('text-black-900', 'dark:text-slate-50')">{{
|
||||
item.label
|
||||
}}</label>
|
||||
<input
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mapGetters } from 'vuex';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
Spinner,
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
messageContentAttributes: {
|
||||
type: Object,
|
||||
@@ -21,10 +22,6 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
@@ -47,8 +44,8 @@ export default {
|
||||
return !(this.selectedRating && this.feedback);
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
|
||||
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||
},
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
@@ -108,13 +105,10 @@ export default {
|
||||
<template>
|
||||
<div
|
||||
class="customer-satisfaction"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||
:style="{ borderColor: widgetColor }"
|
||||
>
|
||||
<h6
|
||||
class="title"
|
||||
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
|
||||
>
|
||||
<h6 class="title" :class="$dm('text-slate-900', 'dark:text-slate-50')">
|
||||
{{ title }}
|
||||
</h6>
|
||||
<div class="ratings">
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<script>
|
||||
import { formatDate } from 'shared/helpers/DateHelper';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
date: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {
|
||||
formattedDate() {
|
||||
return formatDate({
|
||||
@@ -28,7 +25,7 @@ export default {
|
||||
<template>
|
||||
<div
|
||||
class="date--separator"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||
>
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import DateSeparator from '../DateSeparator.vue';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
const localVue = createLocalVue();
|
||||
import i18n from 'dashboard/i18n';
|
||||
localVue.use(Vuex);
|
||||
@@ -39,6 +40,7 @@ describe('dateSeparator', () => {
|
||||
propsData: { date: 'Nov 18, 2019' },
|
||||
mocks: { $t: msg => msg },
|
||||
i18n: i18nConfig,
|
||||
mixins: [darkModeMixin],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { setHeader } from 'widget/helpers/axios';
|
||||
import addHours from 'date-fns/addHours';
|
||||
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
||||
import configMixin from './mixins/configMixin';
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import { getLocale } from './helpers/urlParamsHelper';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import useReplaceRoute from 'widget/composables/useReplaceRoute';
|
||||
import routerMixin from './mixins/routerMixin';
|
||||
import {
|
||||
getExtraSpaceToScroll,
|
||||
loadedEventConfig,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
ON_CAMPAIGN_MESSAGE_CLICK,
|
||||
ON_UNREAD_MESSAGE_CLICK,
|
||||
} from './constants/widgetBusEvents';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
@@ -27,11 +27,10 @@ export default {
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
mixins: [availabilityMixin, configMixin],
|
||||
mixins: [configMixin, routerMixin, darkModeMixin],
|
||||
setup() {
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
const replaceRoute = useReplaceRoute();
|
||||
return { replaceRoute, prefersDarkMode };
|
||||
const { isInBusinessHours } = useAvailability();
|
||||
return { isInBusinessHours };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import messageMixin from '../mixins/messageMixin';
|
||||
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
import ReplyToChip from 'widget/components/ReplyToChip.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
MessageReplyButton,
|
||||
ReplyToChip,
|
||||
},
|
||||
mixins: [configMixin, messageMixin],
|
||||
mixins: [configMixin, messageMixin, darkModeMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -39,12 +39,6 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return {
|
||||
getThemeClass,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
@@ -198,9 +192,7 @@ export default {
|
||||
<div
|
||||
v-if="hasAttachments"
|
||||
class="space-y-2 chat-bubble has-attachment agent"
|
||||
:class="
|
||||
(wrapClass, getThemeClass('bg-white', 'dark:bg-slate-700'))
|
||||
"
|
||||
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-700'))"
|
||||
>
|
||||
<div
|
||||
v-for="attachment in message.attachments"
|
||||
@@ -239,7 +231,7 @@ export default {
|
||||
v-if="message.showAvatar || hasRecordedResponse"
|
||||
v-dompurify-html="agentName"
|
||||
class="agent-name"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import ChatOptions from 'shared/components/ChatOptions.vue';
|
||||
import ChatArticle from './template/Article.vue';
|
||||
import EmailInput from './template/EmailInput.vue';
|
||||
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
import IntegrationCard from './template/IntegrationCard.vue';
|
||||
|
||||
export default {
|
||||
@@ -20,6 +20,7 @@ export default {
|
||||
CustomerSatisfaction,
|
||||
IntegrationCard,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
message: { type: String, default: null },
|
||||
contentType: { type: String, default: null },
|
||||
@@ -33,13 +34,11 @@ export default {
|
||||
setup() {
|
||||
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
|
||||
useMessageFormatter();
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return {
|
||||
formatMessage,
|
||||
getPlainText,
|
||||
truncateMessage,
|
||||
highlightContent,
|
||||
getThemeClass,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -99,7 +98,7 @@ export default {
|
||||
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
|
||||
"
|
||||
class="chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700 has-dark-mode')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-700 has-dark-mode')"
|
||||
>
|
||||
<div
|
||||
v-dompurify-html="formatMessage(message, false)"
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<script>
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixing from 'widget/mixins/darkModeMixin.js';
|
||||
export default {
|
||||
name: 'AgentTypingBubble',
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
mixins: [darkModeMixing],
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -16,7 +13,7 @@ export default {
|
||||
<div class="message-wrap mt-2">
|
||||
<div
|
||||
class="typing-bubble chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||
>
|
||||
<img
|
||||
src="~widget/assets/images/typing.gif"
|
||||
|
||||
@@ -6,7 +6,7 @@ import FooterReplyTo from 'widget/components/FooterReplyTo.vue';
|
||||
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { sendEmailTranscript } from 'widget/api/conversation';
|
||||
import useReplaceRoute from 'widget/composables/useReplaceRoute';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import { IFrameHelper } from '../helpers/utils';
|
||||
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
|
||||
|
||||
@@ -16,10 +16,7 @@ export default {
|
||||
CustomButton,
|
||||
FooterReplyTo,
|
||||
},
|
||||
setup() {
|
||||
const replaceRoute = useReplaceRoute();
|
||||
return { replaceRoute };
|
||||
},
|
||||
mixins: [routerMixin],
|
||||
data() {
|
||||
return {
|
||||
inReplyTo: null,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script>
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import { useNextAvailabilityTime } from 'widget/composables/useNextAvailabilityTime';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import useReplaceRoute from 'widget/composables/useReplaceRoute';
|
||||
import darkMixin from 'widget/mixins/darkModeMixin.js';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
export default {
|
||||
name: 'ChatHeader',
|
||||
@@ -14,7 +12,7 @@ export default {
|
||||
FluentIcon,
|
||||
HeaderActions,
|
||||
},
|
||||
mixins: [nextAvailabilityTime, availabilityMixin],
|
||||
mixins: [routerMixin, darkMixin],
|
||||
props: {
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
@@ -32,26 +30,25 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const replaceRoute = useReplaceRoute();
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass, replaceRoute };
|
||||
const {
|
||||
isOnline,
|
||||
channelConfig,
|
||||
isInBetweenTheWorkingHours,
|
||||
replyWaitMessage,
|
||||
} = useAvailability();
|
||||
const { setTimeSlot } = useNextAvailabilityTime();
|
||||
return {
|
||||
isOnline,
|
||||
channelConfig,
|
||||
isInBetweenTheWorkingHours,
|
||||
replyWaitMessage,
|
||||
setTimeSlot,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isOnline() {
|
||||
const { workingHoursEnabled } = this.channelConfig;
|
||||
const anyAgentOnline = this.availableAgents.length > 0;
|
||||
|
||||
if (workingHoursEnabled) {
|
||||
return this.isInBetweenTheWorkingHours;
|
||||
}
|
||||
return anyAgentOnline;
|
||||
},
|
||||
mounted() {
|
||||
this.setTimeSlot();
|
||||
},
|
||||
methods: {
|
||||
onBackButtonClick() {
|
||||
@@ -64,7 +61,7 @@ export default {
|
||||
<template>
|
||||
<header
|
||||
class="flex justify-between w-full p-5"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-900')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
@@ -75,7 +72,7 @@ export default {
|
||||
<FluentIcon
|
||||
icon="chevron-left"
|
||||
size="24"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
</button>
|
||||
<img
|
||||
@@ -87,7 +84,7 @@ export default {
|
||||
<div>
|
||||
<div
|
||||
class="flex items-center text-base font-medium leading-4"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||
>
|
||||
<span v-dompurify-html="title" class="mr-1" />
|
||||
<div
|
||||
@@ -97,7 +94,7 @@ export default {
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 text-xs leading-3"
|
||||
:class="getThemeClass('text-black-700', 'dark:text-slate-400')"
|
||||
:class="$dm('text-black-700', 'dark:text-slate-400')"
|
||||
>
|
||||
{{ replyWaitMessage }}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
export default {
|
||||
name: 'ChatHeaderExpanded',
|
||||
components: {
|
||||
HeaderActions,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
@@ -25,10 +26,6 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -54,12 +51,12 @@ export default {
|
||||
<h2
|
||||
v-dompurify-html="introHeading"
|
||||
class="mt-4 text-2xl mb-1.5 font-medium"
|
||||
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
|
||||
:class="$dm('text-slate-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
<p
|
||||
v-dompurify-html="introBody"
|
||||
class="text-base leading-normal"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import ChatSendButton from 'widget/components/ChatSendButton.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
FluentIcon,
|
||||
ResizableTextArea,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
props: {
|
||||
onSendMessage: {
|
||||
type: Function,
|
||||
@@ -30,10 +30,7 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
userInput: '',
|
||||
@@ -54,16 +51,13 @@ export default {
|
||||
return this.userInput.length > 0;
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
|
||||
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||
},
|
||||
emojiIconColor() {
|
||||
return this.showEmojiPicker
|
||||
? `text-woot-500 ${this.getThemeClass(
|
||||
'text-black-900',
|
||||
'dark:text-slate-100'
|
||||
)}`
|
||||
: `${this.getThemeClass('text-black-900', 'dark:text-slate-100')}`;
|
||||
? `text-woot-500 ${this.$dm('text-black-900', 'dark:text-slate-100')}`
|
||||
: `${this.$dm('text-black-900', 'dark:text-slate-100')}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -134,7 +128,7 @@ export default {
|
||||
<template>
|
||||
<div
|
||||
class="chat-message--input is-focused"
|
||||
:class="getThemeClass('bg-white ', 'dark:bg-slate-600')"
|
||||
:class="$dm('bg-white ', 'dark:bg-slate-600')"
|
||||
@keydown.esc="hideEmojiPicker"
|
||||
>
|
||||
<ResizableTextArea
|
||||
@@ -154,7 +148,7 @@ export default {
|
||||
<div class="button-wrap">
|
||||
<ChatAttachmentButton
|
||||
v-if="showAttachment"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-100')"
|
||||
:class="$dm('text-black-900', 'dark:text-slate-100')"
|
||||
:on-attach="onSendAttachment"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -3,7 +3,7 @@ import ChatMessage from 'widget/components/ChatMessage.vue';
|
||||
import AgentTypingBubble from 'widget/components/AgentTypingBubble.vue';
|
||||
import DateSeparator from 'shared/components/DateSeparator.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
@@ -15,16 +15,13 @@ export default {
|
||||
DateSeparator,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
groupedMessages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { darkMode } = useDarkMode();
|
||||
return { darkMode };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
previousScrollHeight: 0,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
@@ -25,10 +26,6 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.isInProgress
|
||||
@@ -48,7 +45,7 @@ export default {
|
||||
},
|
||||
titleColor() {
|
||||
return !this.isUserBubble
|
||||
? this.getThemeClass('text-black-900', 'dark:text-slate-50')
|
||||
? this.$dm('text-black-900', 'dark:text-slate-50')
|
||||
: '';
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
export default {
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
@@ -27,33 +28,20 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {
|
||||
labelClass() {
|
||||
return this.error
|
||||
? `text-red-400 ${this.getThemeClass(
|
||||
'text-black-800',
|
||||
'dark:text-slate-50'
|
||||
)}`
|
||||
: `text-black-800 ${this.getThemeClass(
|
||||
'text-black-800',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
? `text-red-400 ${this.$dm('text-black-800', 'dark:text-slate-50')}`
|
||||
: `text-black-800 ${this.$dm('text-black-800', 'dark:text-slate-50')}`;
|
||||
},
|
||||
isInputDarkOrLightMode() {
|
||||
return `${this.getThemeClass(
|
||||
'bg-white',
|
||||
'dark:bg-slate-600'
|
||||
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
inputBorderColor() {
|
||||
return `${this.getThemeClass(
|
||||
'border-black-200',
|
||||
'dark:border-black-500'
|
||||
)}`;
|
||||
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||
},
|
||||
inputHasError() {
|
||||
return this.error
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import FormulateInputMixin from '@braid/vue-formulate/src/FormulateInputMixin';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [FormulateInputMixin],
|
||||
mixins: [FormulateInputMixin, darkModeMixin],
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
@@ -19,10 +19,6 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: -1,
|
||||
@@ -49,31 +45,28 @@ export default {
|
||||
return this.activeCountryCode ? 'Clear selection' : 'Select Country';
|
||||
},
|
||||
dropdownClass() {
|
||||
return `${this.getThemeClass(
|
||||
'bg-slate-100',
|
||||
'dark:bg-slate-700'
|
||||
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
|
||||
},
|
||||
dropdownBackgroundClass() {
|
||||
return `${this.getThemeClass(
|
||||
'bg-white',
|
||||
'dark:bg-slate-700'
|
||||
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
|
||||
},
|
||||
dropdownItemClass() {
|
||||
return `${this.getThemeClass(
|
||||
return `${this.$dm('bg-slate-100', 'dark:bg-slate-700')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)} ${this.getThemeClass('hover:bg-slate-50', 'dark:hover:bg-slate-600')}`;
|
||||
},
|
||||
activeDropdownItemClass() {
|
||||
return `active ${this.getThemeClass(
|
||||
'bg-slate-100',
|
||||
'dark:bg-slate-800'
|
||||
)}`;
|
||||
},
|
||||
dropdownBackgroundClass() {
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-700')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
dropdownItemClass() {
|
||||
return `${this.$dm('text-slate-700', 'dark:text-slate-50')} ${this.$dm(
|
||||
'hover:bg-slate-50',
|
||||
'dark:hover:bg-slate-600'
|
||||
)}`;
|
||||
},
|
||||
activeDropdownItemClass() {
|
||||
return `active ${this.$dm('bg-slate-100', 'dark:bg-slate-800')}`;
|
||||
},
|
||||
focusedDropdownItemClass() {
|
||||
return `focus ${this.getThemeClass('bg-slate-50', 'dark:bg-slate-600')}`;
|
||||
return `focus ${this.$dm('bg-slate-50', 'dark:bg-slate-600')}`;
|
||||
},
|
||||
inputHasError() {
|
||||
return this.hasErrorInPhoneInput
|
||||
@@ -81,16 +74,13 @@ export default {
|
||||
: `hover:border-black-300 focus:border-black-300 ${this.inputLightAndDarkModeColor} ${this.inputBorderColor}`;
|
||||
},
|
||||
inputBorderColor() {
|
||||
return `${this.getThemeClass(
|
||||
'border-black-200',
|
||||
'dark:border-black-500'
|
||||
)}`;
|
||||
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||
},
|
||||
inputLightAndDarkModeColor() {
|
||||
return `${this.getThemeClass(
|
||||
'bg-white',
|
||||
'dark:bg-slate-600'
|
||||
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
items() {
|
||||
return this.countries.filter(country => {
|
||||
@@ -251,7 +241,7 @@ export default {
|
||||
<span
|
||||
v-if="activeDialCode"
|
||||
class="py-2 pl-2 pr-0 text-base"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-50')"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-50')"
|
||||
>
|
||||
{{ activeDialCode }}
|
||||
</span>
|
||||
@@ -283,10 +273,7 @@ export default {
|
||||
type="text"
|
||||
:placeholder="$t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DROPDOWN_SEARCH')"
|
||||
class="w-full h-8 px-3 py-2 mt-1 mb-1 text-sm border border-solid rounded outline-none dropdown-search"
|
||||
:class="[
|
||||
getThemeClass('bg-slate-50', 'dark:bg-slate-600'),
|
||||
inputBorderColor,
|
||||
]"
|
||||
:class="[$dm('bg-slate-50', 'dark:bg-slate-600'), inputBorderColor]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -311,7 +298,7 @@ export default {
|
||||
<div v-if="items.length === 0">
|
||||
<span
|
||||
class="flex justify-center mt-4 text-sm text-center"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-50')"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-50')"
|
||||
>
|
||||
{{ $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DROPDOWN_EMPTY') }}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
export default {
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
@@ -19,33 +20,20 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {
|
||||
labelClass() {
|
||||
return this.error
|
||||
? `text-red-400 ${this.getThemeClass(
|
||||
'text-black-800',
|
||||
'dark:text-slate-50'
|
||||
)}`
|
||||
: `text-black-800 ${this.getThemeClass(
|
||||
'text-black-800',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
? `text-red-400 ${this.$dm('text-black-800', 'dark:text-slate-50')}`
|
||||
: `text-black-800 ${this.$dm('text-black-800', 'dark:text-slate-50')}`;
|
||||
},
|
||||
isTextAreaDarkOrLightMode() {
|
||||
return `${this.getThemeClass(
|
||||
'bg-white',
|
||||
'dark:bg-slate-600'
|
||||
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
textAreaBorderColor() {
|
||||
return `${this.getThemeClass(
|
||||
'border-black-200',
|
||||
'dark:border-black-500'
|
||||
)}`;
|
||||
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||
},
|
||||
isTextAreaHasError() {
|
||||
return this.error
|
||||
|
||||
@@ -3,14 +3,14 @@ import { mapGetters } from 'vuex';
|
||||
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
||||
import { popoutChatWindow } from '../helpers/popoutHelper';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import { CONVERSATION_STATUS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
name: 'HeaderActions',
|
||||
components: { FluentIcon },
|
||||
mixins: [configMixin],
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
props: {
|
||||
showPopoutButton: {
|
||||
type: Boolean,
|
||||
@@ -21,10 +21,6 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
conversationAttributes: 'conversationAttributes/getConversationParams',
|
||||
@@ -96,7 +92,7 @@ export default {
|
||||
<FluentIcon
|
||||
icon="sign-out"
|
||||
size="22"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
@@ -107,7 +103,7 @@ export default {
|
||||
<FluentIcon
|
||||
icon="open"
|
||||
size="22"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
@@ -120,7 +116,7 @@ export default {
|
||||
<FluentIcon
|
||||
icon="dismiss"
|
||||
size="24"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,20 +5,17 @@ import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import { getRegexp } from 'shared/helpers/Validators';
|
||||
import useReplaceRoute from 'widget/composables/useReplaceRoute';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomButton,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
mixins: [routerMixin, darkModeMixin, configMixin],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
@@ -27,9 +24,9 @@ export default {
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
const replaceRoute = useReplaceRoute();
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { formatMessage, getThemeClass, replaceRoute };
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -134,28 +131,25 @@ export default {
|
||||
return `mt-1 border rounded w-full py-2 px-3 text-slate-700 outline-none`;
|
||||
},
|
||||
isInputDarkOrLightMode() {
|
||||
return `${this.getThemeClass(
|
||||
'bg-white',
|
||||
'dark:bg-slate-600'
|
||||
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
inputBorderColor() {
|
||||
return `${this.getThemeClass(
|
||||
'border-black-200',
|
||||
'dark:border-black-500'
|
||||
)}`;
|
||||
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
labelClass(context) {
|
||||
const { hasErrors } = context;
|
||||
if (!hasErrors) {
|
||||
return `text-xs font-medium ${this.getThemeClass(
|
||||
return `text-xs font-medium ${this.$dm(
|
||||
'text-black-800',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
}
|
||||
return `text-xs font-medium ${this.getThemeClass(
|
||||
return `text-xs font-medium ${this.$dm(
|
||||
'text-red-400',
|
||||
'dark:text-red-400'
|
||||
)}`;
|
||||
@@ -270,7 +264,7 @@ export default {
|
||||
v-if="shouldShowHeaderMessage"
|
||||
v-dompurify-html="formatMessage(headerMessage, false)"
|
||||
class="mb-4 text-sm leading-5 pre-chat-header-message"
|
||||
:class="getThemeClass('text-black-800', 'dark:text-slate-50')"
|
||||
:class="$dm('text-black-800', 'dark:text-slate-50')"
|
||||
/>
|
||||
<FormulateInput
|
||||
v-for="item in enabledPreChatFields"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
|
||||
import { useNextAvailabilityTime } from 'widget/composables/useNextAvailabilityTime';
|
||||
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import { IFrameHelper } from 'widget/helpers/utils';
|
||||
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
AvailableAgents,
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [configMixin, nextAvailabilityTime, availabilityMixin],
|
||||
mixins: [configMixin],
|
||||
props: {
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
@@ -26,7 +26,11 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { isInBetweenTheWorkingHours, replyWaitMessage } = useAvailability();
|
||||
const { setTimeSlot } = useNextAvailabilityTime();
|
||||
return { setTimeSlot, isInBetweenTheWorkingHours, replyWaitMessage };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
@@ -44,6 +48,9 @@ export default {
|
||||
return anyAgentOnline;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setTimeSlot();
|
||||
},
|
||||
methods: {
|
||||
startConversation() {
|
||||
this.$emit('startConversation');
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
ON_CAMPAIGN_MESSAGE_CLICK,
|
||||
ON_UNREAD_MESSAGE_CLICK,
|
||||
} from '../constants/widgetBusEvents';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
export default {
|
||||
name: 'UnreadMessage',
|
||||
components: { Thumbnail },
|
||||
mixins: [configMixin],
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
@@ -31,15 +31,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
|
||||
useMessageFormatter();
|
||||
const { getThemeClass } = useDarkMode();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
getPlainText,
|
||||
truncateMessage,
|
||||
highlightContent,
|
||||
getThemeClass,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -97,7 +91,7 @@ export default {
|
||||
<div class="chat-bubble-wrap">
|
||||
<button
|
||||
class="chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-50')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-50')"
|
||||
@click="onClickMessage"
|
||||
>
|
||||
<div v-if="showSender" class="row--agent-block">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -15,8 +16,9 @@ export default {
|
||||
},
|
||||
setup() {
|
||||
const { truncateMessage } = useMessageFormatter();
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass, truncateMessage };
|
||||
return {
|
||||
truncateMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -25,7 +27,7 @@ export default {
|
||||
<div
|
||||
v-if="!!items.length"
|
||||
class="chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||
>
|
||||
<div v-for="item in items" :key="item.link" class="article-item">
|
||||
<a :href="item.link" target="_blank" rel="noopener noreferrer nofollow">
|
||||
@@ -33,16 +35,15 @@ export default {
|
||||
<FluentIcon
|
||||
icon="link"
|
||||
class="mr-1"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
<span
|
||||
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
|
||||
>{{ item.title }}</span
|
||||
>
|
||||
<span :class="$dm('text-slate-900', 'dark:text-slate-50')">{{
|
||||
item.title
|
||||
}}</span>
|
||||
</span>
|
||||
<span
|
||||
class="description"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||
>
|
||||
{{ truncateMessage(item.description) }}
|
||||
</span>
|
||||
|
||||
@@ -6,13 +6,14 @@ import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
messageId: {
|
||||
type: Number,
|
||||
@@ -24,8 +25,7 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { v$: useVuelidate(), getThemeClass };
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -47,9 +47,9 @@ export default {
|
||||
);
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
|
||||
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}
|
||||
${this.getThemeClass('border-black-200', 'dark:border-black-500')}`;
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||
${this.$dm('text-black-900', 'dark:text-slate-50')}
|
||||
${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||
},
|
||||
inputHasError() {
|
||||
return this.v$.email.$error
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { ref } from 'vue';
|
||||
import { useAvailability } from '../useAvailability';
|
||||
import * as useNextAvailabilityTime from '../useNextAvailabilityTime';
|
||||
import * as useI18n from 'dashboard/composables/useI18n';
|
||||
import * as useMapGetter from 'dashboard/composables/store';
|
||||
|
||||
vi.mock('../useNextAvailabilityTime');
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
vi.mock('dashboard/composables/store');
|
||||
|
||||
describe('useAvailability', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
useNextAvailabilityTime.useNextAvailabilityTime.mockReturnValue({
|
||||
timeLeftToBackInOnline: ref('2 hours'),
|
||||
});
|
||||
useI18n.useI18n.mockReturnValue({
|
||||
t: vi.fn(key => key),
|
||||
});
|
||||
useMapGetter.useMapGetter.mockReturnValue(ref([]));
|
||||
|
||||
global.chatwootWebChannel = {
|
||||
workingHoursEnabled: true,
|
||||
workingHours: [
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 8,
|
||||
open_minutes: 30,
|
||||
close_hour: 17,
|
||||
close_minutes: 35,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 8,
|
||||
open_minutes: 30,
|
||||
close_hour: 17,
|
||||
close_minutes: 30,
|
||||
open_all_day: false,
|
||||
},
|
||||
],
|
||||
utcOffset: '-07:00',
|
||||
replyTime: 'in_a_few_hours',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns valid isInBetweenTheWorkingHours if in different timezone', () => {
|
||||
vi.setSystemTime(new Date('Thu Apr 14 2022 06:04:46 GMT+0530'));
|
||||
const { isInBetweenTheWorkingHours } = useAvailability();
|
||||
expect(isInBetweenTheWorkingHours.value).toBe(true);
|
||||
});
|
||||
|
||||
it('returns valid isInBetweenTheWorkingHours if in same timezone', () => {
|
||||
global.chatwootWebChannel.utcOffset = '+05:30';
|
||||
vi.setSystemTime(new Date('Thu Apr 14 2022 09:01:46 GMT+0530'));
|
||||
const { isInBetweenTheWorkingHours } = useAvailability();
|
||||
expect(isInBetweenTheWorkingHours.value).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if closed all day', () => {
|
||||
global.chatwootWebChannel.utcOffset = '-07:00';
|
||||
global.chatwootWebChannel.workingHours = [
|
||||
{ day_of_week: 3, closed_all_day: true },
|
||||
];
|
||||
vi.setSystemTime(new Date('Thu Apr 14 2022 09:01:46 GMT+0530'));
|
||||
const { isInBetweenTheWorkingHours } = useAvailability();
|
||||
expect(isInBetweenTheWorkingHours.value).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if open all day', () => {
|
||||
global.chatwootWebChannel.utcOffset = '-07:00';
|
||||
global.chatwootWebChannel.workingHours = [
|
||||
{ day_of_week: 3, open_all_day: true },
|
||||
];
|
||||
vi.setSystemTime(new Date('Thu Apr 14 2022 09:01:46 GMT+0530'));
|
||||
const { isInBetweenTheWorkingHours } = useAvailability();
|
||||
expect(isInBetweenTheWorkingHours.value).toBe(true);
|
||||
});
|
||||
|
||||
it('returns correct replyTimeStatus', () => {
|
||||
const { replyTimeStatus } = useAvailability();
|
||||
expect(replyTimeStatus.value).toBe('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
});
|
||||
|
||||
it('returns correct replyWaitMessage when online', () => {
|
||||
global.chatwootWebChannel.workingHoursEnabled = true;
|
||||
global.chatwootWebChannel.utcOffset = '+05:30';
|
||||
vi.setSystemTime(new Date('Thu Apr 14 2022 09:01:46 GMT+0530'));
|
||||
const { replyWaitMessage, isInBetweenTheWorkingHours } = useAvailability();
|
||||
expect(isInBetweenTheWorkingHours.value).toBe(true);
|
||||
expect(replyWaitMessage.value).toBe('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
});
|
||||
|
||||
it('returns correct replyWaitMessage when offline', () => {
|
||||
global.chatwootWebChannel.workingHoursEnabled = true;
|
||||
global.chatwootWebChannel.workingHours = [
|
||||
{ day_of_week: 3, closed_all_day: true },
|
||||
];
|
||||
vi.setSystemTime(new Date('Thu Apr 14 2022 09:01:46 GMT+0530'));
|
||||
const { replyWaitMessage } = useAvailability();
|
||||
expect(replyWaitMessage.value).toBe('REPLY_TIME.BACK_IN 2 hours');
|
||||
});
|
||||
|
||||
it('returns correct isInBusinessHours', () => {
|
||||
global.chatwootWebChannel.workingHoursEnabled = true;
|
||||
global.chatwootWebChannel.utcOffset = '+05:30';
|
||||
vi.setSystemTime(new Date('Thu Apr 14 2022 09:01:46 GMT+0530'));
|
||||
const { isInBusinessHours, isInBetweenTheWorkingHours } = useAvailability();
|
||||
expect(isInBetweenTheWorkingHours.value).toBe(true);
|
||||
expect(isInBusinessHours.value).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
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, and getThemeClass', () => {
|
||||
const result = useDarkMode();
|
||||
expect(result).toHaveProperty('darkMode');
|
||||
expect(result).toHaveProperty('prefersDarkMode');
|
||||
expect(result).toHaveProperty('getThemeClass');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThemeClass', () => {
|
||||
it('returns light class when darkMode is light', () => {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
expect(getThemeClass('light-class', 'dark-class')).toBe('light-class');
|
||||
});
|
||||
|
||||
it('returns dark class when darkMode is dark', () => {
|
||||
mockDarkMode.value = 'dark';
|
||||
const { getThemeClass } = useDarkMode();
|
||||
expect(getThemeClass('light-class', 'dark-class')).toBe('dark-class');
|
||||
});
|
||||
|
||||
it('returns both classes when darkMode is auto', () => {
|
||||
mockDarkMode.value = 'auto';
|
||||
const { getThemeClass } = useDarkMode();
|
||||
expect(getThemeClass('light-class', 'dark-class')).toBe(
|
||||
'light-class dark-class'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
import { useNextAvailabilityTime } from '../useNextAvailabilityTime';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { generateRelativeTime } from 'shared/helpers/DateHelper';
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
|
||||
describe('useNextAvailabilityTime', () => {
|
||||
const chatwootWebChannel = {
|
||||
workingHoursEnabled: true,
|
||||
workingHours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
open_hour: 9,
|
||||
closed_all_day: false,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
locale: 'en',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.chatwootWebChannel = chatwootWebChannel;
|
||||
useI18n.mockReturnValue({
|
||||
t: vi.fn(key => {
|
||||
if (key === 'DAY_NAMES') {
|
||||
return [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
}
|
||||
return key;
|
||||
}),
|
||||
});
|
||||
generateRelativeTime.mockImplementation((value, unit) => {
|
||||
if (unit === 'hour') return `in ${value} hour${value > 1 ? 's' : ''}`;
|
||||
if (unit === 'minutes')
|
||||
return `in ${value} minute${value > 1 ? 's' : ''}`;
|
||||
if (unit === 'days') return value === 1 ? 'tomorrow' : `in ${value} days`;
|
||||
return `in ${value} ${unit}`;
|
||||
});
|
||||
utcToZonedTime.mockImplementation(date => new Date(date));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.chatwootWebChannel;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return day names', () => {
|
||||
const { dayNames } = useNextAvailabilityTime();
|
||||
expect(dayNames).toEqual([
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return channelConfig', () => {
|
||||
const { channelConfig } = useNextAvailabilityTime();
|
||||
expect(channelConfig.value).toEqual(chatwootWebChannel);
|
||||
});
|
||||
|
||||
it('should return workingHours', () => {
|
||||
const { workingHours } = useNextAvailabilityTime();
|
||||
expect(workingHours.value).toEqual(chatwootWebChannel.workingHours);
|
||||
});
|
||||
|
||||
it('should return currentDayWorkingHours', () => {
|
||||
const { currentDayWorkingHours } = useNextAvailabilityTime();
|
||||
const currentDay = new Date().getDay();
|
||||
const expectedWorkingHours = chatwootWebChannel.workingHours.find(
|
||||
slot => slot.day_of_week === currentDay
|
||||
);
|
||||
expect(currentDayWorkingHours.value).toEqual(expectedWorkingHours);
|
||||
});
|
||||
|
||||
it('should return nextDayWorkingHours', () => {
|
||||
const { nextDayWorkingHours } = useNextAvailabilityTime();
|
||||
const currentDay = new Date().getDay();
|
||||
const nextDay = currentDay === 6 ? 0 : currentDay + 1;
|
||||
const expectedWorkingHours = chatwootWebChannel.workingHours.find(
|
||||
slot => slot.day_of_week === nextDay
|
||||
);
|
||||
expect(nextDayWorkingHours.value).toEqual(expectedWorkingHours);
|
||||
});
|
||||
|
||||
it('should return presentHour', () => {
|
||||
const { presentHour } = useNextAvailabilityTime();
|
||||
expect(presentHour.value).toBe(new Date().getUTCHours());
|
||||
});
|
||||
|
||||
it('should return presentMinute', () => {
|
||||
const { presentMinute } = useNextAvailabilityTime();
|
||||
expect(presentMinute.value).toBe(new Date().getUTCMinutes());
|
||||
});
|
||||
|
||||
it('should return currentDay', () => {
|
||||
const { currentDay } = useNextAvailabilityTime();
|
||||
const date = new Date();
|
||||
const day = date.getUTCDay();
|
||||
expect(currentDay.value).toBe(day);
|
||||
});
|
||||
|
||||
it('should return currentDayTimings', () => {
|
||||
const { currentDayTimings, currentDayWorkingHours } =
|
||||
useNextAvailabilityTime();
|
||||
const {
|
||||
open_hour: openHour,
|
||||
open_minutes: openMinute,
|
||||
close_hour: closeHour,
|
||||
} = currentDayWorkingHours.value;
|
||||
expect(currentDayTimings.value).toEqual({
|
||||
openHour,
|
||||
openMinute,
|
||||
closeHour,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return nextDayTimings', () => {
|
||||
const { nextDayTimings, nextDayWorkingHours } = useNextAvailabilityTime();
|
||||
const { open_hour: openHour, open_minutes: openMinute } =
|
||||
nextDayWorkingHours.value;
|
||||
expect(nextDayTimings.value).toEqual({
|
||||
openHour,
|
||||
openMinute,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return dayDiff', () => {
|
||||
const { dayDiff, currentDay, nextDayWorkingHours } =
|
||||
useNextAvailabilityTime();
|
||||
const nextDay = nextDayWorkingHours.value.day_of_week;
|
||||
const totalDays = 6;
|
||||
const expectedDayDiff =
|
||||
nextDay > currentDay.value
|
||||
? nextDay - currentDay.value - 1
|
||||
: totalDays - currentDay.value + nextDay;
|
||||
expect(dayDiff.value).toEqual(expectedDayDiff);
|
||||
});
|
||||
|
||||
it('should return dayNameOfNextWorkingDay', () => {
|
||||
const { dayNameOfNextWorkingDay, nextDayWorkingHours, dayNames } =
|
||||
useNextAvailabilityTime();
|
||||
const nextDay = nextDayWorkingHours.value.day_of_week;
|
||||
const expectedDayName = dayNames[nextDay];
|
||||
expect(dayNameOfNextWorkingDay.value).toEqual(expectedDayName);
|
||||
});
|
||||
|
||||
it('should return hoursAndMinutesBackInOnline', () => {
|
||||
const { hoursAndMinutesBackInOnline } = useNextAvailabilityTime();
|
||||
expect(hoursAndMinutesBackInOnline.value).toBeDefined();
|
||||
expect(typeof hoursAndMinutesBackInOnline.value.hoursLeft).toBe('number');
|
||||
expect(typeof hoursAndMinutesBackInOnline.value.minutesLeft).toBe('number');
|
||||
});
|
||||
|
||||
// Time-based tests
|
||||
describe('time-based tests', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return in 30 minutes', () => {
|
||||
vi.setSystemTime(new Date('2022-04-14T17:30:00.000Z')); // Thursday
|
||||
chatwootWebChannel.workingHours[4].open_hour = 18;
|
||||
chatwootWebChannel.workingHours[4].open_minutes = 0;
|
||||
chatwootWebChannel.workingHours[4].close_hour = 23;
|
||||
|
||||
const { timeLeftToBackInOnline, setTimeSlot } = useNextAvailabilityTime();
|
||||
setTimeSlot();
|
||||
expect(timeLeftToBackInOnline.value).toBe('in 30 minutes');
|
||||
});
|
||||
|
||||
it('should return in 2 hours', () => {
|
||||
vi.setSystemTime(new Date('2022-04-14T17:00:00.000Z')); // Thursday
|
||||
chatwootWebChannel.workingHours[4].open_hour = 19;
|
||||
chatwootWebChannel.workingHours[4].open_minutes = 0;
|
||||
chatwootWebChannel.workingHours[4].close_hour = 23;
|
||||
|
||||
const { timeLeftToBackInOnline, setTimeSlot } = useNextAvailabilityTime();
|
||||
setTimeSlot();
|
||||
expect(timeLeftToBackInOnline.value).toBe('in 2 hours');
|
||||
});
|
||||
|
||||
it('should return at 10:00 AM', () => {
|
||||
vi.setSystemTime(new Date('2022-04-14T22:00:00.000Z')); // Thursday
|
||||
chatwootWebChannel.workingHours[5].open_hour = 10; // Friday
|
||||
chatwootWebChannel.workingHours[5].open_minutes = 0;
|
||||
chatwootWebChannel.workingHours[5].close_hour = 18;
|
||||
|
||||
const { timeLeftToBackInOnline, setTimeSlot } = useNextAvailabilityTime();
|
||||
setTimeSlot();
|
||||
expect(timeLeftToBackInOnline.value).toBe('at 10:00 AM');
|
||||
});
|
||||
|
||||
it('should return tomorrow', () => {
|
||||
vi.setSystemTime(new Date('2022-04-14T22:00:00.000Z')); // Thursday
|
||||
chatwootWebChannel.workingHours[5].open_hour = 9; // Friday
|
||||
chatwootWebChannel.workingHours[5].open_minutes = 0;
|
||||
chatwootWebChannel.workingHours[5].close_hour = 17;
|
||||
|
||||
const { timeLeftToBackInOnline, setTimeSlot } = useNextAvailabilityTime();
|
||||
setTimeSlot();
|
||||
expect(timeLeftToBackInOnline.value).toBe('tomorrow');
|
||||
});
|
||||
|
||||
it('should return on Saturday', () => {
|
||||
vi.setSystemTime(new Date('2022-04-14T22:00:00.000Z')); // Thursday
|
||||
chatwootWebChannel.workingHours[4].open_hour = 9;
|
||||
chatwootWebChannel.workingHours[4].close_hour = 17;
|
||||
chatwootWebChannel.workingHours[5].closed_all_day = true; // Friday closed
|
||||
|
||||
const { timeLeftToBackInOnline, setTimeSlot } = useNextAvailabilityTime();
|
||||
setTimeSlot();
|
||||
expect(timeLeftToBackInOnline.value).toBe('on Saturday');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useReplaceRoute } from '../useReplaceRoute';
|
||||
|
||||
vi.mock('dashboard/composables/route', () => ({
|
||||
useRouter: vi.fn(),
|
||||
useRoute: vi.fn(),
|
||||
}));
|
||||
|
||||
import { useRouter, useRoute } from 'dashboard/composables/route';
|
||||
|
||||
describe('useReplaceRoute', () => {
|
||||
const mockReplace = vi.fn();
|
||||
const mockRouter = { replace: mockReplace };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useRouter.mockReturnValue(mockRouter);
|
||||
});
|
||||
|
||||
it('should replace route when current route is different', () => {
|
||||
useRoute.mockReturnValue({ name: 'currentRoute' });
|
||||
const replaceRoute = useReplaceRoute();
|
||||
|
||||
replaceRoute('newRoute', { id: 1 });
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith({
|
||||
name: 'newRoute',
|
||||
params: { id: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not replace route when current route is the same', () => {
|
||||
useRoute.mockReturnValue({ name: 'sameRoute' });
|
||||
const replaceRoute = useReplaceRoute();
|
||||
|
||||
const result = replaceRoute('sameRoute');
|
||||
|
||||
expect(mockReplace).not.toHaveBeenCalled();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the result of router.replace when replacing route', () => {
|
||||
useRoute.mockReturnValue({ name: 'currentRoute' });
|
||||
mockReplace.mockReturnValue('replacementResult');
|
||||
const replaceRoute = useReplaceRoute();
|
||||
|
||||
const result = replaceRoute('newRoute');
|
||||
|
||||
expect(result).toBe('replacementResult');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
import { isTimeAfter } from 'shared/helpers/DateHelper';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useNextAvailabilityTime } from './useNextAvailabilityTime';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
export const useAvailability = () => {
|
||||
const availableAgents = useMapGetter('agent/availableAgents');
|
||||
|
||||
const { t } = useI18n();
|
||||
const { timeLeftToBackInOnline } = useNextAvailabilityTime();
|
||||
const channelConfig = ref(window.chatwootWebChannel);
|
||||
|
||||
const replyTimeStatus = computed(() => {
|
||||
switch (channelConfig.value.replyTime) {
|
||||
case 'in_a_few_minutes':
|
||||
return t('REPLY_TIME.IN_A_FEW_MINUTES');
|
||||
case 'in_a_few_hours':
|
||||
return t('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
case 'in_a_day':
|
||||
return t('REPLY_TIME.IN_A_DAY');
|
||||
default:
|
||||
return t('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
}
|
||||
});
|
||||
|
||||
function getDateWithOffset(utcOffset) {
|
||||
return utcToZonedTime(new Date().toISOString(), utcOffset);
|
||||
}
|
||||
|
||||
const currentDayAvailability = computed(() => {
|
||||
const { utcOffset, workingHours } = channelConfig.value;
|
||||
const dayOfTheWeek = getDateWithOffset(utcOffset).getDay();
|
||||
const [workingHourConfig = {}] = workingHours.filter(
|
||||
workingHour => workingHour.day_of_week === dayOfTheWeek
|
||||
);
|
||||
return {
|
||||
closedAllDay: workingHourConfig.closed_all_day,
|
||||
openHour: workingHourConfig.open_hour,
|
||||
openMinute: workingHourConfig.open_minutes,
|
||||
closeHour: workingHourConfig.close_hour,
|
||||
closeMinute: workingHourConfig.close_minutes,
|
||||
openAllDay: workingHourConfig.open_all_day,
|
||||
};
|
||||
});
|
||||
|
||||
const isInBetweenTheWorkingHours = computed(() => {
|
||||
const {
|
||||
openHour,
|
||||
openMinute,
|
||||
closeHour,
|
||||
closeMinute,
|
||||
closedAllDay,
|
||||
openAllDay,
|
||||
} = currentDayAvailability.value;
|
||||
|
||||
if (openAllDay) return true;
|
||||
if (closedAllDay) return false;
|
||||
|
||||
const { utcOffset } = channelConfig.value;
|
||||
const today = getDateWithOffset(utcOffset);
|
||||
const currentHours = today.getHours();
|
||||
const currentMinutes = today.getMinutes();
|
||||
|
||||
const isAfterStartTime = isTimeAfter(
|
||||
currentHours,
|
||||
currentMinutes,
|
||||
openHour,
|
||||
openMinute
|
||||
);
|
||||
const isBeforeEndTime = isTimeAfter(
|
||||
closeHour,
|
||||
closeMinute,
|
||||
currentHours,
|
||||
currentMinutes
|
||||
);
|
||||
return isAfterStartTime && isBeforeEndTime;
|
||||
});
|
||||
|
||||
const isOnline = computed(() => {
|
||||
const { workingHoursEnabled } = channelConfig.value;
|
||||
const anyAgentOnline = availableAgents.value.length > 0;
|
||||
|
||||
if (workingHoursEnabled) {
|
||||
return isInBetweenTheWorkingHours.value;
|
||||
}
|
||||
return anyAgentOnline;
|
||||
});
|
||||
|
||||
const replyWaitMessage = computed(() => {
|
||||
const { workingHoursEnabled } = channelConfig.value;
|
||||
if (workingHoursEnabled) {
|
||||
return isOnline.value
|
||||
? replyTimeStatus.value
|
||||
: `${t('REPLY_TIME.BACK_IN')} ${timeLeftToBackInOnline.value}`;
|
||||
}
|
||||
return isOnline.value
|
||||
? replyTimeStatus.value
|
||||
: t('TEAM_AVAILABILITY.OFFLINE');
|
||||
});
|
||||
|
||||
const outOfOfficeMessage = computed(
|
||||
() => channelConfig.value.outOfOfficeMessage
|
||||
);
|
||||
|
||||
const isInBusinessHours = computed(() => {
|
||||
const { workingHoursEnabled } = channelConfig.value;
|
||||
return workingHoursEnabled ? isInBetweenTheWorkingHours.value : true;
|
||||
});
|
||||
|
||||
return {
|
||||
channelConfig,
|
||||
replyTimeStatus,
|
||||
currentDayAvailability,
|
||||
isInBetweenTheWorkingHours,
|
||||
isOnline,
|
||||
replyWaitMessage,
|
||||
outOfOfficeMessage,
|
||||
isInBusinessHours,
|
||||
};
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
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);
|
||||
|
||||
const calculateThemeClass = (mode, light, dark) => {
|
||||
if (isDarkModeAuto(mode)) return `${light} ${dark}`;
|
||||
return isDarkMode(mode) ? dark : light;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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)
|
||||
);
|
||||
|
||||
const getThemeClass = (light, dark) =>
|
||||
calculateThemeClass(darkMode.value, light, dark);
|
||||
|
||||
return {
|
||||
darkMode,
|
||||
prefersDarkMode,
|
||||
getThemeClass,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import {
|
||||
timeSlotParse,
|
||||
defaultTimeSlot,
|
||||
} from 'dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js';
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
import { generateRelativeTime } from 'shared/helpers/DateHelper';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
|
||||
const MINUTE_ROUNDING_FACTOR = 5;
|
||||
|
||||
export const useNextAvailabilityTime = () => {
|
||||
const { t } = useI18n();
|
||||
const dayNames = t('DAY_NAMES');
|
||||
const timeSlots = ref([...defaultTimeSlot]);
|
||||
const timeSlot = ref({});
|
||||
|
||||
const channelConfig = computed(() => window.chatwootWebChannel);
|
||||
const workingHours = computed(() => channelConfig.value.workingHours);
|
||||
const timeZoneValue = computed(() => channelConfig.value.timezone);
|
||||
const languageCode = computed(() => window.chatwootWebChannel.locale);
|
||||
|
||||
const newDateWithTimeZone = computed(() =>
|
||||
utcToZonedTime(new Date(), timeZoneValue.value)
|
||||
);
|
||||
const presentHour = computed(() => newDateWithTimeZone.value.getHours());
|
||||
const presentMinute = computed(() => newDateWithTimeZone.value.getMinutes());
|
||||
|
||||
const currentDay = computed(() => {
|
||||
const date = newDateWithTimeZone.value;
|
||||
const day = date.getDay();
|
||||
const getCurrentDay = Object.keys(dayNames).find(
|
||||
key => dayNames[key] === dayNames[day]
|
||||
);
|
||||
return Number(getCurrentDay);
|
||||
});
|
||||
|
||||
const currentDayWorkingHours = computed(() =>
|
||||
workingHours.value.find(slot => slot.day_of_week === currentDay.value)
|
||||
);
|
||||
|
||||
function getNextDay(day) {
|
||||
return (day + 1) % 7;
|
||||
}
|
||||
|
||||
function getNextWorkingHour(day) {
|
||||
const workingHour = workingHours.value.find(
|
||||
slot => slot.day_of_week === day
|
||||
);
|
||||
if (workingHour && !workingHour.closed_all_day) {
|
||||
return workingHour;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextDayWorkingHours = computed(() => {
|
||||
let nextDay = getNextDay(currentDay.value);
|
||||
let nextWorkingHour = getNextWorkingHour(nextDay);
|
||||
|
||||
while (!nextWorkingHour) {
|
||||
nextDay = getNextDay(nextDay);
|
||||
nextWorkingHour = getNextWorkingHour(nextDay);
|
||||
}
|
||||
return nextWorkingHour;
|
||||
});
|
||||
|
||||
const currentDayTimings = computed(() => {
|
||||
const {
|
||||
open_hour: openHour,
|
||||
open_minutes: openMinute,
|
||||
close_hour: closeHour,
|
||||
} = currentDayWorkingHours.value ?? {};
|
||||
return { openHour, openMinute, closeHour };
|
||||
});
|
||||
|
||||
const nextDayTimings = computed(() => {
|
||||
const { open_hour: openHour, open_minutes: openMinute } =
|
||||
nextDayWorkingHours.value ?? {};
|
||||
return { openHour, openMinute };
|
||||
});
|
||||
|
||||
const dayDiff = computed(() => {
|
||||
const nextDay = nextDayWorkingHours.value.day_of_week;
|
||||
const totalDays = 6;
|
||||
return nextDay > currentDay.value
|
||||
? nextDay - currentDay.value - 1
|
||||
: totalDays - currentDay.value + nextDay;
|
||||
});
|
||||
|
||||
const dayNameOfNextWorkingDay = computed(
|
||||
() => dayNames[nextDayWorkingHours.value.day_of_week]
|
||||
);
|
||||
|
||||
function getHoursAndMinutesUntilNextDayOpen(
|
||||
openHour,
|
||||
openMinutes,
|
||||
closeHour
|
||||
) {
|
||||
if (closeHour < openHour) {
|
||||
openHour += 24;
|
||||
}
|
||||
let diffMinutes =
|
||||
openHour * 60 +
|
||||
openMinutes -
|
||||
(presentHour.value * 60 + presentMinute.value);
|
||||
diffMinutes = diffMinutes < 0 ? diffMinutes + 24 * 60 : diffMinutes;
|
||||
const [hoursLeft, minutesLeft] = [
|
||||
Math.floor(diffMinutes / 60),
|
||||
diffMinutes % 60,
|
||||
];
|
||||
|
||||
return { hoursLeft, minutesLeft };
|
||||
}
|
||||
|
||||
const hoursAndMinutesBackInOnline = computed(() => {
|
||||
if (presentHour.value >= currentDayTimings.value.closeHour) {
|
||||
return getHoursAndMinutesUntilNextDayOpen(
|
||||
nextDayWorkingHours.value.open_all_day
|
||||
? 0
|
||||
: nextDayTimings.value.openHour,
|
||||
nextDayTimings.value.openMinute,
|
||||
currentDayTimings.value.closeHour
|
||||
);
|
||||
}
|
||||
return getHoursAndMinutesUntilNextDayOpen(
|
||||
currentDayTimings.value.openHour,
|
||||
currentDayTimings.value.openMinute,
|
||||
currentDayTimings.value.closeHour
|
||||
);
|
||||
});
|
||||
|
||||
const exactTimeInAmPm = computed(
|
||||
() =>
|
||||
`${
|
||||
timeSlot.value.day === currentDay.value
|
||||
? `at ${timeSlot.value.from}`
|
||||
: ''
|
||||
}`
|
||||
);
|
||||
|
||||
const hoursAndMinutesLeft = computed(() => {
|
||||
const { hoursLeft, minutesLeft } = hoursAndMinutesBackInOnline.value;
|
||||
const timeLeftChars = [];
|
||||
|
||||
if (hoursLeft > 0) {
|
||||
const roundedUpHoursLeft = minutesLeft > 0 ? hoursLeft + 1 : hoursLeft;
|
||||
const hourRelative = generateRelativeTime(
|
||||
roundedUpHoursLeft,
|
||||
'hour',
|
||||
languageCode.value
|
||||
);
|
||||
timeLeftChars.push(`${hourRelative}`);
|
||||
}
|
||||
|
||||
if (minutesLeft > 0 && hoursLeft === 0) {
|
||||
const roundedUpMinLeft =
|
||||
Math.ceil(minutesLeft / MINUTE_ROUNDING_FACTOR) *
|
||||
MINUTE_ROUNDING_FACTOR;
|
||||
const minRelative = generateRelativeTime(
|
||||
roundedUpMinLeft,
|
||||
'minutes',
|
||||
languageCode.value
|
||||
);
|
||||
timeLeftChars.push(`${minRelative}`);
|
||||
}
|
||||
|
||||
return timeLeftChars.join(' ');
|
||||
});
|
||||
|
||||
const hoursAndMinutesToBack = computed(() => {
|
||||
const { hoursLeft, minutesLeft } = hoursAndMinutesBackInOnline.value;
|
||||
if (hoursLeft >= 3) {
|
||||
return exactTimeInAmPm.value;
|
||||
}
|
||||
if (hoursLeft > 0 || minutesLeft > 0) {
|
||||
return hoursAndMinutesLeft.value;
|
||||
}
|
||||
return 'in some time';
|
||||
});
|
||||
|
||||
const timeLeftToBackInOnline = computed(() => {
|
||||
if (
|
||||
hoursAndMinutesBackInOnline.value.hoursLeft >= 24 ||
|
||||
(timeSlot.value.day !== currentDay.value && dayDiff.value === 0)
|
||||
) {
|
||||
const hourRelative = generateRelativeTime(
|
||||
dayDiff.value + 1,
|
||||
'days',
|
||||
languageCode.value
|
||||
);
|
||||
return `${hourRelative}`;
|
||||
}
|
||||
if (
|
||||
dayDiff.value >= 1 &&
|
||||
presentHour.value >= currentDayTimings.value.closeHour
|
||||
) {
|
||||
return `on ${dayNameOfNextWorkingDay.value}`;
|
||||
}
|
||||
return hoursAndMinutesToBack.value;
|
||||
});
|
||||
|
||||
function setTimeSlot() {
|
||||
const workingTimeSlots = workingHours.value;
|
||||
|
||||
const currentSlot =
|
||||
presentHour.value >= currentDayTimings.value.closeHour
|
||||
? nextDayWorkingHours.value
|
||||
: currentDayWorkingHours.value;
|
||||
|
||||
const slots = timeSlotParse(workingTimeSlots).length
|
||||
? timeSlotParse(workingTimeSlots)
|
||||
: defaultTimeSlot;
|
||||
timeSlots.value = slots;
|
||||
|
||||
timeSlot.value = timeSlots.value.find(
|
||||
slot => slot.day === currentSlot.day_of_week
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
dayNames,
|
||||
timeSlots,
|
||||
timeSlot,
|
||||
channelConfig,
|
||||
workingHours,
|
||||
newDateWithTimeZone,
|
||||
presentHour,
|
||||
presentMinute,
|
||||
currentDay,
|
||||
timeZoneValue,
|
||||
languageCode,
|
||||
currentDayWorkingHours,
|
||||
nextDayWorkingHours,
|
||||
currentDayTimings,
|
||||
nextDayTimings,
|
||||
dayDiff,
|
||||
dayNameOfNextWorkingDay,
|
||||
hoursAndMinutesBackInOnline,
|
||||
exactTimeInAmPm,
|
||||
hoursAndMinutesLeft,
|
||||
hoursAndMinutesToBack,
|
||||
timeLeftToBackInOnline,
|
||||
setTimeSlot,
|
||||
};
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useRouter, useRoute } from 'dashboard/composables/route';
|
||||
|
||||
/**
|
||||
* Composable for replacing the current route with a new one if it's different.
|
||||
* @param {string} name - The name of the route to replace with.
|
||||
* @param {Object} params - The params to pass to the new route.
|
||||
* @returns {Function} A function that replaces the route when called.
|
||||
*/
|
||||
export const useReplaceRoute = () => {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
return (name, params = {}) => {
|
||||
if (route.name !== name) {
|
||||
return router.replace({ name, params });
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export default useReplaceRoute;
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
if (workingHoursEnabled) {
|
||||
return this.isOnline
|
||||
? this.replyTimeStatus
|
||||
: `${this.$t('REPLY_TIME.BACK_IN')} ${this.timeLeftToBackInOnline}`;
|
||||
: `${this.$t('REPLY_TIME.BACK_IN')} ${this.timeLeftToBackInOnline}`; // timeLeftToBackInOnline should be pulled in from the useNextAvailabilityTime composable
|
||||
}
|
||||
return this.isOnline
|
||||
? this.replyTimeStatus
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({ darkMode: 'appConfig/darkMode' }),
|
||||
prefersDarkMode() {
|
||||
const isOSOnDarkMode =
|
||||
this.darkMode === 'auto' &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
return isOSOnDarkMode || this.darkMode === 'dark';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
$dm(light, dark) {
|
||||
if (this.darkMode === 'light') {
|
||||
return light;
|
||||
}
|
||||
if (this.darkMode === 'dark') {
|
||||
return dark;
|
||||
}
|
||||
return light + ' ' + dark;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
methods: {
|
||||
async replaceRoute(name, params = {}) {
|
||||
if (this.$route.name !== name) {
|
||||
return this.$router.replace({ name, params });
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import darkModeMixin from '../darkModeMixin';
|
||||
import Vuex from 'vuex';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const darkModeValues = ['light', 'auto'];
|
||||
|
||||
describe('darkModeMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
'appConfig/darkMode': () => darkModeValues[0],
|
||||
};
|
||||
store = new Vuex.Store({ getters });
|
||||
});
|
||||
|
||||
it('if light theme', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [darkModeMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100');
|
||||
});
|
||||
|
||||
it('if auto theme', () => {
|
||||
getters = {
|
||||
'appConfig/darkMode': () => darkModeValues[2],
|
||||
};
|
||||
store = new Vuex.Store({ getters });
|
||||
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [darkModeMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100 bg-600');
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,6 @@ import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoad
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import useReplaceRoute from 'widget/composables/useReplaceRoute';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
|
||||
@@ -17,12 +15,7 @@ export default {
|
||||
TeamAvailability,
|
||||
ArticleCardSkeletonLoader,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const replaceRoute = useReplaceRoute();
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
return { prefersDarkMode, replaceRoute };
|
||||
},
|
||||
mixins: [configMixin, routerMixin, darkModeMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
availableAgents: 'agent/availableAgents',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import PreChatForm from '../components/PreChat/Form.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import useReplaceRoute from 'widget/composables/useReplaceRoute';
|
||||
import routerMixin from '../mixins/routerMixin';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import { ON_CONVERSATION_CREATED } from '../constants/widgetBusEvents';
|
||||
|
||||
@@ -9,11 +9,7 @@ export default {
|
||||
components: {
|
||||
PreChatForm,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const replaceRoute = useReplaceRoute();
|
||||
return { replaceRoute };
|
||||
},
|
||||
mixins: [configMixin, routerMixin],
|
||||
mounted() {
|
||||
this.$emitter.on(ON_CONVERSATION_CREATED, () => {
|
||||
// Redirect to messages page after conversation is created
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate": "^2.5.2",
|
||||
"@chatwoot/ninja-keys": "1.2.3",
|
||||
"@chatwoot/prosemirror-schema": "1.0.17",
|
||||
"@chatwoot/prosemirror-schema": "1.0.16",
|
||||
"@chatwoot/utils": "^0.0.25",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@june-so/analytics-next": "^2.0.0",
|
||||
|
||||
@@ -2914,10 +2914,10 @@
|
||||
hotkeys-js "3.8.7"
|
||||
lit "2.2.6"
|
||||
|
||||
"@chatwoot/prosemirror-schema@1.0.17":
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.17.tgz#5c04a94c8200b303d943369b2534730d5ba15a64"
|
||||
integrity sha512-3hwBSMdb5a+Ilct0fNSlAvQu/9+pnyI/FoGshVRC9Hn1tCdZdS7LUlxOuDweta3HneVclhE+SxJN5lM33v5WGw==
|
||||
"@chatwoot/prosemirror-schema@1.0.16":
|
||||
version "1.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.16.tgz#63cfd4cff0f138466a95fedba5d237409a0b418e"
|
||||
integrity sha512-qFPHPrfkG14vB0C/8+kF6huAvz4c2h6s07WKznZqUZ5CgJYd+fESQ49CScOYwOTO6luV4ghILv9kAVRjiaenOQ==
|
||||
dependencies:
|
||||
markdown-it-sup "^1.0.0"
|
||||
prosemirror-commands "^1.1.4"
|
||||
|
||||
Reference in New Issue
Block a user