Compare commits

...
Author SHA1 Message Date
Muhsin Keloth 80591fa9d2 feat: Add copy and paste image to editor 2024-08-29 15:49:40 +05:30
Muhsin Keloth 05b1771bbe Merge branch 'fix/support-image-paste-editor' of https://github.com/chatwoot/chatwoot into fix/support-image-paste-editor 2024-08-29 14:15:25 +05:30
Muhsin Keloth 3af593256b Merge branch 'develop' into fix/support-image-paste-editor 2024-08-29 14:15:17 +05:30
Muhsin KelothandGitHub 45028561e0 Merge branch 'develop' into fix/support-image-paste-editor 2024-08-29 11:46:02 +05:30
Muhsin Keloth 9e17e41ac7 chore: remove success message for image upload 2024-08-29 11:40:52 +05:30
31e7663258 feat: Update the design for the Inbox management console (#10043)
This is the continuation of the design update PR. This changes the design for the inbox pages.
---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-08-29 11:19:32 +05:30
Muhsin Keloth fa44166609 feat: add support for pasting image in editor 2024-08-28 15:31:51 +05:30
3b5f5b41ad chore: Replace campaign mixin with composable [CW-3463] (#9987)
# Pull Request Template

## Description

Repalces campaignMixin and its usage with the new useCampaign mixin

Fixes
https://linear.app/chatwoot/issue/CW-3463/rewrite-campaignmixin-mixin-to-a-composable

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-08-28 00:53:18 +05:30
15 changed files with 409 additions and 280 deletions
@@ -120,7 +120,6 @@ export default {
if (fileUrl) {
this.onImageUploadStart(fileUrl);
}
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
} catch (error) {
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
}
@@ -153,6 +152,45 @@ export default {
this.editorView.updateState(this.state);
this.focusEditorInputField();
},
insertImage(src, alt = '') {
const { selection } = this.editorView.state;
const from = selection.from;
const node = this.editorView.state.schema.nodes.image.create({
src,
alt,
});
const paragraphNode = this.editorView.state.schema.node('paragraph');
if (node) {
const tr = this.editorView.state.tr
.replaceSelectionWith(paragraphNode)
.insert(from + 1, node);
this.editorView.dispatch(tr.scrollIntoView());
this.focusEditorInputField();
}
},
handlePastedText(text, view) {
const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
let match;
let lastIndex = 0;
while ((match = markdownImageRegex.exec(text)) !== null) {
// Insert text before the image
if (match.index > lastIndex) {
const textBefore = text.slice(lastIndex, match.index);
view.dispatch(view.state.tr.insertText(textBefore));
}
// // Insert the image
const [, alt, src] = match;
this.insertImage(src, alt);
lastIndex = markdownImageRegex.lastIndex;
}
// Insert any remaining text after the last image
if (lastIndex < text.length) {
const remainingText = text.slice(lastIndex);
view.dispatch(view.state.tr.insertText(remainingText));
}
},
createEditorView() {
this.editorView = new EditorView(this.$refs.editor, {
state: this.state,
@@ -173,6 +211,21 @@ export default {
blur: () => {
this.onBlur();
},
paste: (view, event) => {
const clipboardData = event.clipboardData;
const pastedText = clipboardData.getData('text/plain');
const files = clipboardData.files;
if (files.length > 0) {
// Handle direct image file paste
files.forEach(file => this.uploadImageToStorage(file));
event.preventDefault();
} else if (pastedText) {
// Handle potential markdown image links
this.handlePastedText(pastedText, view);
event.preventDefault();
}
},
},
});
},
@@ -9,6 +9,7 @@ const FEATURE_HELP_URLS = {
custom_attributes: 'https://chwt.app/hc/custom-attributes',
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
help_center: 'https://chwt.app/hc/help-center',
inboxes: 'https://chwt.app/hc/inboxes',
integrations: 'https://chwt.app/hc/integrations',
labels: 'https://chwt.app/hc/labels',
macros: 'https://chwt.app/hc/macros',
@@ -1,7 +1,8 @@
{
"INBOX_MGMT": {
"HEADER": "Inboxes",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
"LEARN_MORE": "Learn more about inboxes",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
@@ -745,6 +746,18 @@
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
},
"CHANNELS": {
"MESSENGER": "Messenger",
"WEB_WIDGET": "Website",
"TWITTER_PROFILE": "Twitter",
"TWILIO_SMS": "Twilio SMS",
"WHATSAPP": "WhatsApp",
"SMS": "SMS",
"EMAIL": "Email",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
}
}
}
@@ -4,7 +4,7 @@ import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import campaignMixin from 'shared/mixins/campaignMixin';
import { useCampaign } from 'shared/composables/useCampaign';
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
import { URLPattern } from 'urlpattern-polyfill';
import { CAMPAIGNS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
@@ -14,9 +14,9 @@ export default {
WootDateTimePicker,
WootMessageEditor,
},
mixins: [campaignMixin],
setup() {
return { v$: useVuelidate() };
const { campaignType, isOngoingType, isOneOffType } = useCampaign();
return { v$: useVuelidate(), campaignType, isOngoingType, isOneOffType };
},
data() {
return {
@@ -1,7 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import campaignMixin from 'shared/mixins/campaignMixin';
import { useCampaign } from 'shared/composables/useCampaign';
import CampaignsTable from './CampaignsTable.vue';
import EditCampaign from './EditCampaign.vue';
export default {
@@ -9,13 +9,16 @@ export default {
CampaignsTable,
EditCampaign,
},
mixins: [campaignMixin],
props: {
type: {
type: String,
default: '',
},
},
setup() {
const { campaignType } = useCampaign();
return { campaignType };
},
data() {
return {
showEditPopup: false,
@@ -1,7 +1,7 @@
<script>
import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import campaignMixin from 'shared/mixins/campaignMixin';
import { useCampaign } from 'shared/composables/useCampaign';
import CampaignCard from './CampaignCard.vue';
export default {
@@ -10,9 +10,6 @@ export default {
Spinner,
CampaignCard,
},
mixins: [campaignMixin],
props: {
campaigns: {
type: Array,
@@ -27,7 +24,10 @@ export default {
default: false,
},
},
setup() {
const { isOngoingType } = useCampaign();
return { isOngoingType };
},
computed: {
currentInboxId() {
return this.$route.params.inboxId;
@@ -4,14 +4,13 @@ import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import campaignMixin from 'shared/mixins/campaignMixin';
import { useCampaign } from 'shared/composables/useCampaign';
import { URLPattern } from 'urlpattern-polyfill';
export default {
components: {
WootMessageEditor,
},
mixins: [campaignMixin],
props: {
selectedCampaign: {
type: Object,
@@ -19,7 +18,8 @@ export default {
},
},
setup() {
return { v$: useVuelidate() };
const { isOngoingType } = useCampaign();
return { v$: useVuelidate(), isOngoingType };
},
data() {
return {
@@ -1,5 +1,5 @@
<script>
import campaignMixin from 'shared/mixins/campaignMixin';
import { useCampaign } from 'shared/composables/useCampaign';
import Campaign from './Campaign.vue';
import AddCampaign from './AddCampaign.vue';
@@ -8,7 +8,10 @@ export default {
Campaign,
AddCampaign,
},
mixins: [campaignMixin],
setup() {
const { isOngoingType } = useCampaign();
return { isOngoingType };
},
data() {
return { showAddPopup: false };
},
@@ -1,219 +1,177 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { useAlert } from 'dashboard/composables';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useAccount } from 'dashboard/composables/useAccount';
import Settings from './Settings.vue';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import SettingsLayout from '../SettingsLayout.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import { computed, ref } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import ChannelName from './components/ChannelName.vue';
export default {
components: {
Settings,
},
mixins: [globalConfigMixin],
setup() {
const { isAdmin } = useAdmin();
const { accountScopedUrl } = useAccount();
return {
isAdmin,
accountScopedUrl,
};
},
data() {
return {
loading: {},
showSettings: false,
showDeletePopup: false,
selectedInbox: {},
};
},
computed: {
...mapGetters({
inboxesList: 'inboxes/getInboxes',
globalConfig: 'globalConfig/get',
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedInbox.name
}`;
},
deleteRejectText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedInbox.name
}`;
},
confirmDeleteMessage() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedInbox.name
}?`;
},
confirmPlaceHolderText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
inboxName: this.selectedInbox.name,
})}`;
},
},
methods: {
twilioChannelName(item) {
const { medium = '' } = item;
if (medium === 'whatsapp') return 'WhatsApp';
return 'Twilio SMS';
},
openSettings(inbox) {
this.showSettings = true;
this.selectedInbox = inbox;
},
closeSettings() {
this.showSettings = false;
this.selectedInbox = {};
},
async deleteInbox({ id }) {
try {
await this.$store.dispatch('inboxes/delete', id);
useAlert(this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
}
},
const getters = useStoreGetters();
const store = useStore();
const { t } = useI18n();
const { isAdmin } = useAdmin();
confirmDeletion() {
this.deleteInbox(this.selectedInbox);
this.closeDelete();
},
openDelete(inbox) {
this.showDeletePopup = true;
this.selectedInbox = inbox;
},
closeDelete() {
this.showDeletePopup = false;
this.selectedInbox = {};
},
},
const showDeletePopup = ref(false);
const selectedInbox = ref({});
const inboxesList = computed(() => getters['inboxes/getInboxes'].value);
const uiFlags = computed(() => getters['inboxes/getUIFlags'].value);
const deleteConfirmText = computed(
() => `${t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${selectedInbox.value.name}`
);
const deleteRejectText = computed(
() => `${t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${selectedInbox.value.name}`
);
const confirmDeleteMessage = computed(
() => `${t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${selectedInbox.value.name}?`
);
const confirmPlaceHolderText = computed(
() =>
`${t('INBOX_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
inboxName: selectedInbox.value.name,
})}`
);
const deleteInbox = async ({ id }) => {
try {
await store.dispatch('inboxes/delete', id);
useAlert(t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
}
};
const closeDelete = () => {
showDeletePopup.value = false;
selectedInbox.value = {};
};
const confirmDeletion = () => {
deleteInbox(selectedInbox.value);
closeDelete();
};
const openDelete = inbox => {
showDeletePopup.value = true;
selectedInbox.value = inbox;
};
</script>
<template>
<div class="flex-1 overflow-auto">
<div class="flex flex-row gap-4 p-8">
<div class="w-full lg:w-3/5">
<p
v-if="!inboxesList.length"
class="flex flex-col items-center justify-center h-full"
>
{{ $t('INBOX_MGMT.LIST.404') }}
<SettingsLayout
:no-records-found="!inboxesList.length"
:no-records-message="$t('INBOX_MGMT.LIST.404')"
:is-loading="uiFlags.isFetching"
>
<template #header>
<BaseSettingsHeader
:title="$t('INBOX_MGMT.HEADER')"
:description="$t('INBOX_MGMT.DESCRIPTION')"
:link-text="$t('INBOX_MGMT.LEARN_MORE')"
feature-name="inboxes"
>
<template #actions>
<router-link
v-if="isAdmin"
:to="accountScopedUrl('settings/inboxes/new')"
class="button nice rounded-md"
:to="{ name: 'settings_inbox_new' }"
>
<fluent-icon icon="add-circle" />
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
</router-link>
</p>
<table v-if="inboxesList.length" class="woot-table">
<tbody>
<tr v-for="item in inboxesList" :key="item.id">
<td>
<img
v-if="item.avatar_url"
class="woot-thumbnail"
:src="item.avatar_url"
alt="No Page Image"
</template>
</BaseSettingsHeader>
</template>
<template #body>
<table
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
>
<tbody
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
>
<tr v-for="inbox in inboxesList" :key="inbox.id">
<td class="py-4 ltr:pr-4 rtl:pl-4">
<div class="flex items-center flex-row gap-4">
<Thumbnail
v-if="inbox.avatar_url"
class="bg-black-50 dark:bg-black-800 rounded-full p-2 ring ring-opacity-20 dark:ring-opacity-80 ring-black-100 dark:ring-black-900 border border-slate-100 dark:border-slate-700/50 shadow-sm"
:src="inbox.avatar_url"
:username="inbox.name"
size="48px"
/>
<img
<div
v-else
class="woot-thumbnail"
src="~dashboard/assets/images/flag.svg"
alt="No Page Image"
/>
</td>
<!-- Short Code -->
<td>
<span class="agent-name">{{ item.name }}</span>
<span v-if="item.channel_type === 'Channel::FacebookPage'">
{{ 'Facebook' }}
</span>
<span v-if="item.channel_type === 'Channel::WebWidget'">
{{ 'Website' }}
</span>
<span v-if="item.channel_type === 'Channel::TwitterProfile'">
{{ 'Twitter' }}
</span>
<span v-if="item.channel_type === 'Channel::TwilioSms'">
{{ twilioChannelName(item) }}
</span>
<span v-if="item.channel_type === 'Channel::Whatsapp'">
{{ 'Whatsapp' }}
</span>
<span v-if="item.channel_type === 'Channel::Sms'">
{{ 'Sms' }}
</span>
<span v-if="item.channel_type === 'Channel::Email'">
{{ 'Email' }}
</span>
<span v-if="item.channel_type === 'Channel::Telegram'">
{{ 'Telegram' }}
</span>
<span v-if="item.channel_type === 'Channel::Line'">
{{ 'Line' }}
</span>
<span v-if="item.channel_type === 'Channel::Api'">
{{ globalConfig.apiChannelName || 'API' }}
</span>
</td>
<!-- Action Buttons -->
<td>
<div class="button-wrapper">
<router-link
:to="accountScopedUrl(`settings/inboxes/${item.id}`)"
class="w-12 h-12 bg-black-50 dark:bg-black-800 rounded-full p-2 ring ring-opacity-20 dark:ring-opacity-80 ring-black-100 dark:ring-black-900 border border-slate-100 dark:border-slate-700/50 shadow-sm block"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
class="opacity-80 p-1"
>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
variant="smooth"
size="tiny"
icon="settings"
color-scheme="secondary"
class-names="grey-btn"
<path
fill="currentColor"
d="M1 12c0-5.185 0-7.778 1.61-9.39C4.223 1 6.816 1 12 1s7.778 0 9.39 1.61C23 4.223 23 6.816 23 12s0 7.778-1.61 9.39C19.777 23 17.184 23 12 23s-7.778 0-9.39-1.61C1 19.777 1 17.184 1 12"
opacity=".35"
/>
</router-link>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
variant="smooth"
color-scheme="alert"
size="tiny"
class-names="grey-btn"
:is-loading="loading[item.id]"
icon="dismiss-circle"
@click="openDelete(item)"
<path
fill="currentColor"
d="M2.61 21.389c1.612 1.61 4.205 1.61 9.39 1.61s7.778 0 9.39-1.61c1.492-1.493 1.601-3.829 1.61-8.29h-3.476c-.996 0-1.494 0-1.931.202c-.438.201-.762.58-1.41 1.335l-.666.777c-.648.756-.972 1.134-1.41 1.335s-.935.202-1.93.202h-.353c-.996 0-1.494 0-1.931-.202c-.438-.2-.762-.579-1.41-1.335l-.666-.777c-.648-.756-.972-1.134-1.41-1.335s-.935-.201-1.93-.201H1c.008 4.46.118 6.796 1.61 8.289"
/>
</svg>
</div>
<div>
<span class="block font-medium capitalize">
{{ inbox.name }}
</span>
<ChannelName
:channel-type="inbox.channel_type"
:medium="inbox.medium"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
<div class="hidden w-1/3 lg:block">
<span
v-dompurify-html="
useInstallationName(
$t('INBOX_MGMT.SIDEBAR_TXT'),
globalConfig.installationName
)
"
/>
</div>
</div>
<Settings
v-if="showSettings"
:show.sync="showSettings"
:on-close="closeSettings"
:inbox="selectedInbox"
/>
<td class="py-4">
<div class="flex gap-1 justify-end">
<router-link
:to="{
name: 'settings_inbox_show',
params: { inboxId: inbox.id },
}"
>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
variant="smooth"
size="tiny"
icon="settings"
color-scheme="secondary"
class-names="grey-btn"
/>
</router-link>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
variant="smooth"
color-scheme="alert"
size="tiny"
class-names="grey-btn"
icon="dismiss-circle"
@click="openDelete(inbox)"
/>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<woot-confirm-delete-modal
v-if="showDeletePopup"
@@ -227,5 +185,5 @@ export default {
@onConfirm="confirmDeletion"
@onClose="closeDelete"
/>
</div>
</SettingsLayout>
</template>
@@ -0,0 +1,55 @@
<script setup>
import { useI18n } from 'dashboard/composables/useI18n';
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
const props = defineProps({
channelType: {
type: String,
required: true,
},
medium: {
type: String,
default: '',
},
});
const getters = useStoreGetters();
const { t } = useI18n();
const globalConfig = computed(() => getters['globalConfig/get'].value);
const i18nMap = {
'Channel::FacebookPage': 'MESSENGER',
'Channel::WebWidget': 'WEB_WIDGET',
'Channel::TwitterProfile': 'TWITTER_PROFILE',
'Channel::TwilioSms': 'TWILIO_SMS',
'Channel::Whatsapp': 'WHATSAPP',
'Channel::Sms': 'SMS',
'Channel::Email': 'EMAIL',
'Channel::Telegram': 'TELEGRAM',
'Channel::Line': 'LINE',
'Channel::Api': 'API',
};
const twilioChannelName = () => {
if (props.medium === 'whatsapp') {
return t(`INBOX_MGMT.CHANNELS.WHATSAPP`);
}
return t(`INBOX_MGMT.CHANNELS.TWILIO_SMS`);
};
const readableChannelName = computed(() => {
if (props.channelType === 'Channel::Api') {
return globalConfig.value.apiChannelName || t('INBOX_MGMT.CHANNELS.API');
}
if (props.channelType === 'Channel::TwilioSms') {
return twilioChannelName();
}
return t(`INBOX_MGMT.CHANNELS.${i18nMap[props.channelType]}`);
});
</script>
<template>
<span>
{{ readableChannelName }}
</span>
</template>
@@ -1,8 +1,8 @@
/* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../../helper/URLHelper';
import channelFactory from './channel-factory';
const SettingsContent = () => import('../Wrapper.vue');
const SettingWrapper = () => import('../SettingsWrapper.vue');
const InboxHome = () => import('./Index.vue');
const Settings = () => import('./Settings.vue');
const InboxChannel = () => import('./InboxChannels.vue');
@@ -12,6 +12,24 @@ const FinishSetup = () => import('./FinishSetup.vue');
export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/inboxes'),
component: SettingWrapper,
children: [
{
path: '',
redirect: 'list',
},
{
path: 'list',
name: 'settings_inbox_list',
component: InboxHome,
meta: {
permissions: ['administrator'],
},
},
],
},
{
path: frontendURL('accounts/:accountId/settings/inboxes'),
component: SettingsContent,
@@ -26,18 +44,6 @@ export default {
};
},
children: [
{
path: '',
redirect: 'list',
},
{
path: 'list',
name: 'settings_inbox_list',
component: InboxHome,
meta: {
permissions: ['administrator'],
},
},
{
path: 'new',
component: InboxChannel,
@@ -0,0 +1,47 @@
import { describe, it, expect, vi } from 'vitest';
import { useCampaign } from '../useCampaign';
import { useRoute } from 'dashboard/composables/route';
import { CAMPAIGN_TYPES } from '../../constants/campaign';
// Mock the useRoute composable
vi.mock('dashboard/composables/route', () => ({
useRoute: vi.fn(),
}));
describe('useCampaign', () => {
it('returns the correct campaign type for ongoing campaigns', () => {
useRoute.mockReturnValue({ name: 'ongoing_campaigns' });
const { campaignType } = useCampaign();
expect(campaignType.value).toBe(CAMPAIGN_TYPES.ONGOING);
});
it('returns the correct campaign type for one-off campaigns', () => {
useRoute.mockReturnValue({ name: 'one_off' });
const { campaignType } = useCampaign();
expect(campaignType.value).toBe(CAMPAIGN_TYPES.ONE_OFF);
});
it('isOngoingType returns true for ongoing campaigns', () => {
useRoute.mockReturnValue({ name: 'ongoing_campaigns' });
const { isOngoingType } = useCampaign();
expect(isOngoingType.value).toBe(true);
});
it('isOngoingType returns false for one-off campaigns', () => {
useRoute.mockReturnValue({ name: 'one_off' });
const { isOngoingType } = useCampaign();
expect(isOngoingType.value).toBe(false);
});
it('isOneOffType returns true for one-off campaigns', () => {
useRoute.mockReturnValue({ name: 'one_off' });
const { isOneOffType } = useCampaign();
expect(isOneOffType.value).toBe(true);
});
it('isOneOffType returns false for ongoing campaigns', () => {
useRoute.mockReturnValue({ name: 'ongoing_campaigns' });
const { isOneOffType } = useCampaign();
expect(isOneOffType.value).toBe(false);
});
});
@@ -0,0 +1,43 @@
import { computed } from 'vue';
import { useRoute } from 'dashboard/composables/route';
import { CAMPAIGN_TYPES } from '../constants/campaign';
/**
* Composable to manage campaign types.
*
* @returns {Object} - Computed properties for campaign type and its checks.
*/
export const useCampaign = () => {
const route = useRoute();
/**
* Computed property to determine the current campaign type based on the route name.
*/
const campaignType = computed(() => {
const campaignTypeMap = {
ongoing_campaigns: CAMPAIGN_TYPES.ONGOING,
one_off: CAMPAIGN_TYPES.ONE_OFF,
};
return campaignTypeMap[route.name];
});
/**
* Computed property to check if the current campaign type is 'ongoing'.
*/
const isOngoingType = computed(() => {
return campaignType.value === CAMPAIGN_TYPES.ONGOING;
});
/**
* Computed property to check if the current campaign type is 'one-off'.
*/
const isOneOffType = computed(() => {
return campaignType.value === CAMPAIGN_TYPES.ONE_OFF;
});
return {
campaignType,
isOngoingType,
isOneOffType,
};
};
@@ -1,19 +0,0 @@
import { CAMPAIGN_TYPES } from '../constants/campaign';
export default {
computed: {
campaignType() {
const campaignTypeMap = {
ongoing_campaigns: CAMPAIGN_TYPES.ONGOING,
one_off: CAMPAIGN_TYPES.ONE_OFF,
};
return campaignTypeMap[this.$route.name];
},
isOngoingType() {
return this.campaignType === CAMPAIGN_TYPES.ONGOING;
},
isOneOffType() {
return this.campaignType === CAMPAIGN_TYPES.ONE_OFF;
},
},
};
@@ -1,34 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import campaignMixin from '../campaignMixin';
const buildComponent = routeName => ({
template: '<div></div>',
computed: {
$route() {
return { name: routeName };
},
},
mixins: [campaignMixin],
});
describe('campaignMixin', () => {
beforeEach(() => {
global.window = Object.create(window);
});
it('returns the correct campaign type', () => {
const Component = buildComponent('one_off');
const wrapper = shallowMount(Component);
expect(wrapper.vm.campaignType).toBe('one_off');
});
it('isOneOffType returns true if path is one_off', () => {
const Component = buildComponent('one_off');
const wrapper = shallowMount(Component);
expect(wrapper.vm.isOneOffType).toBe(true);
});
it('isOngoingType returns true if path is ongoing_campaigns', () => {
const Component = buildComponent('ongoing_campaigns');
const wrapper = shallowMount(Component);
expect(wrapper.vm.isOngoingType).toBe(true);
});
});