feat(v4): Update the campaigns page design (#10371)
<img width="1439" alt="Screenshot 2024-10-30 at 8 58 12 PM" src="https://github.com/user-attachments/assets/26231270-5e73-40fb-9efa-c661585ebe7c"> Fixes https://linear.app/chatwoot/project/campaign-redesign-f82bede26ca7/overview --------- Co-authored-by: Pranav <pranavrajs@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||||
|
import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
|
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import LiveChatCampaignDetails from './LiveChatCampaignDetails.vue';
|
||||||
|
import SMSCampaignDetails from './SMSCampaignDetails.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
isLiveChatType: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
inbox: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
scheduledAt: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['edit', 'delete']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const STATUS_COMPLETED = 'completed';
|
||||||
|
|
||||||
|
const { formatMessage } = useMessageFormatter();
|
||||||
|
|
||||||
|
const isActive = computed(() =>
|
||||||
|
props.isLiveChatType ? props.isEnabled : props.status !== STATUS_COMPLETED
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusTextColor = computed(() => ({
|
||||||
|
'text-n-teal-11': isActive.value,
|
||||||
|
'text-n-slate-12': !isActive.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const campaignStatus = computed(() => {
|
||||||
|
if (props.isLiveChatType) {
|
||||||
|
return props.isEnabled
|
||||||
|
? t('CAMPAIGN.LIVE_CHAT.CARD.STATUS.ENABLED')
|
||||||
|
: t('CAMPAIGN.LIVE_CHAT.CARD.STATUS.DISABLED');
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.status === STATUS_COMPLETED
|
||||||
|
? t('CAMPAIGN.SMS.CARD.STATUS.COMPLETED')
|
||||||
|
: t('CAMPAIGN.SMS.CARD.STATUS.SCHEDULED');
|
||||||
|
});
|
||||||
|
|
||||||
|
const inboxName = computed(() => props.inbox?.name || '');
|
||||||
|
|
||||||
|
const inboxIcon = computed(() => {
|
||||||
|
const { phone_number: phoneNumber, channel_type: type } = props.inbox;
|
||||||
|
return getInboxIconByType(type, phoneNumber);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardLayout class="flex flex-row justify-between flex-1 gap-8" layout="row">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<div class="flex justify-between gap-3 w-fit">
|
||||||
|
<span
|
||||||
|
class="text-base font-medium capitalize text-n-slate-12 line-clamp-1"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="text-xs font-medium inline-flex items-center h-6 px-2 py-0.5 rounded-md bg-n-alpha-2"
|
||||||
|
:class="statusTextColor"
|
||||||
|
>
|
||||||
|
{{ campaignStatus }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-dompurify-html="formatMessage(message)"
|
||||||
|
class="text-sm text-n-slate-11 line-clamp-1 [&>p]:mb-0 h-6"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center w-full h-6 gap-2 overflow-hidden">
|
||||||
|
<LiveChatCampaignDetails
|
||||||
|
v-if="isLiveChatType"
|
||||||
|
:sender="sender"
|
||||||
|
:inbox-name="inboxName"
|
||||||
|
:inbox-icon="inboxIcon"
|
||||||
|
/>
|
||||||
|
<SMSCampaignDetails
|
||||||
|
v-else
|
||||||
|
:inbox-name="inboxName"
|
||||||
|
:inbox-icon="inboxIcon"
|
||||||
|
:scheduled-at="scheduledAt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-end w-20 gap-2">
|
||||||
|
<Button
|
||||||
|
v-if="isLiveChatType"
|
||||||
|
variant="faded"
|
||||||
|
size="sm"
|
||||||
|
color="slate"
|
||||||
|
icon="i-lucide-sliders-vertical"
|
||||||
|
@click="emit('edit')"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="ruby"
|
||||||
|
size="sm"
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
@click="emit('delete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CardLayout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sender: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
inboxName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
inboxIcon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const senderName = computed(
|
||||||
|
() => props.sender?.name || t('CAMPAIGN.LIVE_CHAT.CARD.CAMPAIGN_DETAILS.BOT')
|
||||||
|
);
|
||||||
|
|
||||||
|
const senderThumbnailSrc = computed(() => props.sender?.thumbnail);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="flex-shrink-0 text-sm text-n-slate-11 whitespace-nowrap">
|
||||||
|
{{ t('CAMPAIGN.LIVE_CHAT.CARD.CAMPAIGN_DETAILS.SENT_BY') }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||||
|
<Thumbnail
|
||||||
|
:author="sender || { name: senderName }"
|
||||||
|
:name="senderName"
|
||||||
|
:src="senderThumbnailSrc"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ senderName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="flex-shrink-0 text-sm text-n-slate-11 whitespace-nowrap">
|
||||||
|
{{ t('CAMPAIGN.LIVE_CHAT.CARD.CAMPAIGN_DETAILS.FROM') }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||||
|
<Icon :icon="inboxIcon" class="flex-shrink-0 text-n-slate-12 size-3" />
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ inboxName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
defineProps({
|
||||||
|
inboxName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
inboxIcon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
scheduledAt: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="flex-shrink-0 text-sm text-n-slate-11 whitespace-nowrap">
|
||||||
|
{{ t('CAMPAIGN.SMS.CARD.CAMPAIGN_DETAILS.SENT_FROM') }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||||
|
<Icon :icon="inboxIcon" class="flex-shrink-0 text-n-slate-12 size-3" />
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ inboxName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="flex-shrink-0 text-sm text-n-slate-11 whitespace-nowrap">
|
||||||
|
{{ t('CAMPAIGN.SMS.CARD.CAMPAIGN_DETAILS.ON') }}
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 text-sm font-medium truncate text-n-slate-12">
|
||||||
|
{{ messageStamp(new Date(scheduledAt), 'LLL d, h:mm a') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup>
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
headerTitle: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['click', 'close']);
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
emit('click');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background">
|
||||||
|
<header class="sticky top-0 z-10 px-6 lg:px-0">
|
||||||
|
<div class="w-full max-w-[900px] mx-auto">
|
||||||
|
<div class="flex items-center justify-between w-full h-20 gap-2">
|
||||||
|
<span class="text-xl font-medium text-n-slate-12">
|
||||||
|
{{ headerTitle }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-on-clickaway="() => emit('close')"
|
||||||
|
class="relative group/campaign-button"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
:label="buttonLabel"
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
size="sm"
|
||||||
|
class="group-hover/campaign-button:brightness-110"
|
||||||
|
@click="handleButtonClick"
|
||||||
|
/>
|
||||||
|
<slot name="action" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
|
||||||
|
<div class="w-full max-w-[900px] mx-auto py-4">
|
||||||
|
<slot name="default" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
export const ONGOING_CAMPAIGN_EMPTY_STATE_CONTENT = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Chatbot Assistance',
|
||||||
|
inbox: {
|
||||||
|
id: 2,
|
||||||
|
name: 'PaperLayer Website',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
phone_number: '',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Alexa Rivera',
|
||||||
|
},
|
||||||
|
message: 'Hello! 👋 Need help with our chatbot features? Feel free to ask!',
|
||||||
|
campaign_status: 'active',
|
||||||
|
enabled: true,
|
||||||
|
campaign_type: 'ongoing',
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://www.chatwoot.com/features/chatbot/',
|
||||||
|
time_on_page: 10,
|
||||||
|
},
|
||||||
|
trigger_only_during_business_hours: true,
|
||||||
|
created_at: '2024-10-24T13:10:26.636Z',
|
||||||
|
updated_at: '2024-10-24T13:10:26.636Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Pricing Information Support',
|
||||||
|
inbox: {
|
||||||
|
id: 2,
|
||||||
|
name: 'PaperLayer Website',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
phone_number: '',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Jamie Lee',
|
||||||
|
},
|
||||||
|
message: 'Hello! 👋 Any questions on pricing? I’m here to help!',
|
||||||
|
campaign_status: 'active',
|
||||||
|
enabled: false,
|
||||||
|
campaign_type: 'ongoing',
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://www.chatwoot.com/pricings',
|
||||||
|
time_on_page: 10,
|
||||||
|
},
|
||||||
|
trigger_only_during_business_hours: false,
|
||||||
|
created_at: '2024-10-24T13:11:08.763Z',
|
||||||
|
updated_at: '2024-10-24T13:11:08.763Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Product Setup Assistance',
|
||||||
|
inbox: {
|
||||||
|
id: 2,
|
||||||
|
name: 'PaperLayer Website',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
phone_number: '',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Chatwoot',
|
||||||
|
},
|
||||||
|
message: 'Hi! Chatwoot here. Need help setting up? Let me know!',
|
||||||
|
campaign_status: 'active',
|
||||||
|
enabled: false,
|
||||||
|
campaign_type: 'ongoing',
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://{*.}?chatwoot.com/apps/account/*/settings/inboxes/new/',
|
||||||
|
time_on_page: 10,
|
||||||
|
},
|
||||||
|
trigger_only_during_business_hours: false,
|
||||||
|
created_at: '2024-10-24T13:11:44.285Z',
|
||||||
|
updated_at: '2024-10-24T13:11:44.285Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'General Assistance Campaign',
|
||||||
|
inbox: {
|
||||||
|
id: 2,
|
||||||
|
name: 'PaperLayer Website',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
phone_number: '',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Chris Barlow',
|
||||||
|
},
|
||||||
|
message:
|
||||||
|
'Hi there! 👋 I’m here for any questions you may have. Let’s chat!',
|
||||||
|
campaign_status: 'active',
|
||||||
|
enabled: true,
|
||||||
|
campaign_type: 'ongoing',
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://siv.com',
|
||||||
|
time_on_page: 200,
|
||||||
|
},
|
||||||
|
trigger_only_during_business_hours: false,
|
||||||
|
created_at: '2024-10-29T19:54:33.741Z',
|
||||||
|
updated_at: '2024-10-29T19:56:26.296Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Customer Feedback Request',
|
||||||
|
inbox: {
|
||||||
|
id: 6,
|
||||||
|
name: 'PaperLayer Mobile',
|
||||||
|
channel_type: 'Channel::Sms',
|
||||||
|
phone_number: '+29818373149903',
|
||||||
|
provider: 'default',
|
||||||
|
},
|
||||||
|
message:
|
||||||
|
'Hello! Enjoying our product? Share your feedback on G2 and earn a $25 Amazon coupon: https://chwt.app/g2-review',
|
||||||
|
campaign_status: 'active',
|
||||||
|
enabled: true,
|
||||||
|
campaign_type: 'one_off',
|
||||||
|
scheduled_at: 1729775588,
|
||||||
|
audience: [
|
||||||
|
{ id: 4, type: 'Label' },
|
||||||
|
{ id: 5, type: 'Label' },
|
||||||
|
{ id: 6, type: 'Label' },
|
||||||
|
],
|
||||||
|
trigger_rules: {},
|
||||||
|
trigger_only_during_business_hours: false,
|
||||||
|
created_at: '2024-10-24T13:13:08.496Z',
|
||||||
|
updated_at: '2024-10-24T13:15:38.698Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Welcome New Customer',
|
||||||
|
inbox: {
|
||||||
|
id: 6,
|
||||||
|
name: 'PaperLayer Mobile',
|
||||||
|
channel_type: 'Channel::Sms',
|
||||||
|
phone_number: '+29818373149903',
|
||||||
|
provider: 'default',
|
||||||
|
},
|
||||||
|
message: 'Welcome aboard! 🎉 Let us know if you have any questions.',
|
||||||
|
campaign_status: 'completed',
|
||||||
|
enabled: true,
|
||||||
|
campaign_type: 'one_off',
|
||||||
|
scheduled_at: 1729732500,
|
||||||
|
audience: [
|
||||||
|
{ id: 1, type: 'Label' },
|
||||||
|
{ id: 6, type: 'Label' },
|
||||||
|
{ id: 5, type: 'Label' },
|
||||||
|
{ id: 2, type: 'Label' },
|
||||||
|
{ id: 4, type: 'Label' },
|
||||||
|
],
|
||||||
|
trigger_rules: {},
|
||||||
|
trigger_only_during_business_hours: false,
|
||||||
|
created_at: '2024-10-24T13:14:00.168Z',
|
||||||
|
updated_at: '2024-10-24T13:15:38.707Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'New Business Welcome',
|
||||||
|
inbox: {
|
||||||
|
id: 6,
|
||||||
|
name: 'PaperLayer Mobile',
|
||||||
|
channel_type: 'Channel::Sms',
|
||||||
|
phone_number: '+29818373149903',
|
||||||
|
provider: 'default',
|
||||||
|
},
|
||||||
|
message: 'Hello! We’re excited to have your business with us!',
|
||||||
|
campaign_status: 'active',
|
||||||
|
enabled: true,
|
||||||
|
campaign_type: 'one_off',
|
||||||
|
scheduled_at: 1730368440,
|
||||||
|
audience: [
|
||||||
|
{ id: 1, type: 'Label' },
|
||||||
|
{ id: 3, type: 'Label' },
|
||||||
|
{ id: 6, type: 'Label' },
|
||||||
|
{ id: 4, type: 'Label' },
|
||||||
|
{ id: 2, type: 'Label' },
|
||||||
|
{ id: 5, type: 'Label' },
|
||||||
|
],
|
||||||
|
trigger_rules: {},
|
||||||
|
trigger_only_during_business_hours: false,
|
||||||
|
created_at: '2024-10-30T07:54:49.915Z',
|
||||||
|
updated_at: '2024-10-30T07:54:49.915Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'New Member Onboarding',
|
||||||
|
inbox: {
|
||||||
|
id: 6,
|
||||||
|
name: 'PaperLayer Mobile',
|
||||||
|
channel_type: 'Channel::Sms',
|
||||||
|
phone_number: '+29818373149903',
|
||||||
|
provider: 'default',
|
||||||
|
},
|
||||||
|
message: 'Welcome to the team! Reach out if you have questions.',
|
||||||
|
campaign_status: 'completed',
|
||||||
|
enabled: true,
|
||||||
|
campaign_type: 'one_off',
|
||||||
|
scheduled_at: 1730304840,
|
||||||
|
audience: [
|
||||||
|
{ id: 1, type: 'Label' },
|
||||||
|
{ id: 3, type: 'Label' },
|
||||||
|
{ id: 6, type: 'Label' },
|
||||||
|
],
|
||||||
|
trigger_rules: {},
|
||||||
|
trigger_only_during_business_hours: false,
|
||||||
|
created_at: '2024-10-29T16:14:10.374Z',
|
||||||
|
updated_at: '2024-10-30T16:15:03.157Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ONGOING_CAMPAIGN_EMPTY_STATE_CONTENT } from './CampaignEmptyStateContent';
|
||||||
|
|
||||||
|
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||||
|
import CampaignCard from 'dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
||||||
|
<template #empty-state-item>
|
||||||
|
<div class="flex flex-col gap-4 p-px">
|
||||||
|
<CampaignCard
|
||||||
|
v-for="campaign in ONGOING_CAMPAIGN_EMPTY_STATE_CONTENT"
|
||||||
|
:key="campaign.id"
|
||||||
|
:title="campaign.title"
|
||||||
|
:message="campaign.message"
|
||||||
|
:is-enabled="campaign.enabled"
|
||||||
|
:status="campaign.campaign_status"
|
||||||
|
:trigger-rules="campaign.trigger_rules"
|
||||||
|
:sender="campaign.sender"
|
||||||
|
:inbox="campaign.inbox"
|
||||||
|
:scheduled-at="campaign.scheduled_at"
|
||||||
|
is-live-chat-type
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</EmptyStateLayout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT } from './CampaignEmptyStateContent';
|
||||||
|
|
||||||
|
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||||
|
import CampaignCard from 'dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
||||||
|
<template #empty-state-item>
|
||||||
|
<div class="flex flex-col gap-4 p-px">
|
||||||
|
<CampaignCard
|
||||||
|
v-for="campaign in ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT"
|
||||||
|
:key="campaign.id"
|
||||||
|
:title="campaign.title"
|
||||||
|
:message="campaign.message"
|
||||||
|
:is-enabled="campaign.enabled"
|
||||||
|
:status="campaign.campaign_status"
|
||||||
|
:trigger-rules="campaign.trigger_rules"
|
||||||
|
:sender="campaign.sender"
|
||||||
|
:inbox="campaign.inbox"
|
||||||
|
:scheduled-at="campaign.scheduled_at"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</EmptyStateLayout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import CampaignCard from 'dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
campaigns: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isLiveChatType: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['edit', 'delete']);
|
||||||
|
|
||||||
|
const handleEdit = campaign => emit('edit', campaign);
|
||||||
|
const handleDelete = campaign => emit('delete', campaign);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<CampaignCard
|
||||||
|
v-for="campaign in campaigns"
|
||||||
|
:key="campaign.id"
|
||||||
|
:title="campaign.title"
|
||||||
|
:message="campaign.message"
|
||||||
|
:is-enabled="campaign.enabled"
|
||||||
|
:status="campaign.campaign_status"
|
||||||
|
:trigger-rules="campaign.trigger_rules"
|
||||||
|
:sender="campaign.sender"
|
||||||
|
:inbox="campaign.inbox"
|
||||||
|
:scheduled-at="campaign.scheduled_at"
|
||||||
|
:is-live-chat-type="isLiveChatType"
|
||||||
|
@edit="handleEdit(campaign)"
|
||||||
|
@delete="handleDelete(campaign)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useStore } from 'dashboard/composables/store';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectedCampaign: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const dialogRef = ref(null);
|
||||||
|
|
||||||
|
const deleteCampaign = async id => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('campaigns/delete', id);
|
||||||
|
useAlert(t('CAMPAIGN.CONFIRM_DELETE.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
useAlert(t('CAMPAIGN.CONFIRM_DELETE.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogConfirm = async () => {
|
||||||
|
await deleteCampaign(props.selectedCampaign.id);
|
||||||
|
dialogRef.value?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ dialogRef });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
type="alert"
|
||||||
|
:title="t('CAMPAIGN.CONFIRM_DELETE.TITLE')"
|
||||||
|
:description="t('CAMPAIGN.CONFIRM_DELETE.DESCRIPTION')"
|
||||||
|
:confirm-button-label="t('CAMPAIGN.CONFIRM_DELETE.CONFIRM')"
|
||||||
|
@confirm="handleDialogConfirm"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
import LiveChatCampaignForm from 'dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignForm.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectedCampaign: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const dialogRef = ref(null);
|
||||||
|
const liveChatCampaignFormRef = ref(null);
|
||||||
|
|
||||||
|
const uiFlags = useMapGetter('campaigns/getUIFlags');
|
||||||
|
const isUpdatingCampaign = computed(() => uiFlags.value.isUpdating);
|
||||||
|
|
||||||
|
const isInvalidForm = computed(
|
||||||
|
() => liveChatCampaignFormRef.value?.isSubmitDisabled
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCampaignId = computed(() => props.selectedCampaign.id);
|
||||||
|
|
||||||
|
const updateCampaign = async campaignDetails => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('campaigns/update', {
|
||||||
|
id: selectedCampaignId.value,
|
||||||
|
...campaignDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
useAlert(t('CAMPAIGN.LIVE_CHAT.EDIT.FORM.API.SUCCESS_MESSAGE'));
|
||||||
|
dialogRef.value.close();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.response?.message ||
|
||||||
|
t('CAMPAIGN.LIVE_CHAT.EDIT.FORM.API.ERROR_MESSAGE');
|
||||||
|
useAlert(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
updateCampaign(liveChatCampaignFormRef.value.prepareCampaignDetails());
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ dialogRef });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
type="edit"
|
||||||
|
:title="t('CAMPAIGN.LIVE_CHAT.EDIT.TITLE')"
|
||||||
|
:is-loading="isUpdatingCampaign"
|
||||||
|
:disable-confirm-button="isUpdatingCampaign || isInvalidForm"
|
||||||
|
overflow-y-auto
|
||||||
|
@confirm="handleSubmit"
|
||||||
|
>
|
||||||
|
<template #form>
|
||||||
|
<LiveChatCampaignForm
|
||||||
|
ref="liveChatCampaignFormRef"
|
||||||
|
mode="edit"
|
||||||
|
:selected-campaign="selectedCampaign"
|
||||||
|
:show-action-buttons="false"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStore } from 'dashboard/composables/store';
|
||||||
|
import { useAlert, useTrack } from 'dashboard/composables';
|
||||||
|
import { CAMPAIGN_TYPES } from 'shared/constants/campaign.js';
|
||||||
|
import { CAMPAIGNS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events.js';
|
||||||
|
|
||||||
|
import LiveChatCampaignForm from 'dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignForm.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const addCampaign = async campaignDetails => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('campaigns/create', campaignDetails);
|
||||||
|
|
||||||
|
// tracking this here instead of the store to track the type of campaign
|
||||||
|
useTrack(CAMPAIGNS_EVENTS.CREATE_CAMPAIGN, {
|
||||||
|
type: CAMPAIGN_TYPES.ONGOING,
|
||||||
|
});
|
||||||
|
|
||||||
|
useAlert(t('CAMPAIGN.SMS.CREATE.FORM.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.response?.message ||
|
||||||
|
t('CAMPAIGN.SMS.CREATE.FORM.API.ERROR_MESSAGE');
|
||||||
|
useAlert(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => emit('close');
|
||||||
|
|
||||||
|
const handleSubmit = campaignDetails => {
|
||||||
|
addCampaign(campaignDetails);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="w-[400px] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6 max-h-[85vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
|
||||||
|
{{ t(`CAMPAIGN.LIVE_CHAT.CREATE.TITLE`) }}
|
||||||
|
</h3>
|
||||||
|
<LiveChatCampaignForm
|
||||||
|
mode="create"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="handleClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, computed, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { URLPattern } from 'urlpattern-polyfill';
|
||||||
|
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator: value => ['edit', 'create'].includes(value),
|
||||||
|
},
|
||||||
|
selectedCampaign: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
showActionButtons: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const formState = {
|
||||||
|
uiFlags: useMapGetter('campaigns/getUIFlags'),
|
||||||
|
inboxes: useMapGetter('inboxes/getWebsiteInboxes'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const senderList = ref([]);
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
inboxId: null,
|
||||||
|
senderId: 0,
|
||||||
|
enabled: true,
|
||||||
|
triggerOnlyDuringBusinessHours: false,
|
||||||
|
endPoint: '',
|
||||||
|
timeOnPage: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({ ...initialState });
|
||||||
|
|
||||||
|
const urlValidators = {
|
||||||
|
shouldBeAValidURLPattern: value => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
new URLPattern(value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldStartWithHTTP: value =>
|
||||||
|
value ? value.startsWith('https://') || value.startsWith('http://') : false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
title: { required, minLength: minLength(1) },
|
||||||
|
message: { required, minLength: minLength(1) },
|
||||||
|
inboxId: { required },
|
||||||
|
senderId: { required },
|
||||||
|
endPoint: { required, ...urlValidators },
|
||||||
|
timeOnPage: { required },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
|
const isCreating = computed(() => formState.uiFlags.value.isCreating);
|
||||||
|
const isSubmitDisabled = computed(() => v$.value.$invalid);
|
||||||
|
|
||||||
|
const mapToOptions = (items, valueKey, labelKey) =>
|
||||||
|
items?.map(item => ({
|
||||||
|
value: item[valueKey],
|
||||||
|
label: item[labelKey],
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const inboxOptions = computed(() =>
|
||||||
|
mapToOptions(formState.inboxes.value, 'id', 'name')
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendersAndBotList = computed(() => [
|
||||||
|
{ value: 0, label: 'Bot' },
|
||||||
|
...mapToOptions(senderList.value, 'id', 'name'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getErrorMessage = (field, errorKey) => {
|
||||||
|
const baseKey = 'CAMPAIGN.LIVE_CHAT.CREATE.FORM';
|
||||||
|
return v$.value[field].$error ? t(`${baseKey}.${errorKey}.ERROR`) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formErrors = computed(() => ({
|
||||||
|
title: getErrorMessage('title', 'TITLE'),
|
||||||
|
message: getErrorMessage('message', 'MESSAGE'),
|
||||||
|
inbox: getErrorMessage('inboxId', 'INBOX'),
|
||||||
|
endPoint: getErrorMessage('endPoint', 'END_POINT'),
|
||||||
|
timeOnPage: getErrorMessage('timeOnPage', 'TIME_ON_PAGE'),
|
||||||
|
sender: getErrorMessage('senderId', 'SENT_BY'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resetState = () => Object.assign(state, initialState);
|
||||||
|
|
||||||
|
const handleCancel = () => emit('cancel');
|
||||||
|
|
||||||
|
const handleInboxChange = async inboxId => {
|
||||||
|
if (!inboxId) {
|
||||||
|
senderList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await store.dispatch('inboxMembers/get', { inboxId });
|
||||||
|
senderList.value = response?.data?.payload ?? [];
|
||||||
|
} catch (error) {
|
||||||
|
senderList.value = [];
|
||||||
|
useAlert(
|
||||||
|
error?.response?.message ??
|
||||||
|
t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.API.ERROR_MESSAGE')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareCampaignDetails = () => ({
|
||||||
|
title: state.title,
|
||||||
|
message: state.message,
|
||||||
|
inbox_id: state.inboxId,
|
||||||
|
sender_id: state.senderId || null,
|
||||||
|
enabled: state.enabled,
|
||||||
|
trigger_only_during_business_hours: state.triggerOnlyDuringBusinessHours,
|
||||||
|
trigger_rules: {
|
||||||
|
url: state.endPoint,
|
||||||
|
time_on_page: state.timeOnPage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const isFormValid = await v$.value.$validate();
|
||||||
|
if (!isFormValid) return;
|
||||||
|
|
||||||
|
emit('submit', prepareCampaignDetails());
|
||||||
|
if (props.mode === 'create') {
|
||||||
|
resetState();
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStateFromCampaign = campaign => {
|
||||||
|
if (!campaign) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
inbox: { id: inboxId },
|
||||||
|
sender,
|
||||||
|
enabled,
|
||||||
|
trigger_only_during_business_hours: triggerOnlyDuringBusinessHours,
|
||||||
|
trigger_rules: { url: endPoint, time_on_page: timeOnPage },
|
||||||
|
} = campaign;
|
||||||
|
|
||||||
|
Object.assign(state, {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
inboxId,
|
||||||
|
senderId: sender?.id ?? 0,
|
||||||
|
enabled,
|
||||||
|
triggerOnlyDuringBusinessHours,
|
||||||
|
endPoint,
|
||||||
|
timeOnPage,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.inboxId,
|
||||||
|
newInboxId => {
|
||||||
|
if (newInboxId) {
|
||||||
|
handleInboxChange(newInboxId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedCampaign,
|
||||||
|
newCampaign => {
|
||||||
|
if (props.mode === 'edit' && newCampaign) {
|
||||||
|
updateStateFromCampaign(newCampaign);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({ prepareCampaignDetails, isSubmitDisabled });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||||
|
<Input
|
||||||
|
v-model="state.title"
|
||||||
|
:label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.TITLE.LABEL')"
|
||||||
|
:placeholder="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.TITLE.PLACEHOLDER')"
|
||||||
|
:message="formErrors.title"
|
||||||
|
:message-type="formErrors.title ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
v-model="state.message"
|
||||||
|
:label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.MESSAGE.LABEL')"
|
||||||
|
:placeholder="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.MESSAGE.PLACEHOLDER')"
|
||||||
|
:message="formErrors.message"
|
||||||
|
:message-type="formErrors.message ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="inbox" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.INBOX.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<ComboBox
|
||||||
|
id="inbox"
|
||||||
|
v-model="state.inboxId"
|
||||||
|
:options="inboxOptions"
|
||||||
|
:has-error="!!formErrors.inbox"
|
||||||
|
:placeholder="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.INBOX.PLACEHOLDER')"
|
||||||
|
:message="formErrors.inbox"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="sentBy" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.SENT_BY.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<ComboBox
|
||||||
|
id="sentBy"
|
||||||
|
v-model="state.senderId"
|
||||||
|
:options="sendersAndBotList"
|
||||||
|
:has-error="!!formErrors.sender"
|
||||||
|
:disabled="!state.inboxId"
|
||||||
|
:placeholder="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.SENT_BY.PLACEHOLDER')"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||||
|
:message="formErrors.sender"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="state.endPoint"
|
||||||
|
type="url"
|
||||||
|
:label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.END_POINT.LABEL')"
|
||||||
|
:placeholder="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.END_POINT.PLACEHOLDER')"
|
||||||
|
:message="formErrors.endPoint"
|
||||||
|
:message-type="formErrors.endPoint ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="state.timeOnPage"
|
||||||
|
type="number"
|
||||||
|
:label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.TIME_ON_PAGE.LABEL')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.TIME_ON_PAGE.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="formErrors.timeOnPage"
|
||||||
|
:message-type="formErrors.timeOnPage ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<fieldset class="flex flex-col gap-2.5">
|
||||||
|
<legend class="mb-2.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.OTHER_PREFERENCES.TITLE') }}
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input v-model="state.enabled" type="checkbox" />
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.OTHER_PREFERENCES.ENABLED') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input v-model="state.triggerOnlyDuringBusinessHours" type="checkbox" />
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'CAMPAIGN.LIVE_CHAT.CREATE.FORM.OTHER_PREFERENCES.TRIGGER_ONLY_BUSINESS_HOURS'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showActionButtons"
|
||||||
|
class="flex items-center justify-between w-full gap-3"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
:label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.BUTTONS.CANCEL')"
|
||||||
|
class="w-full bg-n-alpha-2 n-blue-text hover:bg-n-alpha-3"
|
||||||
|
@click="handleCancel"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
:label="
|
||||||
|
t(`CAMPAIGN.LIVE_CHAT.CREATE.FORM.BUTTONS.${mode.toUpperCase()}`)
|
||||||
|
"
|
||||||
|
class="w-full"
|
||||||
|
:is-loading="isCreating"
|
||||||
|
:disabled="isCreating || isSubmitDisabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStore } from 'dashboard/composables/store';
|
||||||
|
import { useAlert, useTrack } from 'dashboard/composables';
|
||||||
|
import { CAMPAIGN_TYPES } from 'shared/constants/campaign.js';
|
||||||
|
import { CAMPAIGNS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events.js';
|
||||||
|
|
||||||
|
import SMSCampaignForm from 'dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignForm.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const addCampaign = async campaignDetails => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('campaigns/create', campaignDetails);
|
||||||
|
|
||||||
|
// tracking this here instead of the store to track the type of campaign
|
||||||
|
useTrack(CAMPAIGNS_EVENTS.CREATE_CAMPAIGN, {
|
||||||
|
type: CAMPAIGN_TYPES.ONE_OFF,
|
||||||
|
});
|
||||||
|
|
||||||
|
useAlert(t('CAMPAIGN.SMS.CREATE.FORM.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.response?.message ||
|
||||||
|
t('CAMPAIGN.SMS.CREATE.FORM.API.ERROR_MESSAGE');
|
||||||
|
useAlert(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = campaignDetails => {
|
||||||
|
addCampaign(campaignDetails);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => emit('close');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="w-[400px] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6"
|
||||||
|
>
|
||||||
|
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
|
||||||
|
{{ t(`CAMPAIGN.SMS.CREATE.TITLE`) }}
|
||||||
|
</h3>
|
||||||
|
<SMSCampaignForm @submit="handleSubmit" @cancel="handleClose" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiSelectComboBox.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const formState = {
|
||||||
|
uiFlags: useMapGetter('campaigns/getUIFlags'),
|
||||||
|
labels: useMapGetter('labels/getLabels'),
|
||||||
|
inboxes: useMapGetter('inboxes/getSMSInboxes'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
inboxId: null,
|
||||||
|
scheduledAt: null,
|
||||||
|
selectedAudience: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({ ...initialState });
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
title: { required, minLength: minLength(1) },
|
||||||
|
message: { required, minLength: minLength(1) },
|
||||||
|
inboxId: { required },
|
||||||
|
scheduledAt: { required },
|
||||||
|
selectedAudience: { required },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(rules, state);
|
||||||
|
|
||||||
|
const isCreating = computed(() => formState.uiFlags.value.isCreating);
|
||||||
|
|
||||||
|
const currentDateTime = computed(() => {
|
||||||
|
// Added to disable the scheduled at field from being set to the current time
|
||||||
|
const now = new Date();
|
||||||
|
const localTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
|
||||||
|
return localTime.toISOString().slice(0, 16);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapToOptions = (items, valueKey, labelKey) =>
|
||||||
|
items?.map(item => ({
|
||||||
|
value: item[valueKey],
|
||||||
|
label: item[labelKey],
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const audienceList = computed(() =>
|
||||||
|
mapToOptions(formState.labels.value, 'id', 'title')
|
||||||
|
);
|
||||||
|
|
||||||
|
const inboxOptions = computed(() =>
|
||||||
|
mapToOptions(formState.inboxes.value, 'id', 'name')
|
||||||
|
);
|
||||||
|
|
||||||
|
const getErrorMessage = (field, errorKey) => {
|
||||||
|
const baseKey = 'CAMPAIGN.SMS.CREATE.FORM';
|
||||||
|
return v$.value[field].$error ? t(`${baseKey}.${errorKey}.ERROR`) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formErrors = computed(() => ({
|
||||||
|
title: getErrorMessage('title', 'TITLE'),
|
||||||
|
message: getErrorMessage('message', 'MESSAGE'),
|
||||||
|
inbox: getErrorMessage('inboxId', 'INBOX'),
|
||||||
|
scheduledAt: getErrorMessage('scheduledAt', 'SCHEDULED_AT'),
|
||||||
|
audience: getErrorMessage('selectedAudience', 'AUDIENCE'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const isSubmitDisabled = computed(() => v$.value.$invalid);
|
||||||
|
|
||||||
|
const formatToUTCString = localDateTime =>
|
||||||
|
localDateTime ? new Date(localDateTime).toISOString() : null;
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
Object.assign(state, initialState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => emit('cancel');
|
||||||
|
|
||||||
|
const prepareCampaignDetails = () => ({
|
||||||
|
title: state.title,
|
||||||
|
message: state.message,
|
||||||
|
inbox_id: state.inboxId,
|
||||||
|
scheduled_at: formatToUTCString(state.scheduledAt),
|
||||||
|
audience: state.selectedAudience?.map(id => ({
|
||||||
|
id,
|
||||||
|
type: 'Label',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const isFormValid = await v$.value.$validate();
|
||||||
|
if (!isFormValid) return;
|
||||||
|
|
||||||
|
emit('submit', prepareCampaignDetails());
|
||||||
|
resetState();
|
||||||
|
handleCancel();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||||
|
<Input
|
||||||
|
v-model="state.title"
|
||||||
|
:label="t('CAMPAIGN.SMS.CREATE.FORM.TITLE.LABEL')"
|
||||||
|
:placeholder="t('CAMPAIGN.SMS.CREATE.FORM.TITLE.PLACEHOLDER')"
|
||||||
|
:message="formErrors.title"
|
||||||
|
:message-type="formErrors.title ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
v-model="state.message"
|
||||||
|
:label="t('CAMPAIGN.SMS.CREATE.FORM.MESSAGE.LABEL')"
|
||||||
|
:placeholder="t('CAMPAIGN.SMS.CREATE.FORM.MESSAGE.PLACEHOLDER')"
|
||||||
|
show-character-count
|
||||||
|
:message="formErrors.message"
|
||||||
|
:message-type="formErrors.message ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="inbox" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAMPAIGN.SMS.CREATE.FORM.INBOX.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<ComboBox
|
||||||
|
id="inbox"
|
||||||
|
v-model="state.inboxId"
|
||||||
|
:options="inboxOptions"
|
||||||
|
:has-error="!!formErrors.inbox"
|
||||||
|
:placeholder="t('CAMPAIGN.SMS.CREATE.FORM.INBOX.PLACEHOLDER')"
|
||||||
|
:message="formErrors.inbox"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="audience" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAMPAIGN.SMS.CREATE.FORM.AUDIENCE.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<TagMultiSelectComboBox
|
||||||
|
v-model="state.selectedAudience"
|
||||||
|
:options="audienceList"
|
||||||
|
:label="t('CAMPAIGN.SMS.CREATE.FORM.AUDIENCE.LABEL')"
|
||||||
|
:placeholder="t('CAMPAIGN.SMS.CREATE.FORM.AUDIENCE.PLACEHOLDER')"
|
||||||
|
:has-error="!!formErrors.audience"
|
||||||
|
:message="formErrors.audience"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="state.scheduledAt"
|
||||||
|
:label="t('CAMPAIGN.SMS.CREATE.FORM.SCHEDULED_AT.LABEL')"
|
||||||
|
type="datetime-local"
|
||||||
|
:min="currentDateTime"
|
||||||
|
:placeholder="t('CAMPAIGN.SMS.CREATE.FORM.SCHEDULED_AT.PLACEHOLDER')"
|
||||||
|
:message="formErrors.scheduledAt"
|
||||||
|
:message-type="formErrors.scheduledAt ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between w-full gap-3">
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
type="button"
|
||||||
|
:label="t('CAMPAIGN.SMS.CREATE.FORM.BUTTONS.CANCEL')"
|
||||||
|
class="w-full bg-n-alpha-2 n-blue-text hover:bg-n-alpha-3"
|
||||||
|
@click="handleCancel"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:label="t('CAMPAIGN.SMS.CREATE.FORM.BUTTONS.CREATE')"
|
||||||
|
class="w-full"
|
||||||
|
type="submit"
|
||||||
|
:is-loading="isCreating"
|
||||||
|
:disabled="isCreating || isSubmitDisabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
default: 'col',
|
||||||
|
},
|
||||||
|
});
|
||||||
const emit = defineEmits(['click']);
|
const emit = defineEmits(['click']);
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
emit('click');
|
emit('click');
|
||||||
@@ -7,7 +13,8 @@ const handleClick = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col w-full gap-3 px-6 py-5 shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
|
class="relative flex w-full gap-3 px-6 py-5 shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
|
||||||
|
:class="props.layout === 'col' ? 'flex-col' : 'flex-row'"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
|
|||||||
173
app/javascript/dashboard/components-next/Editor/Editor.vue
Normal file
173
app/javascript/dashboard/components-next/Editor/Editor.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import WootEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
focusOnMount: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 200,
|
||||||
|
},
|
||||||
|
showCharacterCount: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
messageType: {
|
||||||
|
type: String,
|
||||||
|
default: 'info',
|
||||||
|
validator: value => ['info', 'error', 'success'].includes(value),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const isFocused = ref(false);
|
||||||
|
|
||||||
|
const characterCount = computed(() => props.modelValue.length);
|
||||||
|
|
||||||
|
const messageClass = computed(() => {
|
||||||
|
switch (props.messageType) {
|
||||||
|
case 'error':
|
||||||
|
return 'text-n-ruby-9 dark:text-n-ruby-9';
|
||||||
|
case 'success':
|
||||||
|
return 'text-green-500 dark:text-green-400';
|
||||||
|
default:
|
||||||
|
return 'text-n-slate-11 dark:text-n-slate-11';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInput = value => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
isFocused.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
isFocused.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
newValue => {
|
||||||
|
if (props.maxLength && props.showCharacterCount) {
|
||||||
|
if (characterCount.value >= props.maxLength) {
|
||||||
|
emit('update:modelValue', newValue.slice(0, props.maxLength));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-w-0 gap-1">
|
||||||
|
<label v-if="label" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-full gap-2 px-3 py-3 transition-all duration-500 ease-in-out border rounded-lg editor-wrapper bg-n-alpha-black2"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'cursor-not-allowed opacity-50 pointer-events-none !bg-n-alpha-black2 disabled:border-n-weak dark:disabled:border-n-weak':
|
||||||
|
disabled,
|
||||||
|
'border-n-brand dark:border-n-brand': isFocused,
|
||||||
|
'hover:border-n-slate-6 dark:hover:border-n-slate-6 border-n-weak dark:border-n-weak':
|
||||||
|
!isFocused && messageType !== 'error',
|
||||||
|
'border-n-ruby-8 dark:border-n-ruby-8 hover:border-n-ruby-9 dark:hover:border-n-ruby-9':
|
||||||
|
messageType === 'error' && !isFocused,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<WootEditor
|
||||||
|
:model-value="modelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:focus-on-mount="focusOnMount"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="handleInput"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showCharacterCount"
|
||||||
|
class="flex items-center justify-end h-4 ltr:right-3 rtl:left-3"
|
||||||
|
>
|
||||||
|
<span class="text-xs tabular-nums text-n-slate-10">
|
||||||
|
{{ characterCount }} / {{ maxLength }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="message"
|
||||||
|
class="min-w-0 mt-1 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||||
|
:class="messageClass"
|
||||||
|
>
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.editor-wrapper {
|
||||||
|
::v-deep {
|
||||||
|
.ProseMirror-menubar-wrapper {
|
||||||
|
@apply gap-2 !important;
|
||||||
|
|
||||||
|
.ProseMirror-menubar {
|
||||||
|
@apply bg-transparent dark:bg-transparent w-fit left-1 pt-0 h-5 !important;
|
||||||
|
|
||||||
|
.ProseMirror-menuitem {
|
||||||
|
@apply h-5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-icon {
|
||||||
|
@apply p-1 w-3 h-3 text-n-slate-12 dark:text-n-slate-12 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ProseMirror.ProseMirror-woot-style {
|
||||||
|
p {
|
||||||
|
@apply first:mt-0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-node {
|
||||||
|
@apply m-0 !important;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply text-n-slate-11 dark:text-n-slate-11 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -34,7 +34,7 @@ defineProps({
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
class="max-w-lg text-base text-center text-slate-600 dark:text-slate-300 font-interDisplay tracking-[0.3px]"
|
class="max-w-xl text-base text-center text-slate-600 dark:text-slate-300 font-interDisplay tracking-[0.3px]"
|
||||||
>
|
>
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const onClick = () => {
|
|||||||
<template>
|
<template>
|
||||||
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
||||||
<template #empty-state-item>
|
<template #empty-state-item>
|
||||||
<div class="grid grid-cols-1 gap-4 overflow-hidden">
|
<div class="grid grid-cols-1 gap-4 p-px overflow-hidden">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
v-for="(article, index) in articleContent.slice(0, 5)"
|
v-for="(article, index) in articleContent.slice(0, 5)"
|
||||||
:id="article.id"
|
:id="article.id"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
||||||
<template #empty-state-item>
|
<template #empty-state-item>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4 p-px">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<CategoryCard
|
<CategoryCard
|
||||||
v-for="category in categoryContent"
|
v-for="category in categoryContent"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const onPortalCreate = ({ slug: portalSlug, locale }) => {
|
|||||||
:subtitle="$t('HELP_CENTER.NEW_PAGE.DESCRIPTION')"
|
:subtitle="$t('HELP_CENTER.NEW_PAGE.DESCRIPTION')"
|
||||||
>
|
>
|
||||||
<template #empty-state-item>
|
<template #empty-state-item>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4 p-px">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
v-for="(article, index) in articleContent"
|
v-for="(article, index) in articleContent"
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const togglePortalSwitcher = () => {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="activePortalName"
|
v-if="activePortalName"
|
||||||
class="text-xl font-medium text-slate-900 dark:text-white"
|
class="text-xl font-medium text-n-slate-12"
|
||||||
>
|
>
|
||||||
{{ activePortalName }}
|
{{ activePortalName }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -157,24 +157,24 @@ defineExpose({ state, isSubmitDisabled });
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-start gap-8 px-4 py-2 border rounded-lg border-slate-50 dark:border-slate-700/50"
|
class="flex items-center justify-start gap-8 px-4 py-2 border rounded-lg border-n-strong"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-start w-full gap-2 py-2">
|
<div class="flex flex-col items-start w-full gap-2 py-2">
|
||||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
<span class="text-sm font-medium text-n-slate-11">
|
||||||
{{ t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.HEADER.PORTAL') }}
|
{{ t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.HEADER.PORTAL') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm text-slate-800 dark:text-slate-100">
|
<span class="text-sm text-n-slate-12">
|
||||||
{{ portalName }}
|
{{ portalName }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-start w-px h-10 bg-slate-50 dark:bg-slate-700/50" />
|
<div class="justify-start w-px h-10 bg-n-strong" />
|
||||||
<div class="flex flex-col w-full gap-2 py-2">
|
<div class="flex flex-col w-full gap-2 py-2">
|
||||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
<span class="text-sm font-medium text-n-slate-11">
|
||||||
{{ t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.HEADER.LOCALE') }}
|
{{ t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.HEADER.LOCALE') }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
:title="`${activeLocaleName} (${activeLocaleCode})`"
|
:title="`${activeLocaleName} (${activeLocaleCode})`"
|
||||||
class="text-sm line-clamp-1 text-slate-800 dark:text-slate-100"
|
class="text-sm line-clamp-1 text-n-slate-12"
|
||||||
>
|
>
|
||||||
{{ `${activeLocaleName} (${activeLocaleCode})` }}
|
{{ `${activeLocaleName} (${activeLocaleCode})` }}
|
||||||
</span>
|
</span>
|
||||||
@@ -192,7 +192,7 @@ defineExpose({ state, isSubmitDisabled });
|
|||||||
"
|
"
|
||||||
:message="nameError"
|
:message="nameError"
|
||||||
:message-type="nameError ? 'error' : 'info'"
|
:message-type="nameError ? 'error' : 'info'"
|
||||||
custom-input-class="!h-10 ltr:!pl-12 rtl:!pr-12 !bg-slate-25 dark:!bg-slate-900"
|
custom-input-class="!h-10 ltr:!pl-12 rtl:!pr-12"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<OnClickOutside @trigger="isEmojiPickerOpen = false">
|
<OnClickOutside @trigger="isEmojiPickerOpen = false">
|
||||||
@@ -223,7 +223,7 @@ defineExpose({ state, isSubmitDisabled });
|
|||||||
:disabled="isEditMode"
|
:disabled="isEditMode"
|
||||||
:message="slugError ? slugError : slugHelpText"
|
:message="slugError ? slugError : slugHelpText"
|
||||||
:message-type="slugError ? 'error' : 'info'"
|
:message-type="slugError ? 'error' : 'info'"
|
||||||
custom-input-class="!h-10 !bg-slate-25 dark:!bg-slate-900 "
|
custom-input-class="!h-10"
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
v-model="state.description"
|
v-model="state.description"
|
||||||
@@ -236,7 +236,6 @@ defineExpose({ state, isSubmitDisabled });
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
show-character-count
|
show-character-count
|
||||||
custom-text-area-wrapper-class="!bg-slate-25 dark:!bg-slate-900"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showActionButtons"
|
v-if="showActionButtons"
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ const STYLE_CONFIG = {
|
|||||||
solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
|
solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
|
||||||
faded:
|
faded:
|
||||||
'bg-n-brand/10 text-n-slate-12 hover:bg-n-brand/20 outline-transparent',
|
'bg-n-brand/10 text-n-slate-12 hover:bg-n-brand/20 outline-transparent',
|
||||||
outline: 'text-n-blue-text hover:bg-n-brand/10 outline-n-blue-border',
|
outline: 'text-n-blue-text outline-n-blue-border',
|
||||||
link: 'text-n-brand hover:underline outline-transparent',
|
link: 'text-n-brand hover:underline outline-transparent',
|
||||||
},
|
},
|
||||||
ruby: {
|
ruby: {
|
||||||
solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
|
solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
|
||||||
faded:
|
faded:
|
||||||
'bg-n-ruby-9/10 text-n-slate-12 hover:bg-n-ruby-9/20 outline-transparent',
|
'bg-n-ruby-9/10 text-n-ruby-11 hover:bg-n-ruby-9/20 outline-transparent',
|
||||||
outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-9',
|
outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-8',
|
||||||
link: 'text-n-ruby-9 hover:underline outline-transparent',
|
link: 'text-n-ruby-9 hover:underline outline-transparent',
|
||||||
},
|
},
|
||||||
amber: {
|
amber: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { OnClickOutside } from '@vueuse/components';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import ComboBoxDropdown from 'dashboard/components-next/combobox/ComboBoxDropdown.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
options: {
|
options: {
|
||||||
@@ -36,6 +37,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
hasError: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
@@ -45,7 +50,7 @@ const { t } = useI18n();
|
|||||||
const selectedValue = ref(props.modelValue);
|
const selectedValue = ref(props.modelValue);
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
const searchInput = ref(null);
|
const dropdownRef = ref(null);
|
||||||
const comboboxRef = ref(null);
|
const comboboxRef = ref(null);
|
||||||
|
|
||||||
const filteredOptions = computed(() => {
|
const filteredOptions = computed(() => {
|
||||||
@@ -70,11 +75,13 @@ const selectOption = option => {
|
|||||||
open.value = false;
|
open.value = false;
|
||||||
search.value = '';
|
search.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
|
if (props.disabled) return;
|
||||||
open.value = !open.value;
|
open.value = !open.value;
|
||||||
if (open.value) {
|
if (open.value) {
|
||||||
search.value = '';
|
search.value = '';
|
||||||
nextTick(() => searchInput.value.focus());
|
nextTick(() => dropdownRef.value?.focus());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,67 +101,40 @@ watch(
|
|||||||
'cursor-not-allowed': disabled,
|
'cursor-not-allowed': disabled,
|
||||||
'group/combobox': !disabled,
|
'group/combobox': !disabled,
|
||||||
}"
|
}"
|
||||||
|
@click.prevent
|
||||||
>
|
>
|
||||||
<OnClickOutside @trigger="open = false">
|
<OnClickOutside @trigger="open = false">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="slate"
|
:color="hasError && !open ? 'ruby' : open ? 'blue' : 'slate'"
|
||||||
:label="selectedLabel"
|
:label="selectedLabel"
|
||||||
trailing-icon
|
trailing-icon
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6"
|
class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6"
|
||||||
|
:class="{ focused: open }"
|
||||||
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
@click="toggleDropdown"
|
@click="toggleDropdown"
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
v-show="open"
|
<ComboBoxDropdown
|
||||||
class="absolute z-50 w-full mt-1 transition-opacity duration-200 border rounded-md shadow-lg bg-n-solid-1 border-n-strong"
|
ref="dropdownRef"
|
||||||
>
|
:open="open"
|
||||||
<div class="relative border-b border-n-strong">
|
:options="filteredOptions"
|
||||||
<span class="absolute i-lucide-search top-2.5 size-4 left-3" />
|
:search-value="search"
|
||||||
<input
|
:search-placeholder="searchPlaceholder"
|
||||||
ref="searchInput"
|
:empty-state="emptyState"
|
||||||
v-model="search"
|
:selected-values="selectedValue"
|
||||||
type="search"
|
@update:search-value="search = $event"
|
||||||
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
@select="selectOption"
|
||||||
class="w-full py-2 pl-10 pr-2 text-sm border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="py-1 mb-0 overflow-auto max-h-60"
|
|
||||||
role="listbox"
|
|
||||||
:aria-activedescendant="selectedValue"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="option in filteredOptions"
|
|
||||||
:key="option.value"
|
|
||||||
class="flex items-center justify-between !text-n-slate-12 w-full gap-2 px-3 py-2 text-sm transition-colors duration-150 cursor-pointer hover:bg-n-alpha-2"
|
|
||||||
:class="{
|
|
||||||
'bg-n-alpha-2': option.value === selectedValue,
|
|
||||||
}"
|
|
||||||
role="option"
|
|
||||||
:aria-selected="option.value === selectedValue"
|
|
||||||
@click="selectOption(option)"
|
|
||||||
>
|
|
||||||
<span :class="{ 'font-medium': option.value === selectedValue }">
|
|
||||||
{{ option.label }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="option.value === selectedValue"
|
|
||||||
class="flex-shrink-0 i-lucide-check size-4 text-n-slate-11"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="filteredOptions.length === 0"
|
|
||||||
class="px-3 py-2 text-sm text-slate-600 dark:text-slate-300"
|
|
||||||
>
|
|
||||||
{{ emptyState || t('COMBOBOX.EMPTY_STATE') }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<p
|
<p
|
||||||
v-if="message"
|
v-if="message"
|
||||||
class="mt-2 mb-0 text-xs truncate transition-all duration-500 ease-in-out text-n-slate-11 dark:text-n-slate-11"
|
class="mt-2 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||||
|
:class="{
|
||||||
|
'text-n-ruby-9': hasError,
|
||||||
|
'text-n-slate-11': !hasError,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchValue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
selectedValues: {
|
||||||
|
type: [String, Number, Array],
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:searchValue', 'select']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const searchInput = ref(null);
|
||||||
|
|
||||||
|
const isSelected = option => {
|
||||||
|
if (Array.isArray(props.selectedValues)) {
|
||||||
|
return props.selectedValues.includes(option.value);
|
||||||
|
}
|
||||||
|
return option.value === props.selectedValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus: () => searchInput.value?.focus(),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-show="open"
|
||||||
|
class="absolute z-50 w-full mt-1 transition-opacity duration-200 border rounded-md shadow-lg bg-n-solid-1 border-n-strong"
|
||||||
|
>
|
||||||
|
<div class="relative border-b border-n-strong">
|
||||||
|
<span class="absolute i-lucide-search top-2.5 size-4 left-3" />
|
||||||
|
<input
|
||||||
|
ref="searchInput"
|
||||||
|
:value="searchValue"
|
||||||
|
type="search"
|
||||||
|
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||||
|
class="w-full py-2 pl-10 pr-2 text-sm border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
|
||||||
|
@input="emit('update:searchValue', $event.target.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="py-1 mb-0 overflow-auto max-h-60"
|
||||||
|
role="listbox"
|
||||||
|
:aria-multiselectable="multiple"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
class="flex items-center justify-between w-full gap-2 px-3 py-2 text-sm transition-colors duration-150 cursor-pointer hover:bg-n-alpha-2"
|
||||||
|
:class="{
|
||||||
|
'bg-n-alpha-2': isSelected(option),
|
||||||
|
}"
|
||||||
|
role="option"
|
||||||
|
:aria-selected="isSelected(option)"
|
||||||
|
@click="emit('select', option)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
|
'font-medium': isSelected(option),
|
||||||
|
}"
|
||||||
|
class="text-n-slate-12"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isSelected(option)"
|
||||||
|
class="flex-shrink-0 i-lucide-check size-4 text-n-slate-11"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="options.length === 0"
|
||||||
|
class="px-3 py-2 text-sm text-slate-600 dark:text-slate-300"
|
||||||
|
>
|
||||||
|
{{ emptyState || t('COMBOBOX.EMPTY_STATE') }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
|
import { OnClickOutside } from '@vueuse/components';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import ComboBoxDropdown from 'dashboard/components-next/combobox/ComboBoxDropdown.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
validator: value =>
|
||||||
|
value.every(option => 'value' in option && 'label' in option),
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
hasError: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const selectedValues = ref(props.modelValue);
|
||||||
|
const open = ref(false);
|
||||||
|
const search = ref('');
|
||||||
|
const dropdownRef = ref(null);
|
||||||
|
const comboboxRef = ref(null);
|
||||||
|
|
||||||
|
const filteredOptions = computed(() => {
|
||||||
|
const searchTerm = search.value.toLowerCase();
|
||||||
|
return props.options.filter(option =>
|
||||||
|
option.label?.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectPlaceholder = computed(() => {
|
||||||
|
return props.placeholder || t('COMBOBOX.PLACEHOLDER');
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedTags = computed(() => {
|
||||||
|
return selectedValues.value.map(value => {
|
||||||
|
const option = props.options.find(opt => opt.value === value);
|
||||||
|
return option || { value, label: value };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleOption = option => {
|
||||||
|
const index = selectedValues.value.indexOf(option.value);
|
||||||
|
if (index === -1) {
|
||||||
|
selectedValues.value.push(option.value);
|
||||||
|
} else {
|
||||||
|
selectedValues.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
emit('update:modelValue', selectedValues.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTag = value => {
|
||||||
|
const index = selectedValues.value.indexOf(value);
|
||||||
|
if (index !== -1) {
|
||||||
|
selectedValues.value.splice(index, 1);
|
||||||
|
emit('update:modelValue', selectedValues.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (props.disabled) return;
|
||||||
|
open.value = !open.value;
|
||||||
|
if (open.value) {
|
||||||
|
search.value = '';
|
||||||
|
nextTick(() => dropdownRef.value?.focus());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
newValue => {
|
||||||
|
selectedValues.value = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggleDropdown,
|
||||||
|
open,
|
||||||
|
disabled: props.disabled,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="comboboxRef"
|
||||||
|
class="relative w-full min-w-0"
|
||||||
|
:class="{
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'group/combobox': !disabled,
|
||||||
|
}"
|
||||||
|
@click.prevent
|
||||||
|
>
|
||||||
|
<OnClickOutside @trigger="open = false">
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap w-full gap-2 px-3 py-2.5 border rounded-lg cursor-pointer bg-n-alpha-black2 min-h-[42px] transition-all duration-500 ease-in-out"
|
||||||
|
:class="{
|
||||||
|
'border-n-ruby-8': hasError,
|
||||||
|
'border-n-weak dark:border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6':
|
||||||
|
!hasError && !open,
|
||||||
|
'border-n-brand': open,
|
||||||
|
'cursor-not-allowed pointer-events-none opacity-50': disabled,
|
||||||
|
}"
|
||||||
|
@click="toggleDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="tag in selectedTags"
|
||||||
|
:key="tag.value"
|
||||||
|
class="flex items-center justify-center max-w-full gap-1 px-2 py-0.5 rounded-lg bg-n-alpha-black1"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<span class="flex-grow min-w-0 text-sm truncate text-n-slate-12">
|
||||||
|
{{ tag.label }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="flex-shrink-0 cursor-pointer i-lucide-x size-3 text-n-slate-11"
|
||||||
|
@click="removeTag(tag.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="selectedTags.length === 0"
|
||||||
|
class="flex items-center text-sm text-n-slate-11"
|
||||||
|
>
|
||||||
|
{{ selectPlaceholder }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ComboBoxDropdown
|
||||||
|
ref="dropdownRef"
|
||||||
|
:open="open"
|
||||||
|
:options="filteredOptions"
|
||||||
|
:search-value="search"
|
||||||
|
:search-placeholder="searchPlaceholder"
|
||||||
|
:empty-state="emptyState"
|
||||||
|
multiple
|
||||||
|
:selected-values="selectedValues"
|
||||||
|
@update:search-value="search = $event"
|
||||||
|
@select="toggleOption"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="message"
|
||||||
|
class="mt-2 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||||
|
:class="{
|
||||||
|
'text-n-ruby-9': hasError,
|
||||||
|
'text-n-slate-11': !hasError,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</OnClickOutside>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import TagMultiSelectComboBox from './TagMultiSelectComboBox.vue';
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ value: 1, label: 'Option 1' },
|
||||||
|
{ value: 2, label: 'Option 2' },
|
||||||
|
{ value: 3, label: 'Option 3' },
|
||||||
|
{ value: 4, label: 'Option 4' },
|
||||||
|
{ value: 5, label: 'Option 5' },
|
||||||
|
];
|
||||||
|
const selectedValues = ref([]);
|
||||||
|
|
||||||
|
const preselectedValues = ref([1, 2]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/TagMultiSelectComboBox"
|
||||||
|
:layout="{ type: 'grid', width: '300px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default">
|
||||||
|
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
|
||||||
|
<TagMultiSelectComboBox v-model="selectedValues" :options="options" />
|
||||||
|
<p class="mt-2">Selected values: {{ selectedValues }}</p>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="With Preselected Values">
|
||||||
|
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
|
||||||
|
<TagMultiSelectComboBox
|
||||||
|
v-model="preselectedValues"
|
||||||
|
:options="options"
|
||||||
|
placeholder="Select multiple options"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Disabled">
|
||||||
|
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
|
||||||
|
<TagMultiSelectComboBox :options="options" disabled />
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="With Error">
|
||||||
|
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
|
||||||
|
<TagMultiSelectComboBox
|
||||||
|
:options="options"
|
||||||
|
has-error
|
||||||
|
message="This field is required"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -44,6 +44,10 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
overflowYAuto: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['confirm', 'close']);
|
const emit = defineEmits(['confirm', 'close']);
|
||||||
@@ -73,7 +77,8 @@ defineExpose({ open, close });
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<dialog
|
<dialog
|
||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
class="w-full max-w-lg overflow-visible transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
class="w-full max-w-lg transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
||||||
|
:class="overflowYAuto ? 'overflow-y-auto' : 'overflow-visible'"
|
||||||
:dir="isRTL ? 'rtl' : 'ltr'"
|
:dir="isRTL ? 'rtl' : 'ltr'"
|
||||||
@close="close"
|
@close="close"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ const props = defineProps({
|
|||||||
default: 'info',
|
default: 'info',
|
||||||
validator: value => ['info', 'error', 'success'].includes(value),
|
validator: value => ['info', 'error', 'success'].includes(value),
|
||||||
},
|
},
|
||||||
|
min: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'blur', 'input']);
|
const emit = defineEmits(['update:modelValue', 'blur', 'input']);
|
||||||
@@ -73,7 +77,7 @@ const handleInput = event => {
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
|
class="mb-0.5 text-sm font-medium text-n-slate-12"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
@@ -86,7 +90,8 @@ const handleInput = event => {
|
|||||||
:type="type"
|
:type="type"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="flex w-full reset-base text-sm h-10 !px-2 !py-2.5 !mb-0 border rounded-lg focus:border-n-brand dark:focus:border-n-brand bg-white dark:bg-slate-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-200 dark:placeholder:text-slate-500 disabled:cursor-not-allowed disabled:opacity-50 text-slate-900 dark:text-white transition-all duration-500 ease-in-out"
|
:min="['date', 'datetime-local', 'time'].includes(type) ? min : undefined"
|
||||||
|
class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 border rounded-lg focus:border-n-brand dark:focus:border-n-brand bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-11 dark:placeholder:text-n-slate-11 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@blur="emit('blur')"
|
@blur="emit('blur')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -266,14 +266,14 @@ const menuItems = computed(() => {
|
|||||||
icon: 'i-lucide-megaphone',
|
icon: 'i-lucide-megaphone',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Ongoing',
|
name: 'Live chat',
|
||||||
label: t('SIDEBAR.ONGOING'),
|
label: t('SIDEBAR.LIVE_CHAT'),
|
||||||
to: accountScopedRoute('ongoing_campaigns'),
|
to: accountScopedRoute('campaigns_livechat_index'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'One-off',
|
name: 'SMS',
|
||||||
label: t('SIDEBAR.ONE_OFF'),
|
label: t('SIDEBAR.SMS'),
|
||||||
to: accountScopedRoute('one_off'),
|
to: accountScopedRoute('campaigns_sms_index'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -281,9 +281,6 @@ const menuItems = computed(() => {
|
|||||||
name: 'Portals',
|
name: 'Portals',
|
||||||
label: t('SIDEBAR.HELP_CENTER.TITLE'),
|
label: t('SIDEBAR.HELP_CENTER.TITLE'),
|
||||||
icon: 'i-lucide-library-big',
|
icon: 'i-lucide-library-big',
|
||||||
to: accountScopedRoute('portals_index', {
|
|
||||||
navigationPath: 'portals_articles_index',
|
|
||||||
}),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Articles',
|
name: 'Articles',
|
||||||
@@ -442,7 +439,7 @@ const menuItems = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
class="w-[200px] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pt-2 pb-1"
|
class="w-[200px] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pb-1"
|
||||||
>
|
>
|
||||||
<section class="grid gap-2 mt-2 mb-4">
|
<section class="grid gap-2 mt-2 mb-4">
|
||||||
<div class="flex items-center min-w-0 gap-2 px-2">
|
<div class="flex items-center min-w-0 gap-2 px-2">
|
||||||
@@ -490,7 +487,7 @@ const menuItems = computed(() => {
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<section
|
<section
|
||||||
class="p-1 border-t border-n-strong shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)] flex-shrink-0 flex justify-between gap-2 items-center"
|
class="p-1 border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)] flex-shrink-0 flex justify-between gap-2 items-center"
|
||||||
>
|
>
|
||||||
<SidebarProfileMenu
|
<SidebarProfileMenu
|
||||||
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
|
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
messageType: {
|
||||||
|
type: String,
|
||||||
|
default: 'info',
|
||||||
|
validator: value => ['info', 'error', 'success'].includes(value),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
@@ -67,6 +76,17 @@ const isFocused = ref(false);
|
|||||||
|
|
||||||
const characterCount = computed(() => props.modelValue.length);
|
const characterCount = computed(() => props.modelValue.length);
|
||||||
|
|
||||||
|
const messageClass = computed(() => {
|
||||||
|
switch (props.messageType) {
|
||||||
|
case 'error':
|
||||||
|
return 'text-n-ruby-9 dark:text-n-ruby-9';
|
||||||
|
case 'success':
|
||||||
|
return 'text-green-500 dark:text-green-400';
|
||||||
|
default:
|
||||||
|
return 'text-n-slate-11 dark:text-n-slate-11';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TODO - use "field-sizing: content" and "height: auto" in future for auto height, when available.
|
// TODO - use "field-sizing: content" and "height: auto" in future for auto height, when available.
|
||||||
const adjustHeight = () => {
|
const adjustHeight = () => {
|
||||||
if (!props.autoHeight || !textareaRef.value) return;
|
if (!props.autoHeight || !textareaRef.value) return;
|
||||||
@@ -85,11 +105,15 @@ const handleInput = event => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
isFocused.value = true;
|
if (!props.disabled) {
|
||||||
|
isFocused.value = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
isFocused.value = false;
|
if (!props.disabled) {
|
||||||
|
isFocused.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch for changes in modelValue to adjust height
|
// Watch for changes in modelValue to adjust height
|
||||||
@@ -118,19 +142,22 @@ onMounted(() => {
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
|
class="mb-0.5 text-sm font-medium text-n-slate-12"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 px-3 pt-3 pb-3 transition-all duration-500 ease-in-out bg-white border rounded-lg border-n-weak dark:border-n-weak dark:bg-slate-900"
|
class="flex flex-col gap-2 px-3 pt-3 pb-3 transition-all duration-500 ease-in-out border rounded-lg bg-n-alpha-black2"
|
||||||
:class="[
|
:class="[
|
||||||
customTextAreaWrapperClass,
|
customTextAreaWrapperClass,
|
||||||
{
|
{
|
||||||
'cursor-not-allowed opacity-50 !bg-slate-25 dark:!bg-slate-800 disabled:border-n-weak dark:disabled:border-n-weak':
|
'cursor-not-allowed opacity-50 !bg-n-alpha-black2 disabled:border-n-weak dark:disabled:border-n-weak':
|
||||||
disabled,
|
disabled,
|
||||||
'border-n-brand dark:border-n-brand': isFocused,
|
'border-n-brand dark:border-n-brand': isFocused,
|
||||||
'hover:border-n-slate-6 dark:hover:border-n-slate-6': !isFocused,
|
'hover:border-n-slate-6 dark:hover:border-n-slate-6 border-n-weak dark:border-n-weak':
|
||||||
|
!isFocused && messageType !== 'error',
|
||||||
|
'border-n-ruby-8 dark:border-n-ruby-8 hover:border-n-ruby-9 dark:hover:border-n-ruby-9':
|
||||||
|
messageType === 'error' && !isFocused,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -152,7 +179,7 @@ onMounted(() => {
|
|||||||
}"
|
}"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
rows="1"
|
rows="1"
|
||||||
class="flex w-full reset-base text-sm p-0 !rounded-none !bg-transparent dark:!bg-transparent !border-0 !mb-0 placeholder:text-slate-200 dark:placeholder:text-slate-500 text-slate-900 dark:text-white disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-slate-25 dark:disabled:bg-slate-900"
|
class="flex w-full reset-base text-sm p-0 !rounded-none !bg-transparent dark:!bg-transparent !border-0 !mb-0 placeholder:text-n-slate-11 dark:placeholder:text-n-slate-11 text-n-slate-12 dark:text-n-slate-12 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-slate-25 dark:disabled:bg-slate-900"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
@@ -161,10 +188,17 @@ onMounted(() => {
|
|||||||
v-if="showCharacterCount"
|
v-if="showCharacterCount"
|
||||||
class="flex items-center justify-end h-4 mt-1 bottom-3 ltr:right-3 rtl:left-3"
|
class="flex items-center justify-end h-4 mt-1 bottom-3 ltr:right-3 rtl:left-3"
|
||||||
>
|
>
|
||||||
<span class="text-xs tabular-nums text-slate-300 dark:text-slate-600">
|
<span class="text-xs tabular-nums text-n-slate-10">
|
||||||
{{ characterCount }} / {{ maxLength }}
|
{{ characterCount }} / {{ maxLength }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="message"
|
||||||
|
class="min-w-0 mt-1 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||||
|
:class="messageClass"
|
||||||
|
>
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const onImgLoad = () => {
|
|||||||
class="flex items-center justify-center rounded-full bg-n-slate-3 dark:bg-n-slate-4"
|
class="flex items-center justify-center rounded-full bg-n-slate-3 dark:bg-n-slate-4"
|
||||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||||
>
|
>
|
||||||
<div v-if="author">
|
<div v-if="author" class="flex items-center justify-center">
|
||||||
<img
|
<img
|
||||||
v-if="shouldShowImage"
|
v-if="shouldShowImage"
|
||||||
:src="src"
|
:src="src"
|
||||||
|
|||||||
@@ -2,23 +2,23 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
|||||||
|
|
||||||
const campaigns = accountId => ({
|
const campaigns = accountId => ({
|
||||||
parentNav: 'campaigns',
|
parentNav: 'campaigns',
|
||||||
routes: ['ongoing_campaigns', 'one_off'],
|
routes: ['campaigns_sms_index', 'campaigns_livechat_index'],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
icon: 'arrow-swap',
|
icon: 'arrow-swap',
|
||||||
label: 'ONGOING',
|
label: 'LIVE_CHAT',
|
||||||
key: 'ongoingCampaigns',
|
key: 'ongoingCampaigns',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
toState: frontendURL(`accounts/${accountId}/campaigns/live_chat`),
|
||||||
toStateName: 'ongoing_campaigns',
|
toStateName: 'campaigns_livechat_index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'oneOffCampaigns',
|
key: 'oneOffCampaigns',
|
||||||
icon: 'sound-source',
|
icon: 'sound-source',
|
||||||
label: 'ONE_OFF',
|
label: 'SMS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
|
toState: frontendURL(`accounts/${accountId}/campaigns/sms`),
|
||||||
toStateName: 'one_off',
|
toStateName: 'campaigns_sms_index',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const primaryMenuItems = accountId => [
|
|||||||
label: 'CAMPAIGNS',
|
label: 'CAMPAIGNS',
|
||||||
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
||||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||||
toStateName: 'ongoing_campaigns',
|
toStateName: 'campaigns_ongoing_index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'library',
|
icon: 'library',
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const props = defineProps({
|
|||||||
modelValue: { type: String, default: '' },
|
modelValue: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
placeholder: { type: String, default: '' },
|
placeholder: { type: String, default: '' },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
isPrivate: { type: Boolean, default: false },
|
isPrivate: { type: Boolean, default: false },
|
||||||
enableSuggestions: { type: Boolean, default: true },
|
enableSuggestions: { type: Boolean, default: true },
|
||||||
overrideLineBreaks: { type: Boolean, default: false },
|
overrideLineBreaks: { type: Boolean, default: false },
|
||||||
@@ -299,6 +300,8 @@ function handleEmptyBodyWithSignature() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function focusEditor(content) {
|
function focusEditor(content) {
|
||||||
|
if (props.disabled) return;
|
||||||
|
|
||||||
const unrefContent = unref(content);
|
const unrefContent = unref(content);
|
||||||
if (isBodyEmpty(unrefContent) && sendWithSignature.value) {
|
if (isBodyEmpty(unrefContent) && sendWithSignature.value) {
|
||||||
// reload state can be called when switching between conversations, or when drafts is loaded
|
// reload state can be called when switching between conversations, or when drafts is loaded
|
||||||
@@ -561,6 +564,7 @@ function onKeydown(event) {
|
|||||||
function createEditorView() {
|
function createEditorView() {
|
||||||
editorView = new EditorView(editor.value, {
|
editorView = new EditorView(editor.value, {
|
||||||
state: state,
|
state: state,
|
||||||
|
editable: () => !props.disabled,
|
||||||
dispatchTransaction: tx => {
|
dispatchTransaction: tx => {
|
||||||
state = state.apply(tx);
|
state = state.apply(tx);
|
||||||
editorView.updateState(state);
|
editorView.updateState(state);
|
||||||
@@ -570,17 +574,21 @@ function createEditorView() {
|
|||||||
},
|
},
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keyup: () => {
|
keyup: () => {
|
||||||
typingIndicator.start();
|
if (!props.disabled) {
|
||||||
updateImgToolbarOnDelete();
|
typingIndicator.start();
|
||||||
|
updateImgToolbarOnDelete();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
keydown: (view, event) => onKeydown(event),
|
keydown: (view, event) => !props.disabled && onKeydown(event),
|
||||||
focus: () => emit('focus'),
|
focus: () => !props.disabled && emit('focus'),
|
||||||
click: isEditorMouseFocusedOnAnImage,
|
click: () => !props.disabled && isEditorMouseFocusedOnAnImage(),
|
||||||
blur: () => {
|
blur: () => {
|
||||||
|
if (props.disabled) return;
|
||||||
typingIndicator.stop();
|
typingIndicator.stop();
|
||||||
emit('blur');
|
emit('blur');
|
||||||
},
|
},
|
||||||
paste: (_view, event) => {
|
paste: (_view, event) => {
|
||||||
|
if (props.disabled) return;
|
||||||
const data = event.clipboardData.files;
|
const data = event.clipboardData.files;
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -1,4 +1,28 @@
|
|||||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
export const INBOX_TYPES = {
|
||||||
|
WEB: 'Channel::WebWidget',
|
||||||
|
FB: 'Channel::FacebookPage',
|
||||||
|
TWITTER: 'Channel::TwitterProfile',
|
||||||
|
TWILIO: 'Channel::TwilioSms',
|
||||||
|
WHATSAPP: 'Channel::Whatsapp',
|
||||||
|
API: 'Channel::Api',
|
||||||
|
EMAIL: 'Channel::Email',
|
||||||
|
TELEGRAM: 'Channel::Telegram',
|
||||||
|
LINE: 'Channel::Line',
|
||||||
|
SMS: 'Channel::Sms',
|
||||||
|
};
|
||||||
|
|
||||||
|
const INBOX_ICON_MAP = {
|
||||||
|
[INBOX_TYPES.WEB]: 'i-ri-global-fill',
|
||||||
|
[INBOX_TYPES.FB]: 'i-ri-messenger-fill',
|
||||||
|
[INBOX_TYPES.TWITTER]: 'i-ri-twitter-x-fill',
|
||||||
|
[INBOX_TYPES.WHATSAPP]: 'i-ri-whatsapp-fill',
|
||||||
|
[INBOX_TYPES.API]: 'i-ri-cloudy-fill',
|
||||||
|
[INBOX_TYPES.EMAIL]: 'i-ri-mail-fill',
|
||||||
|
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill',
|
||||||
|
[INBOX_TYPES.LINE]: 'i-ri-line-fill',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_ICON = 'i-ri-chat-1-fill';
|
||||||
|
|
||||||
export const getInboxSource = (type, phoneNumber, inbox) => {
|
export const getInboxSource = (type, phoneNumber, inbox) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -86,6 +110,17 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getInboxIconByType = (type, phoneNumber) => {
|
||||||
|
// Special case for Twilio (whatsapp and sms)
|
||||||
|
if (type === INBOX_TYPES.TWILIO) {
|
||||||
|
return phoneNumber?.startsWith('whatsapp')
|
||||||
|
? 'i-ri-whatsapp-fill'
|
||||||
|
: 'i-ri-chat-1-fill';
|
||||||
|
}
|
||||||
|
|
||||||
|
return INBOX_ICON_MAP[type] ?? DEFAULT_ICON;
|
||||||
|
};
|
||||||
|
|
||||||
export const getInboxWarningIconClass = (type, reauthorizationRequired) => {
|
export const getInboxWarningIconClass = (type, reauthorizationRequired) => {
|
||||||
const allowedInboxTypes = [INBOX_TYPES.FB, INBOX_TYPES.EMAIL];
|
const allowedInboxTypes = [INBOX_TYPES.FB, INBOX_TYPES.EMAIL];
|
||||||
if (allowedInboxTypes.includes(type) && reauthorizationRequired) {
|
if (allowedInboxTypes.includes(type) && reauthorizationRequired) {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { getInboxClassByType, getInboxWarningIconClass } from '../inbox';
|
import {
|
||||||
|
INBOX_TYPES,
|
||||||
|
getInboxClassByType,
|
||||||
|
getInboxIconByType,
|
||||||
|
getInboxWarningIconClass,
|
||||||
|
} from '../inbox';
|
||||||
|
|
||||||
describe('#Inbox Helpers', () => {
|
describe('#Inbox Helpers', () => {
|
||||||
describe('getInboxClassByType', () => {
|
describe('getInboxClassByType', () => {
|
||||||
@@ -35,6 +40,74 @@ describe('#Inbox Helpers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getInboxIconByType', () => {
|
||||||
|
it('returns correct icon for web widget', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.WEB)).toBe('i-ri-global-fill');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct icon for Facebook', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.FB)).toBe('i-ri-messenger-fill');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct icon for Twitter', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.TWITTER)).toBe(
|
||||||
|
'i-ri-twitter-x-fill'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Twilio cases', () => {
|
||||||
|
it('returns WhatsApp icon for Twilio WhatsApp number', () => {
|
||||||
|
expect(
|
||||||
|
getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp:+1234567890')
|
||||||
|
).toBe('i-ri-whatsapp-fill');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns SMS icon for regular Twilio number', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.TWILIO, '+1234567890')).toBe(
|
||||||
|
'i-ri-chat-1-fill'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns SMS icon when phone number is undefined', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.TWILIO, undefined)).toBe(
|
||||||
|
'i-ri-chat-1-fill'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct icon for WhatsApp', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.WHATSAPP)).toBe(
|
||||||
|
'i-ri-whatsapp-fill'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct icon for API', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.API)).toBe('i-ri-cloudy-fill');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct icon for Email', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.EMAIL)).toBe('i-ri-mail-fill');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct icon for Telegram', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.TELEGRAM)).toBe(
|
||||||
|
'i-ri-telegram-fill'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct icon for Line', () => {
|
||||||
|
expect(getInboxIconByType(INBOX_TYPES.LINE)).toBe('i-ri-line-fill');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default icon for unknown type', () => {
|
||||||
|
expect(getInboxIconByType('UNKNOWN_TYPE')).toBe('i-ri-chat-1-fill');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default icon for undefined type', () => {
|
||||||
|
expect(getInboxIconByType(undefined)).toBe('i-ri-chat-1-fill');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getInboxWarningIconClass', () => {
|
describe('getInboxWarningIconClass', () => {
|
||||||
it('should return correct class for warning', () => {
|
it('should return correct class for warning', () => {
|
||||||
expect(getInboxWarningIconClass('Channel::FacebookPage', true)).toEqual(
|
expect(getInboxWarningIconClass('Channel::FacebookPage', true)).toEqual(
|
||||||
|
|||||||
@@ -1,126 +1,150 @@
|
|||||||
{
|
{
|
||||||
"CAMPAIGN": {
|
"CAMPAIGN": {
|
||||||
"HEADER": "Campaigns",
|
"LIVE_CHAT": {
|
||||||
"SIDEBAR_TXT": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations. Click on <b>Add Campaign</b> to create a new campaign. You can also edit or delete an existing campaign by clicking on the Edit or Delete button.",
|
"HEADER_TITLE": "Live chat campaigns",
|
||||||
"HEADER_BTN_TXT": {
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
"ONE_OFF": "Create a one off campaign",
|
"CARD": {
|
||||||
"ONGOING": "Create a ongoing campaign"
|
"STATUS": {
|
||||||
},
|
"ENABLED": "Enabled",
|
||||||
"ADD": {
|
"DISABLED": "Disabled"
|
||||||
"TITLE": "Create a campaign",
|
|
||||||
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
|
|
||||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
|
||||||
"CREATE_BUTTON_TEXT": "Create",
|
|
||||||
"FORM": {
|
|
||||||
"TITLE": {
|
|
||||||
"LABEL": "Title",
|
|
||||||
"PLACEHOLDER": "Please enter the title of campaign",
|
|
||||||
"ERROR": "Title is required"
|
|
||||||
},
|
},
|
||||||
"SCHEDULED_AT": {
|
"CAMPAIGN_DETAILS": {
|
||||||
"LABEL": "Scheduled time",
|
"SENT_BY": "Sent by",
|
||||||
"PLACEHOLDER": "Please select the time",
|
"BOT": "Bot",
|
||||||
"CONFIRM": "Confirm",
|
"FROM": "from",
|
||||||
"ERROR": "Scheduled time is required"
|
"URL": "URL:"
|
||||||
},
|
}
|
||||||
"AUDIENCE": {
|
|
||||||
"LABEL": "Audience",
|
|
||||||
"PLACEHOLDER": "Select the customer labels",
|
|
||||||
"ERROR": "Audience is required"
|
|
||||||
},
|
|
||||||
"INBOX": {
|
|
||||||
"LABEL": "Select Inbox",
|
|
||||||
"PLACEHOLDER": "Select Inbox",
|
|
||||||
"ERROR": "Inbox is required"
|
|
||||||
},
|
|
||||||
"MESSAGE": {
|
|
||||||
"LABEL": "Message",
|
|
||||||
"PLACEHOLDER": "Please enter the message of campaign",
|
|
||||||
"ERROR": "Message is required"
|
|
||||||
},
|
|
||||||
"SENT_BY": {
|
|
||||||
"LABEL": "Sent by",
|
|
||||||
"PLACEHOLDER": "Please select the the content of campaign",
|
|
||||||
"ERROR": "Sender is required"
|
|
||||||
},
|
|
||||||
"END_POINT": {
|
|
||||||
"LABEL": "URL",
|
|
||||||
"PLACEHOLDER": "Please enter the URL",
|
|
||||||
"ERROR": "Please enter a valid URL"
|
|
||||||
},
|
|
||||||
"TIME_ON_PAGE": {
|
|
||||||
"LABEL": "Time on page(Seconds)",
|
|
||||||
"PLACEHOLDER": "Please enter the time",
|
|
||||||
"ERROR": "Time on page is required"
|
|
||||||
},
|
|
||||||
"ENABLED": "Enable campaign",
|
|
||||||
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
|
|
||||||
"SUBMIT": "Add Campaign"
|
|
||||||
},
|
},
|
||||||
"API": {
|
"EMPTY_STATE": {
|
||||||
"SUCCESS_MESSAGE": "Campaign created successfully",
|
"TITLE": "No live chat campaigns are available",
|
||||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
"SUBTITLE": "Connect with your customers using proactive messages. Click 'Create campaign' to get started."
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create a live chat campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||||
|
"CREATE_BUTTON_TEXT": "Create",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Title",
|
||||||
|
"PLACEHOLDER": "Please enter the title of campaign",
|
||||||
|
"ERROR": "Title is required"
|
||||||
|
},
|
||||||
|
"MESSAGE": {
|
||||||
|
"LABEL": "Message",
|
||||||
|
"PLACEHOLDER": "Please enter the message of campaign",
|
||||||
|
"ERROR": "Message is required"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Select Inbox",
|
||||||
|
"PLACEHOLDER": "Select Inbox",
|
||||||
|
"ERROR": "Inbox is required"
|
||||||
|
},
|
||||||
|
"SENT_BY": {
|
||||||
|
"LABEL": "Sent by",
|
||||||
|
"PLACEHOLDER": "Please select sender",
|
||||||
|
"ERROR": "Sender is required"
|
||||||
|
},
|
||||||
|
"END_POINT": {
|
||||||
|
"LABEL": "URL",
|
||||||
|
"PLACEHOLDER": "Please enter the URL",
|
||||||
|
"ERROR": "Please enter a valid URL"
|
||||||
|
},
|
||||||
|
"TIME_ON_PAGE": {
|
||||||
|
"LABEL": "Time on page(Seconds)",
|
||||||
|
"PLACEHOLDER": "Please enter the time",
|
||||||
|
"ERROR": "Time on page is required"
|
||||||
|
},
|
||||||
|
"OTHER_PREFERENCES": {
|
||||||
|
"TITLE": "Other preferences",
|
||||||
|
"ENABLED": "Enable campaign",
|
||||||
|
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Live chat campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"TITLE": "Edit live chat campaign",
|
||||||
|
"FORM": {
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Live chat campaign updated successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"SMS": {
|
||||||
"BUTTON_TEXT": "Delete",
|
"HEADER_TITLE": "SMS campaigns",
|
||||||
"CONFIRM": {
|
"NEW_CAMPAIGN": "Create campaign",
|
||||||
"TITLE": "Confirm Deletion",
|
"EMPTY_STATE": {
|
||||||
"MESSAGE": "Are you sure to delete?",
|
"TITLE": "No SMS campaigns are available",
|
||||||
"YES": "Yes, Delete ",
|
"SUBTITLE": "Launch an SMS campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||||
"NO": "No, Keep "
|
|
||||||
},
|
},
|
||||||
|
"CARD": {
|
||||||
|
"STATUS": {
|
||||||
|
"COMPLETED": "Completed",
|
||||||
|
"SCHEDULED": "Scheduled"
|
||||||
|
},
|
||||||
|
"CAMPAIGN_DETAILS": {
|
||||||
|
"SENT_FROM": "Sent from",
|
||||||
|
"ON": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE": {
|
||||||
|
"TITLE": "Create SMS campaign",
|
||||||
|
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||||
|
"CREATE_BUTTON_TEXT": "Create",
|
||||||
|
"FORM": {
|
||||||
|
"TITLE": {
|
||||||
|
"LABEL": "Title",
|
||||||
|
"PLACEHOLDER": "Please enter the title of campaign",
|
||||||
|
"ERROR": "Title is required"
|
||||||
|
},
|
||||||
|
"MESSAGE": {
|
||||||
|
"LABEL": "Message",
|
||||||
|
"PLACEHOLDER": "Please enter the message of campaign",
|
||||||
|
"ERROR": "Message is required"
|
||||||
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Select Inbox",
|
||||||
|
"PLACEHOLDER": "Select Inbox",
|
||||||
|
"ERROR": "Inbox is required"
|
||||||
|
},
|
||||||
|
"AUDIENCE": {
|
||||||
|
"LABEL": "Audience",
|
||||||
|
"PLACEHOLDER": "Select the customer labels",
|
||||||
|
"ERROR": "Audience is required"
|
||||||
|
},
|
||||||
|
"SCHEDULED_AT": {
|
||||||
|
"LABEL": "Scheduled time",
|
||||||
|
"PLACEHOLDER": "Please select the time",
|
||||||
|
"ERROR": "Scheduled time is required"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Create",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "SMS campaign created successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CONFIRM_DELETE": {
|
||||||
|
"TITLE": "Are you sure to delete?",
|
||||||
|
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||||
|
"CONFIRM": "Delete",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Campaign deleted successfully",
|
"SUCCESS_MESSAGE": "Campaign deleted successfully",
|
||||||
"ERROR_MESSAGE": "Could not delete the campaign. Please try again later."
|
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"EDIT": {
|
|
||||||
"TITLE": "Edit campaign",
|
|
||||||
"UPDATE_BUTTON_TEXT": "Update",
|
|
||||||
"API": {
|
|
||||||
"SUCCESS_MESSAGE": "Campaign updated successfully",
|
|
||||||
"ERROR_MESSAGE": "There was an error, please try again"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"LIST": {
|
|
||||||
"LOADING_MESSAGE": "Loading campaigns...",
|
|
||||||
"404": "There are no campaigns created for this inbox.",
|
|
||||||
"TABLE_HEADER": {
|
|
||||||
"TITLE": "Title",
|
|
||||||
"MESSAGE": "Message",
|
|
||||||
"INBOX": "Inbox",
|
|
||||||
"STATUS": "Status",
|
|
||||||
"SENDER": "Sender",
|
|
||||||
"URL": "URL",
|
|
||||||
"SCHEDULED_AT": "Scheduled time",
|
|
||||||
"TIME_ON_PAGE": "Time(Seconds)",
|
|
||||||
"CREATED_AT": "Created at"
|
|
||||||
},
|
|
||||||
"BUTTONS": {
|
|
||||||
"ADD": "Add",
|
|
||||||
"EDIT": "Edit",
|
|
||||||
"DELETE": "Delete"
|
|
||||||
},
|
|
||||||
"STATUS": {
|
|
||||||
"ENABLED": "Enabled",
|
|
||||||
"DISABLED": "Disabled",
|
|
||||||
"COMPLETED": "Completed",
|
|
||||||
"ACTIVE": "Active"
|
|
||||||
},
|
|
||||||
"SENDER": {
|
|
||||||
"BOT": "Bot"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ONE_OFF": {
|
|
||||||
"HEADER": "One off campaigns",
|
|
||||||
"404": "There are no one off campaigns created",
|
|
||||||
"INBOXES_NOT_FOUND": "Please create an sms inbox and start adding campaigns"
|
|
||||||
},
|
|
||||||
"ONGOING": {
|
|
||||||
"HEADER": "Ongoing campaigns",
|
|
||||||
"404": "There are no ongoing campaigns created",
|
|
||||||
"INBOXES_NOT_FOUND": "Please create an website inbox and start adding campaigns"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,6 +267,8 @@
|
|||||||
"NEW_INBOX": "New inbox",
|
"NEW_INBOX": "New inbox",
|
||||||
"REPORTS_CONVERSATION": "Conversations",
|
"REPORTS_CONVERSATION": "Conversations",
|
||||||
"CSAT": "CSAT",
|
"CSAT": "CSAT",
|
||||||
|
"LIVE_CHAT": "Live Chat",
|
||||||
|
"SMS": "SMS",
|
||||||
"CAMPAIGNS": "Campaigns",
|
"CAMPAIGNS": "Campaigns",
|
||||||
"ONGOING": "Ongoing",
|
"ONGOING": "Ongoing",
|
||||||
"ONE_OFF": "One off",
|
"ONE_OFF": "One off",
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { frontendURL } from 'dashboard/helper/URLHelper.js';
|
||||||
|
|
||||||
|
import CampaignsPageRouteView from './pages/CampaignsPageRouteView.vue';
|
||||||
|
import LiveChatCampaignsPage from './pages/LiveChatCampaignsPage.vue';
|
||||||
|
import SMSCampaignsPage from './pages/SMSCampaignsPage.vue';
|
||||||
|
|
||||||
|
const campaignsRoutes = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/campaigns'),
|
||||||
|
component: CampaignsPageRouteView,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirect: to => {
|
||||||
|
return { name: 'campaigns_ongoing_index', params: to.params };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ongoing',
|
||||||
|
name: 'campaigns_ongoing_index',
|
||||||
|
meta: {
|
||||||
|
permissions: ['administrator'],
|
||||||
|
},
|
||||||
|
redirect: to => {
|
||||||
|
return { name: 'campaigns_livechat_index', params: to.params };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'one_off',
|
||||||
|
name: 'campaigns_one_off_index',
|
||||||
|
meta: {
|
||||||
|
permissions: ['administrator'],
|
||||||
|
},
|
||||||
|
redirect: to => {
|
||||||
|
return { name: 'campaigns_sms_index', params: to.params };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'live_chat',
|
||||||
|
name: 'campaigns_livechat_index',
|
||||||
|
meta: {
|
||||||
|
permissions: ['administrator'],
|
||||||
|
},
|
||||||
|
component: LiveChatCampaignsPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sms',
|
||||||
|
name: 'campaigns_sms_index',
|
||||||
|
meta: {
|
||||||
|
permissions: ['administrator'],
|
||||||
|
},
|
||||||
|
component: SMSCampaignsPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default campaignsRoutes;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { useStore } from 'dashboard/composables/store';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
keepAlive: { type: Boolean, default: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.dispatch('campaigns/get');
|
||||||
|
store.dispatch('labels/get');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background"
|
||||||
|
>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive v-if="keepAlive">
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
<component :is="Component" v-else />
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { CAMPAIGN_TYPES } from 'shared/constants/campaign.js';
|
||||||
|
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
import CampaignLayout from 'dashboard/components-next/Campaigns/CampaignLayout.vue';
|
||||||
|
import CampaignList from 'dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue';
|
||||||
|
import LiveChatCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/LiveChatCampaignDialog.vue';
|
||||||
|
import EditLiveChatCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/LiveChatCampaign/EditLiveChatCampaignDialog.vue';
|
||||||
|
import ConfirmDeleteCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue';
|
||||||
|
import LiveChatCampaignEmptyState from 'dashboard/components-next/Campaigns/EmptyState/LiveChatCampaignEmptyState.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const getters = useStoreGetters();
|
||||||
|
|
||||||
|
const editLiveChatCampaignDialogRef = ref(null);
|
||||||
|
const confirmDeleteCampaignDialogRef = ref(null);
|
||||||
|
const selectedCampaign = ref(null);
|
||||||
|
|
||||||
|
const uiFlags = useMapGetter('campaigns/getUIFlags');
|
||||||
|
const isFetchingCampaigns = computed(() => uiFlags.value.isFetching);
|
||||||
|
|
||||||
|
const [showLiveChatCampaignDialog, toggleLiveChatCampaignDialog] = useToggle();
|
||||||
|
|
||||||
|
const liveChatCampaigns = computed(() =>
|
||||||
|
getters['campaigns/getCampaigns'].value(CAMPAIGN_TYPES.ONGOING)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasNoLiveChatCampaigns = computed(
|
||||||
|
() => liveChatCampaigns.value?.length === 0 && !isFetchingCampaigns.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEdit = campaign => {
|
||||||
|
selectedCampaign.value = campaign;
|
||||||
|
editLiveChatCampaignDialogRef.value.dialogRef.open();
|
||||||
|
};
|
||||||
|
const handleDelete = campaign => {
|
||||||
|
selectedCampaign.value = campaign;
|
||||||
|
confirmDeleteCampaignDialogRef.value.dialogRef.open();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CampaignLayout
|
||||||
|
:header-title="t('CAMPAIGN.LIVE_CHAT.HEADER_TITLE')"
|
||||||
|
:button-label="t('CAMPAIGN.LIVE_CHAT.NEW_CAMPAIGN')"
|
||||||
|
@click="toggleLiveChatCampaignDialog()"
|
||||||
|
@close="toggleLiveChatCampaignDialog(false)"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<LiveChatCampaignDialog
|
||||||
|
v-if="showLiveChatCampaignDialog"
|
||||||
|
@close="toggleLiveChatCampaignDialog(false)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isFetchingCampaigns"
|
||||||
|
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<CampaignList
|
||||||
|
v-else-if="!hasNoLiveChatCampaigns"
|
||||||
|
:campaigns="liveChatCampaigns"
|
||||||
|
is-live-chat-type
|
||||||
|
@edit="handleEdit"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
<LiveChatCampaignEmptyState
|
||||||
|
v-else
|
||||||
|
:title="t('CAMPAIGN.LIVE_CHAT.EMPTY_STATE.TITLE')"
|
||||||
|
:subtitle="t('CAMPAIGN.LIVE_CHAT.EMPTY_STATE.SUBTITLE')"
|
||||||
|
class="pt-14"
|
||||||
|
/>
|
||||||
|
<EditLiveChatCampaignDialog
|
||||||
|
ref="editLiveChatCampaignDialogRef"
|
||||||
|
:selected-campaign="selectedCampaign"
|
||||||
|
/>
|
||||||
|
<ConfirmDeleteCampaignDialog
|
||||||
|
ref="confirmDeleteCampaignDialogRef"
|
||||||
|
:selected-campaign="selectedCampaign"
|
||||||
|
/>
|
||||||
|
</CampaignLayout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { CAMPAIGN_TYPES } from 'shared/constants/campaign.js';
|
||||||
|
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
import CampaignLayout from 'dashboard/components-next/Campaigns/CampaignLayout.vue';
|
||||||
|
import CampaignList from 'dashboard/components-next/Campaigns/Pages/CampaignPage/CampaignList.vue';
|
||||||
|
import SMSCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue';
|
||||||
|
import ConfirmDeleteCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue';
|
||||||
|
import SMSCampaignEmptyState from 'dashboard/components-next/Campaigns/EmptyState/SMSCampaignEmptyState.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const getters = useStoreGetters();
|
||||||
|
|
||||||
|
const selectedCampaign = ref(null);
|
||||||
|
const [showSMSCampaignDialog, toggleSMSCampaignDialog] = useToggle();
|
||||||
|
|
||||||
|
const uiFlags = useMapGetter('campaigns/getUIFlags');
|
||||||
|
const isFetchingCampaigns = computed(() => uiFlags.value.isFetching);
|
||||||
|
|
||||||
|
const confirmDeleteCampaignDialogRef = ref(null);
|
||||||
|
|
||||||
|
const SMSCampaigns = computed(() =>
|
||||||
|
getters['campaigns/getCampaigns'].value(CAMPAIGN_TYPES.ONE_OFF)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasNoSMSCampaigns = computed(
|
||||||
|
() => SMSCampaigns.value?.length === 0 && !isFetchingCampaigns.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = campaign => {
|
||||||
|
selectedCampaign.value = campaign;
|
||||||
|
confirmDeleteCampaignDialogRef.value.dialogRef.open();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CampaignLayout
|
||||||
|
:header-title="t('CAMPAIGN.SMS.HEADER_TITLE')"
|
||||||
|
:button-label="t('CAMPAIGN.SMS.NEW_CAMPAIGN')"
|
||||||
|
@click="toggleSMSCampaignDialog()"
|
||||||
|
@close="toggleSMSCampaignDialog(false)"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<SMSCampaignDialog
|
||||||
|
v-if="showSMSCampaignDialog"
|
||||||
|
@close="toggleSMSCampaignDialog(false)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="isFetchingCampaigns"
|
||||||
|
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<CampaignList
|
||||||
|
v-else-if="!hasNoSMSCampaigns"
|
||||||
|
:campaigns="SMSCampaigns"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
<SMSCampaignEmptyState
|
||||||
|
v-else
|
||||||
|
:title="t('CAMPAIGN.SMS.EMPTY_STATE.TITLE')"
|
||||||
|
:subtitle="t('CAMPAIGN.SMS.EMPTY_STATE.SUBTITLE')"
|
||||||
|
class="pt-14"
|
||||||
|
/>
|
||||||
|
<ConfirmDeleteCampaignDialog
|
||||||
|
ref="confirmDeleteCampaignDialogRef"
|
||||||
|
:selected-campaign="selectedCampaign"
|
||||||
|
/>
|
||||||
|
</CampaignLayout>
|
||||||
|
</template>
|
||||||
@@ -3,8 +3,7 @@ import { ref } from 'vue';
|
|||||||
// constants & helpers
|
// constants & helpers
|
||||||
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
||||||
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
||||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
import { getInboxSource, INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||||
import { getInboxSource } from 'dashboard/helper/inbox';
|
|
||||||
|
|
||||||
// store
|
// store
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { routes as notificationRoutes } from './notifications/routes';
|
|||||||
import { routes as inboxRoutes } from './inbox/routes';
|
import { routes as inboxRoutes } from './inbox/routes';
|
||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../helper/URLHelper';
|
||||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||||
|
import campaignsRoutes from './campaigns/campaigns.routes';
|
||||||
|
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export default {
|
|||||||
...searchRoutes,
|
...searchRoutes,
|
||||||
...notificationRoutes,
|
...notificationRoutes,
|
||||||
...helpcenterRoutes.routes,
|
...helpcenterRoutes.routes,
|
||||||
|
...campaignsRoutes.routes,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,389 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
|
||||||
import { required } from '@vuelidate/validators';
|
|
||||||
import { useAlert, useTrack } from 'dashboard/composables';
|
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
|
||||||
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';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
WootDateTimePicker,
|
|
||||||
WootMessageEditor,
|
|
||||||
},
|
|
||||||
emits: ['onClose'],
|
|
||||||
setup() {
|
|
||||||
const { campaignType, isOngoingType, isOneOffType } = useCampaign();
|
|
||||||
return { v$: useVuelidate(), campaignType, isOngoingType, isOneOffType };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
title: '',
|
|
||||||
message: '',
|
|
||||||
selectedSender: 0,
|
|
||||||
selectedInbox: null,
|
|
||||||
endPoint: '',
|
|
||||||
timeOnPage: 10,
|
|
||||||
show: true,
|
|
||||||
enabled: true,
|
|
||||||
triggerOnlyDuringBusinessHours: false,
|
|
||||||
scheduledAt: null,
|
|
||||||
selectedAudience: [],
|
|
||||||
senderList: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
validations() {
|
|
||||||
const commonValidations = {
|
|
||||||
title: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
selectedInbox: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (this.isOngoingType) {
|
|
||||||
return {
|
|
||||||
...commonValidations,
|
|
||||||
selectedSender: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
endPoint: {
|
|
||||||
required,
|
|
||||||
shouldBeAValidURLPattern(value) {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
new URLPattern(value);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shouldStartWithHTTP(value) {
|
|
||||||
if (value) {
|
|
||||||
return (
|
|
||||||
value.startsWith('https://') || value.startsWith('http://')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timeOnPage: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...commonValidations,
|
|
||||||
selectedAudience: {
|
|
||||||
isEmpty() {
|
|
||||||
return !!this.selectedAudience.length;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
uiFlags: 'campaigns/getUIFlags',
|
|
||||||
audienceList: 'labels/getLabels',
|
|
||||||
}),
|
|
||||||
inboxes() {
|
|
||||||
if (this.isOngoingType) {
|
|
||||||
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
|
||||||
}
|
|
||||||
return this.$store.getters['inboxes/getSMSInboxes'];
|
|
||||||
},
|
|
||||||
sendersAndBotList() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'Bot',
|
|
||||||
},
|
|
||||||
...this.senderList,
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
useTrack(CAMPAIGNS_EVENTS.OPEN_NEW_CAMPAIGN_MODAL, {
|
|
||||||
type: this.campaignType,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onClose() {
|
|
||||||
this.$emit('onClose');
|
|
||||||
},
|
|
||||||
onChange(value) {
|
|
||||||
this.scheduledAt = value;
|
|
||||||
},
|
|
||||||
async onChangeInbox() {
|
|
||||||
try {
|
|
||||||
const response = await this.$store.dispatch('inboxMembers/get', {
|
|
||||||
inboxId: this.selectedInbox,
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
data: { payload: inboxMembers },
|
|
||||||
} = response;
|
|
||||||
this.senderList = inboxMembers;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error?.response?.message || this.$t('CAMPAIGN.ADD.API.ERROR_MESSAGE');
|
|
||||||
useAlert(errorMessage);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCampaignDetails() {
|
|
||||||
let campaignDetails = null;
|
|
||||||
if (this.isOngoingType) {
|
|
||||||
campaignDetails = {
|
|
||||||
title: this.title,
|
|
||||||
message: this.message,
|
|
||||||
inbox_id: this.selectedInbox,
|
|
||||||
sender_id: this.selectedSender || null,
|
|
||||||
enabled: this.enabled,
|
|
||||||
trigger_only_during_business_hours:
|
|
||||||
// eslint-disable-next-line prettier/prettier
|
|
||||||
this.triggerOnlyDuringBusinessHours,
|
|
||||||
trigger_rules: {
|
|
||||||
url: this.endPoint,
|
|
||||||
time_on_page: this.timeOnPage,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const audience = this.selectedAudience.map(item => {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
type: 'Label',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
campaignDetails = {
|
|
||||||
title: this.title,
|
|
||||||
message: this.message,
|
|
||||||
inbox_id: this.selectedInbox,
|
|
||||||
scheduled_at: this.scheduledAt,
|
|
||||||
audience,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return campaignDetails;
|
|
||||||
},
|
|
||||||
async addCampaign() {
|
|
||||||
this.v$.$touch();
|
|
||||||
if (this.v$.$invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const campaignDetails = this.getCampaignDetails();
|
|
||||||
await this.$store.dispatch('campaigns/create', campaignDetails);
|
|
||||||
|
|
||||||
// tracking this here instead of the store to track the type of campaign
|
|
||||||
useTrack(CAMPAIGNS_EVENTS.CREATE_CAMPAIGN, {
|
|
||||||
type: this.campaignType,
|
|
||||||
});
|
|
||||||
|
|
||||||
useAlert(this.$t('CAMPAIGN.ADD.API.SUCCESS_MESSAGE'));
|
|
||||||
this.onClose();
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error?.response?.message || this.$t('CAMPAIGN.ADD.API.ERROR_MESSAGE');
|
|
||||||
useAlert(errorMessage);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col h-auto overflow-auto">
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('CAMPAIGN.ADD.TITLE')"
|
|
||||||
:header-content="$t('CAMPAIGN.ADD.DESC')"
|
|
||||||
/>
|
|
||||||
<form class="flex flex-col w-full" @submit.prevent="addCampaign">
|
|
||||||
<div class="w-full">
|
|
||||||
<woot-input
|
|
||||||
v-model="title"
|
|
||||||
:label="$t('CAMPAIGN.ADD.FORM.TITLE.LABEL')"
|
|
||||||
type="text"
|
|
||||||
:class="{ error: v$.title.$error }"
|
|
||||||
:error="v$.title.$error ? $t('CAMPAIGN.ADD.FORM.TITLE.ERROR') : ''"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TITLE.PLACEHOLDER')"
|
|
||||||
@blur="v$.title.$touch"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="isOngoingType" class="editor-wrap">
|
|
||||||
<label>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<WootMessageEditor
|
|
||||||
v-model="message"
|
|
||||||
class="message-editor"
|
|
||||||
:class="{ editor_warning: v$.message.$error }"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.MESSAGE.PLACEHOLDER')"
|
|
||||||
@blur="v$.message.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="v$.message.$error" class="editor-warning__message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label v-else :class="{ error: v$.message.$error }">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.LABEL') }}
|
|
||||||
<textarea
|
|
||||||
v-model="message"
|
|
||||||
rows="5"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.MESSAGE.PLACEHOLDER')"
|
|
||||||
@blur="v$.message.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="v$.message.$error" class="message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label :class="{ error: v$.selectedInbox.$error }">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.INBOX.LABEL') }}
|
|
||||||
<select v-model="selectedInbox" @change="onChangeInbox($event)">
|
|
||||||
<option v-for="item in inboxes" :key="item.name" :value="item.id">
|
|
||||||
{{ item.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="v$.selectedInbox.$error" class="message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.INBOX.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
v-if="isOneOffType"
|
|
||||||
class="multiselect-wrap--small"
|
|
||||||
:class="{ error: v$.selectedAudience.$error }"
|
|
||||||
>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.AUDIENCE.LABEL') }}
|
|
||||||
<multiselect
|
|
||||||
v-model="selectedAudience"
|
|
||||||
:options="audienceList"
|
|
||||||
track-by="id"
|
|
||||||
label="title"
|
|
||||||
multiple
|
|
||||||
:close-on-select="false"
|
|
||||||
:clear-on-select="false"
|
|
||||||
hide-selected
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.AUDIENCE.PLACEHOLDER')"
|
|
||||||
selected-label
|
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
|
||||||
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
|
|
||||||
@blur="v$.selectedAudience.$touch"
|
|
||||||
@select="v$.selectedAudience.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="v$.selectedAudience.$error" class="message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.AUDIENCE.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
v-if="isOngoingType"
|
|
||||||
:class="{ error: v$.selectedSender.$error }"
|
|
||||||
>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }}
|
|
||||||
<select v-model="selectedSender">
|
|
||||||
<option
|
|
||||||
v-for="sender in sendersAndBotList"
|
|
||||||
:key="sender.name"
|
|
||||||
:value="sender.id"
|
|
||||||
>
|
|
||||||
{{ sender.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="v$.selectedSender.$error" class="message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label v-if="isOneOffType">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.SCHEDULED_AT.LABEL') }}
|
|
||||||
<WootDateTimePicker
|
|
||||||
:value="scheduledAt"
|
|
||||||
:confirm-text="$t('CAMPAIGN.ADD.FORM.SCHEDULED_AT.CONFIRM')"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.SCHEDULED_AT.PLACEHOLDER')"
|
|
||||||
@change="onChange"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<woot-input
|
|
||||||
v-if="isOngoingType"
|
|
||||||
v-model="endPoint"
|
|
||||||
:label="$t('CAMPAIGN.ADD.FORM.END_POINT.LABEL')"
|
|
||||||
type="text"
|
|
||||||
:class="{ error: v$.endPoint.$error }"
|
|
||||||
:error="
|
|
||||||
v$.endPoint.$error ? $t('CAMPAIGN.ADD.FORM.END_POINT.ERROR') : ''
|
|
||||||
"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.END_POINT.PLACEHOLDER')"
|
|
||||||
@blur="v$.endPoint.$touch"
|
|
||||||
/>
|
|
||||||
<woot-input
|
|
||||||
v-if="isOngoingType"
|
|
||||||
v-model="timeOnPage"
|
|
||||||
:label="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.LABEL')"
|
|
||||||
type="text"
|
|
||||||
:class="{ error: v$.timeOnPage.$error }"
|
|
||||||
:error="
|
|
||||||
v$.timeOnPage.$error
|
|
||||||
? $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.ERROR')
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.PLACEHOLDER')"
|
|
||||||
@blur="v$.timeOnPage.$touch"
|
|
||||||
/>
|
|
||||||
<label v-if="isOngoingType">
|
|
||||||
<input
|
|
||||||
v-model="enabled"
|
|
||||||
type="checkbox"
|
|
||||||
value="enabled"
|
|
||||||
name="enabled"
|
|
||||||
/>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
|
|
||||||
</label>
|
|
||||||
<label v-if="isOngoingType">
|
|
||||||
<input
|
|
||||||
v-model="triggerOnlyDuringBusinessHours"
|
|
||||||
type="checkbox"
|
|
||||||
value="triggerOnlyDuringBusinessHours"
|
|
||||||
name="triggerOnlyDuringBusinessHours"
|
|
||||||
/>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.TRIGGER_ONLY_BUSINESS_HOURS') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
|
||||||
<woot-button :is-loading="uiFlags.isCreating">
|
|
||||||
{{ $t('CAMPAIGN.ADD.CREATE_BUTTON_TEXT') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button variant="clear" @click.prevent="onClose">
|
|
||||||
{{ $t('CAMPAIGN.ADD.CANCEL_BUTTON_TEXT') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
::v-deep .ProseMirror-woot-style {
|
|
||||||
height: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-editor {
|
|
||||||
@apply px-3;
|
|
||||||
|
|
||||||
::v-deep {
|
|
||||||
.ProseMirror-menubar {
|
|
||||||
@apply rounded-tl-[4px];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
import { useCampaign } from 'shared/composables/useCampaign';
|
|
||||||
import CampaignsTable from './CampaignsTable.vue';
|
|
||||||
import EditCampaign from './EditCampaign.vue';
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
CampaignsTable,
|
|
||||||
EditCampaign,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { campaignType } = useCampaign();
|
|
||||||
return { campaignType };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showEditPopup: false,
|
|
||||||
selectedCampaign: {},
|
|
||||||
showDeleteConfirmationPopup: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
uiFlags: 'campaigns/getUIFlags',
|
|
||||||
}),
|
|
||||||
campaigns() {
|
|
||||||
return this.$store.getters['campaigns/getCampaigns'](this.campaignType);
|
|
||||||
},
|
|
||||||
showEmptyResult() {
|
|
||||||
const hasEmptyResults =
|
|
||||||
!this.uiFlags.isFetching && this.campaigns.length === 0;
|
|
||||||
return hasEmptyResults;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openEditPopup(campaign) {
|
|
||||||
this.selectedCampaign = campaign;
|
|
||||||
this.showEditPopup = true;
|
|
||||||
},
|
|
||||||
hideEditPopup() {
|
|
||||||
this.showEditPopup = false;
|
|
||||||
},
|
|
||||||
openDeletePopup(campaign) {
|
|
||||||
this.showDeleteConfirmationPopup = true;
|
|
||||||
this.selectedCampaign = campaign;
|
|
||||||
},
|
|
||||||
closeDeletePopup() {
|
|
||||||
this.showDeleteConfirmationPopup = false;
|
|
||||||
},
|
|
||||||
confirmDeletion() {
|
|
||||||
this.closeDeletePopup();
|
|
||||||
const { id } = this.selectedCampaign;
|
|
||||||
this.deleteCampaign(id);
|
|
||||||
},
|
|
||||||
async deleteCampaign(id) {
|
|
||||||
try {
|
|
||||||
await this.$store.dispatch('campaigns/delete', id);
|
|
||||||
useAlert(this.$t('CAMPAIGN.DELETE.API.SUCCESS_MESSAGE'));
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(this.$t('CAMPAIGN.DELETE.API.ERROR_MESSAGE'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex-1 overflow-auto">
|
|
||||||
<CampaignsTable
|
|
||||||
:campaigns="campaigns"
|
|
||||||
:show-empty-result="showEmptyResult"
|
|
||||||
:is-loading="uiFlags.isFetching"
|
|
||||||
:campaign-type="type"
|
|
||||||
@edit="openEditPopup"
|
|
||||||
@delete="openDeletePopup"
|
|
||||||
/>
|
|
||||||
<woot-modal v-model:show="showEditPopup" :on-close="hideEditPopup">
|
|
||||||
<EditCampaign
|
|
||||||
:selected-campaign="selectedCampaign"
|
|
||||||
@on-close="hideEditPopup"
|
|
||||||
/>
|
|
||||||
</woot-modal>
|
|
||||||
<woot-delete-modal
|
|
||||||
v-model:show="showDeleteConfirmationPopup"
|
|
||||||
:on-close="closeDeletePopup"
|
|
||||||
:on-confirm="confirmDeletion"
|
|
||||||
:title="$t('CAMPAIGN.DELETE.CONFIRM.TITLE')"
|
|
||||||
:message="$t('CAMPAIGN.DELETE.CONFIRM.MESSAGE')"
|
|
||||||
:confirm-text="$t('CAMPAIGN.DELETE.CONFIRM.YES')"
|
|
||||||
:reject-text="$t('CAMPAIGN.DELETE.CONFIRM.NO')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.button-wrapper {
|
|
||||||
@apply flex justify-end pb-2.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
|
||||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
|
||||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
|
||||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
campaign: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isOngoingType: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['edit', 'delete']);
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const { formatMessage } = useMessageFormatter();
|
|
||||||
|
|
||||||
const campaignStatus = computed(() => {
|
|
||||||
if (props.isOngoingType) {
|
|
||||||
return props.campaign.enabled
|
|
||||||
? t('CAMPAIGN.LIST.STATUS.ENABLED')
|
|
||||||
: t('CAMPAIGN.LIST.STATUS.DISABLED');
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.campaign.campaign_status === 'completed'
|
|
||||||
? t('CAMPAIGN.LIST.STATUS.COMPLETED')
|
|
||||||
: t('CAMPAIGN.LIST.STATUS.ACTIVE');
|
|
||||||
});
|
|
||||||
|
|
||||||
const colorScheme = computed(() => {
|
|
||||||
if (props.isOngoingType) {
|
|
||||||
return props.campaign.enabled ? 'success' : 'secondary';
|
|
||||||
}
|
|
||||||
return props.campaign.campaign_status === 'completed'
|
|
||||||
? 'secondary'
|
|
||||||
: 'success';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="px-5 py-4 mb-2 bg-white border rounded-md dark:bg-slate-800 border-slate-50 dark:border-slate-900"
|
|
||||||
>
|
|
||||||
<div class="flex flex-row items-start justify-between">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div
|
|
||||||
class="mb-1 -mt-1 text-base font-medium text-slate-900 dark:text-slate-100"
|
|
||||||
>
|
|
||||||
{{ campaign.title }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-dompurify-html="formatMessage(campaign.message)"
|
|
||||||
class="text-sm line-clamp-1 [&>p]:mb-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row space-x-4">
|
|
||||||
<woot-button
|
|
||||||
v-if="isOngoingType"
|
|
||||||
variant="link"
|
|
||||||
icon="edit"
|
|
||||||
color-scheme="secondary"
|
|
||||||
size="small"
|
|
||||||
@click="emit('edit', campaign)"
|
|
||||||
>
|
|
||||||
{{ $t('CAMPAIGN.LIST.BUTTONS.EDIT') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
variant="link"
|
|
||||||
icon="dismiss-circle"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
@click="emit('delete', campaign)"
|
|
||||||
>
|
|
||||||
{{ $t('CAMPAIGN.LIST.BUTTONS.DELETE') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row items-center mt-5 space-x-3">
|
|
||||||
<woot-label
|
|
||||||
small
|
|
||||||
:title="campaignStatus"
|
|
||||||
:color-scheme="colorScheme"
|
|
||||||
class="mr-3 text-xs"
|
|
||||||
/>
|
|
||||||
<InboxName :inbox="campaign.inbox" class="mb-1 ltr:ml-0 rtl:mr-0" />
|
|
||||||
<UserAvatarWithName
|
|
||||||
v-if="campaign.sender"
|
|
||||||
:user="campaign.sender"
|
|
||||||
class="mb-1"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="campaign.trigger_rules.url"
|
|
||||||
:title="campaign.trigger_rules.url"
|
|
||||||
class="w-1/4 mb-1 text-xs text-woot-600 truncate"
|
|
||||||
>
|
|
||||||
{{ campaign.trigger_rules.url }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="campaign.scheduled_at"
|
|
||||||
class="w-1/4 mb-1 text-xs text-slate-700 dark:text-slate-500"
|
|
||||||
>
|
|
||||||
{{ messageStamp(new Date(campaign.scheduled_at), 'LLL d, h:mm a') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
|
||||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
|
||||||
import { useCampaign } from 'shared/composables/useCampaign';
|
|
||||||
import CampaignCard from './CampaignCard.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EmptyState,
|
|
||||||
Spinner,
|
|
||||||
CampaignCard,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
campaigns: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
showEmptyResult: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['edit', 'delete'],
|
|
||||||
setup() {
|
|
||||||
const { isOngoingType } = useCampaign();
|
|
||||||
return { isOngoingType };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
currentInboxId() {
|
|
||||||
return this.$route.params.inboxId;
|
|
||||||
},
|
|
||||||
inbox() {
|
|
||||||
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
|
|
||||||
},
|
|
||||||
inboxes() {
|
|
||||||
if (this.isOngoingType) {
|
|
||||||
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
|
||||||
}
|
|
||||||
return this.$store.getters['inboxes/getTwilioInboxes'];
|
|
||||||
},
|
|
||||||
emptyMessage() {
|
|
||||||
if (this.isOngoingType) {
|
|
||||||
return this.inboxes.length
|
|
||||||
? this.$t('CAMPAIGN.ONGOING.404')
|
|
||||||
: this.$t('CAMPAIGN.ONGOING.INBOXES_NOT_FOUND');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.inboxes.length
|
|
||||||
? this.$t('CAMPAIGN.ONE_OFF.404')
|
|
||||||
: this.$t('CAMPAIGN.ONE_OFF.INBOXES_NOT_FOUND');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center flex-col">
|
|
||||||
<div v-if="isLoading" class="items-center flex text-base justify-center">
|
|
||||||
<Spinner color-scheme="primary" />
|
|
||||||
<span>{{ $t('CAMPAIGN.LIST.LOADING_MESSAGE') }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="w-full">
|
|
||||||
<EmptyState v-if="showEmptyResult" :title="emptyMessage" />
|
|
||||||
<div v-else class="w-full">
|
|
||||||
<CampaignCard
|
|
||||||
v-for="campaign in campaigns"
|
|
||||||
:key="campaign.id"
|
|
||||||
:campaign="campaign"
|
|
||||||
:is-ongoing-type="isOngoingType"
|
|
||||||
@edit="campaign => $emit('edit', campaign)"
|
|
||||||
@delete="campaign => $emit('delete', campaign)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
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 { useCampaign } from 'shared/composables/useCampaign';
|
|
||||||
import { URLPattern } from 'urlpattern-polyfill';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
WootMessageEditor,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
selectedCampaign: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['onClose'],
|
|
||||||
setup() {
|
|
||||||
const { isOngoingType } = useCampaign();
|
|
||||||
return { v$: useVuelidate(), isOngoingType };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
title: '',
|
|
||||||
message: '',
|
|
||||||
selectedSender: '',
|
|
||||||
selectedInbox: null,
|
|
||||||
endPoint: '',
|
|
||||||
timeOnPage: 10,
|
|
||||||
triggerOnlyDuringBusinessHours: false,
|
|
||||||
show: true,
|
|
||||||
enabled: true,
|
|
||||||
senderList: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validations: {
|
|
||||||
title: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
selectedSender: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
endPoint: {
|
|
||||||
required,
|
|
||||||
shouldBeAValidURLPattern(value) {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
new URLPattern(value);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shouldStartWithHTTP(value) {
|
|
||||||
if (value) {
|
|
||||||
return value.startsWith('https://') || value.startsWith('http://');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timeOnPage: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
selectedInbox: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
uiFlags: 'campaigns/getUIFlags',
|
|
||||||
inboxes: 'inboxes/getTwilioInboxes',
|
|
||||||
}),
|
|
||||||
inboxes() {
|
|
||||||
if (this.isOngoingType) {
|
|
||||||
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
|
||||||
}
|
|
||||||
return this.$store.getters['inboxes/getSMSInboxes'];
|
|
||||||
},
|
|
||||||
pageTitle() {
|
|
||||||
return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${
|
|
||||||
this.selectedCampaign.title
|
|
||||||
}`;
|
|
||||||
},
|
|
||||||
sendersAndBotList() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: 'Bot',
|
|
||||||
},
|
|
||||||
...this.senderList,
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setFormValues();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onClose() {
|
|
||||||
this.$emit('onClose');
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadInboxMembers() {
|
|
||||||
try {
|
|
||||||
const response = await this.$store.dispatch('inboxMembers/get', {
|
|
||||||
inboxId: this.selectedInbox,
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
data: { payload: inboxMembers },
|
|
||||||
} = response;
|
|
||||||
this.senderList = inboxMembers;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error?.response?.message || this.$t('CAMPAIGN.ADD.API.ERROR_MESSAGE');
|
|
||||||
useAlert(errorMessage);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChangeInbox() {
|
|
||||||
this.loadInboxMembers();
|
|
||||||
},
|
|
||||||
setFormValues() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
enabled,
|
|
||||||
trigger_only_during_business_hours: triggerOnlyDuringBusinessHours,
|
|
||||||
inbox: { id: inboxId },
|
|
||||||
trigger_rules: { url: endPoint, time_on_page: timeOnPage },
|
|
||||||
sender,
|
|
||||||
} = this.selectedCampaign;
|
|
||||||
this.title = title;
|
|
||||||
this.message = message;
|
|
||||||
this.endPoint = endPoint;
|
|
||||||
this.timeOnPage = timeOnPage;
|
|
||||||
this.selectedInbox = inboxId;
|
|
||||||
this.triggerOnlyDuringBusinessHours = triggerOnlyDuringBusinessHours;
|
|
||||||
this.selectedSender = (sender && sender.id) || 0;
|
|
||||||
this.enabled = enabled;
|
|
||||||
this.loadInboxMembers();
|
|
||||||
},
|
|
||||||
async editCampaign() {
|
|
||||||
this.v$.$touch();
|
|
||||||
if (this.v$.$invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.$store.dispatch('campaigns/update', {
|
|
||||||
id: this.selectedCampaign.id,
|
|
||||||
title: this.title,
|
|
||||||
message: this.message,
|
|
||||||
inbox_id: this.selectedInbox,
|
|
||||||
trigger_only_during_business_hours:
|
|
||||||
// eslint-disable-next-line prettier/prettier
|
|
||||||
this.triggerOnlyDuringBusinessHours,
|
|
||||||
sender_id: this.selectedSender || null,
|
|
||||||
enabled: this.enabled,
|
|
||||||
trigger_rules: {
|
|
||||||
url: this.endPoint,
|
|
||||||
time_on_page: this.timeOnPage,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
useAlert(this.$t('CAMPAIGN.EDIT.API.SUCCESS_MESSAGE'));
|
|
||||||
this.onClose();
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(this.$t('CAMPAIGN.EDIT.API.ERROR_MESSAGE'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col h-auto overflow-auto">
|
|
||||||
<woot-modal-header :header-title="pageTitle" />
|
|
||||||
<form class="flex flex-col w-full" @submit.prevent="editCampaign">
|
|
||||||
<div class="w-full">
|
|
||||||
<woot-input
|
|
||||||
v-model="title"
|
|
||||||
:label="$t('CAMPAIGN.ADD.FORM.TITLE.LABEL')"
|
|
||||||
type="text"
|
|
||||||
:class="{ error: v$.title.$error }"
|
|
||||||
:error="v$.title.$error ? $t('CAMPAIGN.ADD.FORM.TITLE.ERROR') : ''"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TITLE.PLACEHOLDER')"
|
|
||||||
@blur="v$.title.$touch"
|
|
||||||
/>
|
|
||||||
<div class="editor-wrap">
|
|
||||||
<label>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<WootMessageEditor
|
|
||||||
v-model="message"
|
|
||||||
class="message-editor"
|
|
||||||
is-format-mode
|
|
||||||
:class="{ editor_warning: v$.message.$error }"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.MESSAGE.PLACEHOLDER')"
|
|
||||||
@input="v$.message.$touch"
|
|
||||||
/>
|
|
||||||
<span v-if="v$.message.$error" class="editor-warning__message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label :class="{ error: v$.selectedInbox.$error }">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.INBOX.LABEL') }}
|
|
||||||
<select v-model="selectedInbox" @change="onChangeInbox($event)">
|
|
||||||
<option v-for="item in inboxes" :key="item.id" :value="item.id">
|
|
||||||
{{ item.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="v$.selectedInbox.$error" class="message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.INBOX.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label :class="{ error: v$.selectedSender.$error }">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }}
|
|
||||||
<select v-model="selectedSender">
|
|
||||||
<option
|
|
||||||
v-for="sender in sendersAndBotList"
|
|
||||||
:key="sender.name"
|
|
||||||
:value="sender.id"
|
|
||||||
>
|
|
||||||
{{ sender.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="v$.selectedSender.$error" class="message">
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<woot-input
|
|
||||||
v-model="endPoint"
|
|
||||||
:label="$t('CAMPAIGN.ADD.FORM.END_POINT.LABEL')"
|
|
||||||
type="text"
|
|
||||||
:class="{ error: v$.endPoint.$error }"
|
|
||||||
:error="
|
|
||||||
v$.endPoint.$error ? $t('CAMPAIGN.ADD.FORM.END_POINT.ERROR') : ''
|
|
||||||
"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.END_POINT.PLACEHOLDER')"
|
|
||||||
@blur="v$.endPoint.$touch"
|
|
||||||
/>
|
|
||||||
<woot-input
|
|
||||||
v-model="timeOnPage"
|
|
||||||
:label="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.LABEL')"
|
|
||||||
type="text"
|
|
||||||
:class="{ error: v$.timeOnPage.$error }"
|
|
||||||
:error="
|
|
||||||
v$.timeOnPage.$error
|
|
||||||
? $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.ERROR')
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
:placeholder="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.PLACEHOLDER')"
|
|
||||||
@blur="v$.timeOnPage.$touch"
|
|
||||||
/>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
v-model="enabled"
|
|
||||||
type="checkbox"
|
|
||||||
value="enabled"
|
|
||||||
name="enabled"
|
|
||||||
/>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
|
|
||||||
</label>
|
|
||||||
<label v-if="isOngoingType">
|
|
||||||
<input
|
|
||||||
v-model="triggerOnlyDuringBusinessHours"
|
|
||||||
type="checkbox"
|
|
||||||
value="triggerOnlyDuringBusinessHours"
|
|
||||||
name="triggerOnlyDuringBusinessHours"
|
|
||||||
/>
|
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.TRIGGER_ONLY_BUSINESS_HOURS') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
|
||||||
<woot-button :is-loading="uiFlags.isCreating">
|
|
||||||
{{ $t('CAMPAIGN.EDIT.UPDATE_BUTTON_TEXT') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button variant="clear" @click.prevent="onClose">
|
|
||||||
{{ $t('CAMPAIGN.ADD.CANCEL_BUTTON_TEXT') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
::v-deep .ProseMirror-woot-style {
|
|
||||||
height: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-editor {
|
|
||||||
@apply px-3;
|
|
||||||
|
|
||||||
::v-deep {
|
|
||||||
.ProseMirror-menubar {
|
|
||||||
@apply rounded-tl-[4px];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { useCampaign } from 'shared/composables/useCampaign';
|
|
||||||
import Campaign from './Campaign.vue';
|
|
||||||
import AddCampaign from './AddCampaign.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Campaign,
|
|
||||||
AddCampaign,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { isOngoingType } = useCampaign();
|
|
||||||
return { isOngoingType };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return { showAddPopup: false };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
buttonText() {
|
|
||||||
if (this.isOngoingType) {
|
|
||||||
return this.$t('CAMPAIGN.HEADER_BTN_TXT.ONGOING');
|
|
||||||
}
|
|
||||||
return this.$t('CAMPAIGN.HEADER_BTN_TXT.ONE_OFF');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$store.dispatch('campaigns/get');
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openAddPopup() {
|
|
||||||
this.showAddPopup = true;
|
|
||||||
},
|
|
||||||
hideAddPopup() {
|
|
||||||
this.showAddPopup = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex-1 p-4 overflow-auto">
|
|
||||||
<woot-button
|
|
||||||
color-scheme="success"
|
|
||||||
class-names="button--fixed-top"
|
|
||||||
icon="add-circle"
|
|
||||||
@click="openAddPopup"
|
|
||||||
>
|
|
||||||
{{ buttonText }}
|
|
||||||
</woot-button>
|
|
||||||
<Campaign />
|
|
||||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
|
||||||
<AddCampaign @on-close="hideAddPopup" />
|
|
||||||
</woot-modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
|
||||||
import SettingsContent from '../Wrapper.vue';
|
|
||||||
import Index from './Index.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: frontendURL('accounts/:accountId/campaigns'),
|
|
||||||
component: SettingsContent,
|
|
||||||
props: {
|
|
||||||
headerTitle: 'CAMPAIGN.ONGOING.HEADER',
|
|
||||||
icon: 'arrow-swap',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
redirect: to => {
|
|
||||||
return { name: 'ongoing_campaigns', params: to.params };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'ongoing',
|
|
||||||
name: 'ongoing_campaigns',
|
|
||||||
meta: {
|
|
||||||
permissions: ['administrator'],
|
|
||||||
},
|
|
||||||
component: Index,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: frontendURL('accounts/:accountId/campaigns'),
|
|
||||||
component: SettingsContent,
|
|
||||||
props: {
|
|
||||||
headerTitle: 'CAMPAIGN.ONE_OFF.HEADER',
|
|
||||||
icon: 'sound-source',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'one_off',
|
|
||||||
name: 'one_off',
|
|
||||||
meta: {
|
|
||||||
permissions: ['administrator'],
|
|
||||||
},
|
|
||||||
component: Index,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -11,7 +11,6 @@ import attributes from './attributes/attributes.routes';
|
|||||||
import automation from './automation/automation.routes';
|
import automation from './automation/automation.routes';
|
||||||
import auditlogs from './auditlogs/audit.routes';
|
import auditlogs from './auditlogs/audit.routes';
|
||||||
import billing from './billing/billing.routes';
|
import billing from './billing/billing.routes';
|
||||||
import campaigns from './campaigns/campaigns.routes';
|
|
||||||
import canned from './canned/canned.routes';
|
import canned from './canned/canned.routes';
|
||||||
import inbox from './inbox/inbox.routes';
|
import inbox from './inbox/inbox.routes';
|
||||||
import integrations from './integrations/integrations.routes';
|
import integrations from './integrations/integrations.routes';
|
||||||
@@ -50,7 +49,6 @@ export default {
|
|||||||
...automation.routes,
|
...automation.routes,
|
||||||
...auditlogs.routes,
|
...auditlogs.routes,
|
||||||
...billing.routes,
|
...billing.routes,
|
||||||
...campaigns.routes,
|
|
||||||
...canned.routes,
|
...canned.routes,
|
||||||
...inbox.routes,
|
...inbox.routes,
|
||||||
...integrations.routes,
|
...integrations.routes,
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export const getters = {
|
|||||||
return _state.uiFlags;
|
return _state.uiFlags;
|
||||||
},
|
},
|
||||||
getCampaigns: _state => campaignType => {
|
getCampaigns: _state => campaignType => {
|
||||||
return _state.records.filter(
|
return _state.records
|
||||||
record => record.campaign_type === campaignType
|
.filter(record => record.campaign_type === campaignType)
|
||||||
);
|
.sort((a1, a2) => a1.id - a2.id);
|
||||||
},
|
},
|
||||||
getAllCampaigns: _state => {
|
getAllCampaigns: _state => {
|
||||||
return _state.records;
|
return _state.records;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||||
import * as types from '../mutation-types';
|
import * as types from '../mutation-types';
|
||||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||||
import InboxesAPI from '../../api/inboxes';
|
import InboxesAPI from '../../api/inboxes';
|
||||||
import WebChannel from '../../api/channel/webChannel';
|
import WebChannel from '../../api/channel/webChannel';
|
||||||
import FBChannel from '../../api/channel/fbChannel';
|
import FBChannel from '../../api/channel/fbChannel';
|
||||||
|
|||||||
@@ -5,36 +5,8 @@ describe('#getters', () => {
|
|||||||
it('get ongoing campaigns', () => {
|
it('get ongoing campaigns', () => {
|
||||||
const state = { records: campaigns };
|
const state = { records: campaigns };
|
||||||
expect(getters.getCampaigns(state)('ongoing')).toEqual([
|
expect(getters.getCampaigns(state)('ongoing')).toEqual([
|
||||||
{
|
campaigns[0],
|
||||||
id: 1,
|
campaigns[2],
|
||||||
title: 'Welcome',
|
|
||||||
description: null,
|
|
||||||
account_id: 1,
|
|
||||||
campaign_type: 'ongoing',
|
|
||||||
message: 'Hey, What brings you today',
|
|
||||||
enabled: true,
|
|
||||||
trigger_rules: {
|
|
||||||
url: 'https://github.com',
|
|
||||||
time_on_page: 10,
|
|
||||||
},
|
|
||||||
created_at: '2021-05-03T04:53:36.354Z',
|
|
||||||
updated_at: '2021-05-03T04:53:36.354Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Thanks',
|
|
||||||
description: null,
|
|
||||||
account_id: 1,
|
|
||||||
campaign_type: 'ongoing',
|
|
||||||
message: 'Thanks for coming to the show. How may I help you?',
|
|
||||||
enabled: false,
|
|
||||||
trigger_rules: {
|
|
||||||
url: 'https://noshow.com',
|
|
||||||
time_on_page: 10,
|
|
||||||
},
|
|
||||||
created_at: '2021-05-03T10:22:51.025Z',
|
|
||||||
updated_at: '2021-05-03T10:22:51.025Z',
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
|
||||||
import { useCampaign } from '../useCampaign';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { CAMPAIGN_TYPES } from '../../constants/campaign';
|
|
||||||
|
|
||||||
// Mock the useRoute composable
|
|
||||||
vi.mock('vue-router', () => ({
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { computed } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
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,15 +1,4 @@
|
|||||||
export const INBOX_TYPES = {
|
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||||
WEB: 'Channel::WebWidget',
|
|
||||||
FB: 'Channel::FacebookPage',
|
|
||||||
TWITTER: 'Channel::TwitterProfile',
|
|
||||||
TWILIO: 'Channel::TwilioSms',
|
|
||||||
WHATSAPP: 'Channel::Whatsapp',
|
|
||||||
API: 'Channel::Api',
|
|
||||||
EMAIL: 'Channel::Email',
|
|
||||||
TELEGRAM: 'Channel::Telegram',
|
|
||||||
LINE: 'Channel::Line',
|
|
||||||
SMS: 'Channel::Sms',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const INBOX_FEATURES = {
|
export const INBOX_FEATURES = {
|
||||||
REPLY_TO: 'replyTo',
|
REPLY_TO: 'replyTo',
|
||||||
|
|||||||
Reference in New Issue
Block a user