feat(v4): Compose new conversation
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
<script setup>
|
||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import WhatsAppOptions from './WhatsAppOptions.vue';
|
||||
|
||||
const props = defineProps({
|
||||
isWhatsappInbox: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isEmailOrWebWidgetInbox: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
// isTwilioInbox: {
|
||||
// type: Boolean,
|
||||
// required: true,
|
||||
// },
|
||||
// isApiInbox: {
|
||||
// type: Boolean,
|
||||
// required: true,
|
||||
// },
|
||||
messageTemplates: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
channelType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'discard',
|
||||
'sendMessage',
|
||||
'sendWhatsappMessage',
|
||||
'insertEmoji',
|
||||
'addSignature',
|
||||
'removeSignature',
|
||||
]);
|
||||
|
||||
const isEmojiPickerOpen = ref(false);
|
||||
|
||||
const EmojiInput = defineAsyncComponent(
|
||||
() => import('shared/components/emoji/EmojiInput.vue')
|
||||
);
|
||||
|
||||
const messageSignature = useMapGetter('getMessageSignature');
|
||||
const signatureToApply = computed(() => messageSignature.value);
|
||||
|
||||
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
|
||||
useUISettings();
|
||||
|
||||
const sendWithSignature = computed(() => {
|
||||
return fetchSignatureFlagFromUISettings(props.channelType);
|
||||
});
|
||||
|
||||
const isSignatureEnabledForInbox = computed(() => {
|
||||
return props.isEmailOrWebWidgetInbox && sendWithSignature.value;
|
||||
});
|
||||
|
||||
const setSignature = () => {
|
||||
if (signatureToApply.value) {
|
||||
if (isSignatureEnabledForInbox.value) {
|
||||
emit('addSignature', signatureToApply.value);
|
||||
} else {
|
||||
emit('removeSignature', signatureToApply.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMessageSignature = () => {
|
||||
setSignatureFlagForInbox(props.channelType, !sendWithSignature.value);
|
||||
setSignature();
|
||||
};
|
||||
|
||||
const onClickInsertEmoji = emoji => {
|
||||
emit('insertEmoji', emoji);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between w-full h-[52px] gap-2 px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<WhatsAppOptions
|
||||
v-if="isWhatsappInbox"
|
||||
:message-templates="messageTemplates"
|
||||
@send-message="emit('sendWhatsappMessage', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="!isWhatsappInbox"
|
||||
v-on-clickaway="() => (isEmojiPickerOpen = false)"
|
||||
class="relative"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-smile-plus"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!w-10"
|
||||
@click="isEmojiPickerOpen = !isEmojiPickerOpen"
|
||||
/>
|
||||
<EmojiInput
|
||||
v-if="isEmojiPickerOpen"
|
||||
class="left-0 top-full mt-1.5"
|
||||
:on-click="onClickInsertEmoji"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="isEmailOrWebWidgetInbox"
|
||||
icon="i-lucide-plus"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!w-10"
|
||||
/>
|
||||
<Button
|
||||
v-if="isEmailOrWebWidgetInbox"
|
||||
icon="i-lucide-type"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!w-10"
|
||||
@click="toggleMessageSignature"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
label="Discard"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!text-xs font-medium"
|
||||
@click="emit('discard')"
|
||||
/>
|
||||
<Button
|
||||
v-if="!isWhatsappInbox"
|
||||
label="Send (↵)"
|
||||
size="sm"
|
||||
class="!text-xs font-medium"
|
||||
@click="emit('sendMessage')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.emoji-dialog::before {
|
||||
@apply hidden;
|
||||
}
|
||||
</style>
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contacts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
showContactsDropdown: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isCreatingContact: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
contactId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
contactableInboxesList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showInboxesDropdown: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'searchContacts',
|
||||
'setSelectedContact',
|
||||
'clearSelectedContact',
|
||||
'updateDropdown',
|
||||
]);
|
||||
|
||||
const contactsList = computed(() => {
|
||||
return props.contacts?.map(({ name, id, thumbnail, email, ...rest }) => ({
|
||||
id,
|
||||
label: `${name} (${email})`,
|
||||
value: id,
|
||||
thumbnail: { name, src: thumbnail },
|
||||
...rest,
|
||||
name,
|
||||
email,
|
||||
action: 'contact',
|
||||
}));
|
||||
});
|
||||
|
||||
const selectedContactLabel = computed(() => {
|
||||
return `${props.selectedContact?.name} (${props.selectedContact?.email})`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex-1 px-4 py-3 overflow-y-visible">
|
||||
<div class="flex items-baseline w-full gap-3 min-h-7">
|
||||
<label class="text-sm font-medium text-n-slate-11 whitespace-nowrap">
|
||||
{{ 'To :' }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="selectedContact"
|
||||
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 px-3 min-h-7"
|
||||
>
|
||||
<span class="text-sm truncate text-n-slate-12">
|
||||
{{ isCreatingContact ? 'Creating contact...' : selectedContactLabel }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="i-lucide-x"
|
||||
color="slate"
|
||||
:disabled="contactId"
|
||||
size="xs"
|
||||
@click="emit('clearSelectedContact')"
|
||||
/>
|
||||
</div>
|
||||
<TagInput
|
||||
v-else
|
||||
placeholder="Search or enter email and press Enter"
|
||||
mode="single"
|
||||
:menu-items="contactsList"
|
||||
:show-dropdown="showContactsDropdown"
|
||||
:is-loading="isLoading"
|
||||
:disabled="contactableInboxesList?.length > 0 && showInboxesDropdown"
|
||||
type="email"
|
||||
class="flex-1 min-h-7"
|
||||
@focus="emit('updateDropdown', 'contacts', true)"
|
||||
@input="emit('searchContacts', $event)"
|
||||
@on-click-outside="emit('updateDropdown', 'contacts', false)"
|
||||
@add="emit('setSelectedContact', $event)"
|
||||
@remove="emit('clearSelectedContact')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
subject: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
ccEmails: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
bccEmails: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contacts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
showCcEmailsDropdown: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showBccEmailsDropdown: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showBccInput: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:ccEmails',
|
||||
'update:bccEmails',
|
||||
'update:subject',
|
||||
'searchCcEmails',
|
||||
'searchBccEmails',
|
||||
'toggleBcc',
|
||||
'updateDropdown',
|
||||
]);
|
||||
|
||||
// Convert string to array for TagInput
|
||||
const ccEmailsArray = computed(() =>
|
||||
props.ccEmails ? props.ccEmails.split(',').map(email => email.trim()) : []
|
||||
);
|
||||
|
||||
const bccEmailsArray = computed(() =>
|
||||
props.bccEmails ? props.bccEmails.split(',').map(email => email.trim()) : []
|
||||
);
|
||||
|
||||
const contactEmailsList = computed(() => {
|
||||
return props.contacts?.map(({ name, id, email }) => ({
|
||||
id,
|
||||
label: email,
|
||||
email,
|
||||
thumbnail: { name: name, src: '' },
|
||||
value: id,
|
||||
action: 'email',
|
||||
}));
|
||||
});
|
||||
|
||||
// Handle updates from TagInput and convert array back to string
|
||||
const handleCcUpdate = value => {
|
||||
emit('update:ccEmails', value.join(','));
|
||||
};
|
||||
|
||||
const handleBccUpdate = value => {
|
||||
emit('update:bccEmails', value.join(','));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col divide-y divide-n-strong">
|
||||
<div class="flex items-baseline flex-1 w-full h-8 gap-3 px-4 py-3">
|
||||
<InlineInput
|
||||
:model-value="subject"
|
||||
placeholder="Enter your email subject here"
|
||||
label="Subject :"
|
||||
focus-on-mount
|
||||
@update:model-value="emit('update:subject', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-baseline flex-1 w-full gap-3 px-4 py-3 min-h-8">
|
||||
<label
|
||||
class="mb-0.5 text-sm font-medium whitespace-nowrap text-n-slate-11"
|
||||
>
|
||||
{{ 'Cc :' }}
|
||||
</label>
|
||||
<div class="flex items-center w-full gap-3 min-h-7">
|
||||
<TagInput
|
||||
:model-value="ccEmailsArray"
|
||||
placeholder="Search or enter email and press Enter"
|
||||
:menu-items="contactEmailsList"
|
||||
:show-dropdown="showCcEmailsDropdown"
|
||||
:is-loading="isLoading"
|
||||
type="email"
|
||||
class="flex-1 min-h-7"
|
||||
@focus="emit('updateDropdown', 'cc', true)"
|
||||
@input="emit('searchCcEmails', $event)"
|
||||
@on-click-outside="emit('updateDropdown', 'cc', false)"
|
||||
@update:model-value="handleCcUpdate"
|
||||
/>
|
||||
<Button
|
||||
label="Bcc"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="slate"
|
||||
class="flex-shrink-0"
|
||||
@click="emit('toggleBcc')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showBccInput"
|
||||
class="flex items-baseline flex-1 w-full gap-3 px-4 py-3 min-h-8"
|
||||
>
|
||||
<label
|
||||
class="mb-0.5 text-sm font-medium whitespace-nowrap text-n-slate-11"
|
||||
>
|
||||
{{ 'Bcc :' }}
|
||||
</label>
|
||||
<TagInput
|
||||
:model-value="bccEmailsArray"
|
||||
placeholder="Search or enter email and press Enter"
|
||||
:menu-items="contactEmailsList"
|
||||
:show-dropdown="showBccEmailsDropdown"
|
||||
:is-loading="isLoading"
|
||||
type="email"
|
||||
class="flex-1 min-h-7"
|
||||
focus-on-mount
|
||||
@focus="emit('updateDropdown', 'bcc', true)"
|
||||
@input="emit('searchBccEmails', $event)"
|
||||
@on-click-outside="emit('updateDropdown', 'bcc', false)"
|
||||
@update:model-value="handleBccUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
const props = defineProps({
|
||||
targetInbox: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
showInboxesDropdown: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
contactableInboxesList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'updateInbox',
|
||||
'toggleDropdown',
|
||||
'handleInboxAction',
|
||||
]);
|
||||
|
||||
const generateLabelForContactableInboxesList = ({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}) => {
|
||||
if (channelType === 'EMAIL') {
|
||||
return `${name} (${email})`;
|
||||
}
|
||||
if (channelType === 'TWILIO' || channelType === 'WHATSAPP') {
|
||||
return `${name} (${phoneNumber})`;
|
||||
}
|
||||
if (channelType === 'API') {
|
||||
return `${name} (API)`;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
const targetInboxLabel = computed(() => {
|
||||
return generateLabelForContactableInboxesList(props.targetInbox);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center flex-1 w-full gap-3 px-4 py-3 overflow-y-visible"
|
||||
>
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-11 whitespace-nowrap">
|
||||
{{ 'Via inbox :' }}
|
||||
</label>
|
||||
<div
|
||||
v-if="targetInbox"
|
||||
class="flex items-center gap-1.5 rounded-md bg-n-alpha-2 truncate px-3 h-7"
|
||||
>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ targetInboxLabel }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="i-lucide-x"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="flex-shrink-0"
|
||||
@click="emit('updateInbox', null)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-on-clickaway="() => emit('toggleDropdown', false)"
|
||||
class="relative flex items-center h-7"
|
||||
>
|
||||
<Button
|
||||
label="Show inboxes"
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="slate"
|
||||
:disabled="!selectedContact"
|
||||
class="hover:!no-underline"
|
||||
@click="emit('toggleDropdown', !showInboxesDropdown)"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="contactableInboxesList?.length > 0 && showInboxesDropdown"
|
||||
:menu-items="contactableInboxesList"
|
||||
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
@action="emit('handleInboxAction', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isEmailOrWebWidgetInbox: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isWhatsappInbox: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isEmailOrWebWidgetInbox" class="flex-1 h-full">
|
||||
<Editor
|
||||
:model-value="modelValue"
|
||||
placeholder="Write your message here..."
|
||||
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4 [&>div]:!bg-transparent h-full [&_.ProseMirror-woot-style]:!max-h-[200px]"
|
||||
:show-character-count="false"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!isWhatsappInbox" class="flex-1 h-full">
|
||||
<TextArea
|
||||
:model-value="modelValue"
|
||||
placeholder="Write your message here..."
|
||||
class="!px-0 [&>div]:!px-4 [&>div]:!border-transparent [&>div]:!bg-transparent"
|
||||
auto-height
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import WhatsappTemplateParser from './WhatsappTemplateParser.vue';
|
||||
|
||||
const props = defineProps({
|
||||
messageTemplates: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage']);
|
||||
|
||||
// TODO: Remove this when we support all formats
|
||||
const formatsToRemove = ['DOCUMENT', 'IMAGE', 'VIDEO'];
|
||||
|
||||
const searchQuery = ref('');
|
||||
const selectedTemplate = ref(null);
|
||||
|
||||
const showTemplatesMenu = ref(false);
|
||||
|
||||
const whatsAppTemplateMessages = computed(() => {
|
||||
return props.messageTemplates
|
||||
.filter(template => template.status.toLowerCase() === 'approved')
|
||||
.filter(template => {
|
||||
return template.components.every(component => {
|
||||
return !formatsToRemove.includes(component.format);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
return whatsAppTemplateMessages.value.filter(template =>
|
||||
template.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const getTemplateBody = template => {
|
||||
return template.components.find(component => component.type === 'BODY').text;
|
||||
};
|
||||
|
||||
const handleTriggerClick = () => {
|
||||
searchQuery.value = '';
|
||||
showTemplatesMenu.value = !showTemplatesMenu.value;
|
||||
};
|
||||
|
||||
const handleTemplateClick = template => {
|
||||
selectedTemplate.value = template;
|
||||
showTemplatesMenu.value = false;
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
selectedTemplate.value = null;
|
||||
showTemplatesMenu.value = true;
|
||||
};
|
||||
|
||||
const handleSendMessage = template => {
|
||||
emit('sendMessage', template);
|
||||
selectedTemplate.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<Button
|
||||
icon="i-ri-whatsapp-line"
|
||||
label="Select template"
|
||||
color="slate"
|
||||
size="sm"
|
||||
:disabled="selectedTemplate"
|
||||
class="!text-xs font-medium"
|
||||
@click="handleTriggerClick"
|
||||
/>
|
||||
<div
|
||||
v-if="showTemplatesMenu"
|
||||
class="absolute top-full mt-1.5 left-0 flex flex-col gap-2 p-4 items-center w-[350px] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<div class="relative w-full">
|
||||
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
placeholder="Search templates"
|
||||
class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="template in filteredTemplates"
|
||||
:key="template.id"
|
||||
class="flex flex-col w-full gap-2 p-2 rounded-lg cursor-pointer dark:hover:bg-n-alpha-3 hover:bg-n-alpha-1"
|
||||
@click="handleTemplateClick(template)"
|
||||
>
|
||||
<span class="text-sm text-n-slate-12">{{ template.name }}</span>
|
||||
<p class="mb-0 text-xs leading-5 text-n-slate-11 line-clamp-2">
|
||||
{{ getTemplateBody(template) }}
|
||||
</p>
|
||||
</div>
|
||||
<template v-if="filteredTemplates.length === 0">
|
||||
<p class="w-full pt-2 text-sm text-n-slate-11">
|
||||
{{ 'No templates found' }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<WhatsappTemplateParser
|
||||
v-if="selectedTemplate"
|
||||
:template="selectedTemplate"
|
||||
@send-message="handleSendMessage"
|
||||
@back="handleBack"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage', 'back']);
|
||||
|
||||
const processedParams = ref({});
|
||||
|
||||
const templateString = computed(() => {
|
||||
return props.template?.components?.find(
|
||||
component => component.type === 'BODY'
|
||||
).text;
|
||||
});
|
||||
|
||||
const allKeysRequired = value => {
|
||||
const keys = Object.keys(value);
|
||||
return keys.every(key => value[key]);
|
||||
};
|
||||
|
||||
const variables = computed(() => {
|
||||
return templateString.value.match(/{{([^}]+)}}/g);
|
||||
});
|
||||
|
||||
const processVariable = str => {
|
||||
return str.replace(/{{|}}/g, '');
|
||||
};
|
||||
|
||||
const processedString = computed(() => {
|
||||
return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||
const variableKey = processVariable(variable);
|
||||
return processedParams.value[variableKey] || `{{${variable}}}`;
|
||||
});
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(
|
||||
{
|
||||
processedParams: {
|
||||
requiredIfKeysPresent: requiredIf(variables),
|
||||
allKeysRequired,
|
||||
},
|
||||
},
|
||||
{ processedParams }
|
||||
);
|
||||
|
||||
const generateVariables = () => {
|
||||
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||
if (!matchedVariables) return;
|
||||
|
||||
const finalVars = matchedVariables.map(i => processVariable(i));
|
||||
processedParams.value = finalVars.reduce((acc, variable) => {
|
||||
acc[variable] = '';
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
const payload = {
|
||||
message: processedString.value,
|
||||
templateParams: {
|
||||
name: props.template.name,
|
||||
category: props.template.category,
|
||||
language: props.template.language,
|
||||
namespace: props.template.namespace,
|
||||
processed_params: processedParams.value,
|
||||
},
|
||||
};
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
generateVariables();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-full mt-1.5 left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[460px] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ `WhatsApp template: ${template.name}` }}
|
||||
</span>
|
||||
<p class="mb-0 text-sm text-n-slate-11">{{ processedString }}</p>
|
||||
|
||||
<span
|
||||
v-if="processedParams.length"
|
||||
class="text-sm font-medium text-n-slate-12"
|
||||
>
|
||||
{{ 'Variables' }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-for="(variable, key) in processedParams"
|
||||
:key="key"
|
||||
class="flex flex-col w-full gap-4"
|
||||
>
|
||||
<span class="w-8 h-8 text-sm text-left text-n-slate-12">{{ key }}</span>
|
||||
<Input v-model="processedParams[key]" custom-input-class="!h-8" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-end justify-between w-full gap-3 h-14">
|
||||
<Button
|
||||
label="Go back"
|
||||
color="slate"
|
||||
class="w-full font-medium"
|
||||
@click="emit('back')"
|
||||
/>
|
||||
<Button
|
||||
label="Send message"
|
||||
class="w-full font-medium"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user