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