Merge branch 'develop' into feature/cw-7513
This commit is contained in:
+53
-2
@@ -4,8 +4,12 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
|
||||
import {
|
||||
buildPortalArticleURL,
|
||||
ARTICLE_STATUSES,
|
||||
} from 'dashboard/helper/portalHelper';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { rendersIdentically } from 'dashboard/helper/articleDiffHelper';
|
||||
|
||||
import ArticleEditor from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue';
|
||||
|
||||
@@ -40,13 +44,60 @@ const articleLink = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
// On a published article, title/content edits stage into draft_* columns (kept
|
||||
// off the live site). Anywhere else they save straight to the live record — and
|
||||
// we drop any leftover draft (e.g. left behind when the card/bulk menu moved a
|
||||
// published article to draft) so a later publish can't resurrect stale content.
|
||||
const stageDraftFields = values => {
|
||||
if (article.value?.status !== ARTICLE_STATUSES.PUBLISHED) {
|
||||
const hasStaleDraft =
|
||||
article.value?.draftTitle != null || article.value?.draftContent != null;
|
||||
if (!hasStaleDraft) return values;
|
||||
// The editor is showing the staged draft, so promote both fields to the live
|
||||
// record (the field being autosaved wins) before dropping the drafts —
|
||||
// otherwise saving one field would snap the other back to the old live value.
|
||||
return {
|
||||
...values,
|
||||
title: values.title ?? article.value.draftTitle ?? article.value.title,
|
||||
content:
|
||||
values.content ?? article.value.draftContent ?? article.value.content,
|
||||
draft_title: null,
|
||||
draft_content: null,
|
||||
};
|
||||
}
|
||||
|
||||
const staged = { ...values };
|
||||
['title', 'content'].forEach(field => {
|
||||
if (field in staged) {
|
||||
staged[`draft_${field}`] = staged[field];
|
||||
delete staged[field];
|
||||
}
|
||||
});
|
||||
|
||||
// Clear the draft when it matches the live version (a revert, or a body edit
|
||||
// the renderer ignores like a blank line) so it doesn't leave a "pending
|
||||
// changes" badge with nothing to compare. The title is shown as raw escaped
|
||||
// text, so compare it exactly; only the body is Markdown, so compare its render.
|
||||
const liveTitle = article.value.title ?? '';
|
||||
const liveContent = article.value.content ?? '';
|
||||
const nextTitle = staged.draft_title ?? article.value.draftTitle ?? liveTitle;
|
||||
const nextContent =
|
||||
staged.draft_content ?? article.value.draftContent ?? liveContent;
|
||||
if (nextTitle === liveTitle && rendersIdentically(liveContent, nextContent)) {
|
||||
staged.draft_title = null;
|
||||
staged.draft_content = null;
|
||||
}
|
||||
|
||||
return staged;
|
||||
};
|
||||
|
||||
const saveArticle = async ({ ...values }) => {
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
await store.dispatch('articles/update', {
|
||||
portalSlug,
|
||||
articleId: articleSlug,
|
||||
...values,
|
||||
...stageDraftFields(values),
|
||||
});
|
||||
isSaved.value = true;
|
||||
} catch (error) {
|
||||
|
||||
+9
-2
@@ -1,4 +1,6 @@
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
// OAuth/SDK channels need installation-level app credentials to be usable. When
|
||||
// the credential is missing the channel is "not configured" and is hidden from
|
||||
@@ -8,19 +10,24 @@ import { useMapGetter } from 'dashboard/composables/store';
|
||||
export function useChannelConfig() {
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
|
||||
const { isCloudFeatureEnabled } = useAccount();
|
||||
const installationConfig = window.chatwootConfig || {};
|
||||
|
||||
const CHANNEL_CONFIGURED = {
|
||||
// WhatsApp is onboarded only via Meta embedded signup, which needs both the
|
||||
// app id (not the 'none' sentinel) and the signup configuration id.
|
||||
whatsapp: () =>
|
||||
!isOnChatwootCloud.value &&
|
||||
(!isOnChatwootCloud.value ||
|
||||
isCloudFeatureEnabled(
|
||||
FEATURE_FLAGS.WHATSAPP_EMBEDDED_SIGNUP_INBOX_CREATION
|
||||
)) &&
|
||||
Boolean(installationConfig.whatsappAppId) &&
|
||||
installationConfig.whatsappAppId !== 'none' &&
|
||||
Boolean(installationConfig.whatsappConfigurationId),
|
||||
facebook: () => Boolean(installationConfig.fbAppId),
|
||||
instagram: () =>
|
||||
!isOnChatwootCloud.value && Boolean(installationConfig.instagramAppId),
|
||||
Boolean(installationConfig.instagramAppId) &&
|
||||
isCloudFeatureEnabled(FEATURE_FLAGS.CHANNEL_INSTAGRAM),
|
||||
tiktok: () => Boolean(installationConfig.tiktokAppId),
|
||||
gmail: () => Boolean(installationConfig.googleOAuthClientId),
|
||||
outlook: () => Boolean(globalConfig.value.azureAppId),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useWhatsappEmbeddedSignup } from 'dashboard/composables/useWhatsappEmbeddedSignup';
|
||||
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import googleClient from 'dashboard/api/channel/googleClient';
|
||||
@@ -24,17 +23,11 @@ export function useChannelConnect() {
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const connectViaOAuth = async provider => {
|
||||
const client = OAUTH_CLIENTS[provider];
|
||||
if (!client) return;
|
||||
|
||||
if (provider === 'instagram' && isOnChatwootCloud.value) {
|
||||
useAlert(t('INBOX_MGMT.ADD.INSTAGRAM.RESTRICTED_WARNING'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { url },
|
||||
|
||||
+3
@@ -6,6 +6,9 @@ vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: key => key }) }));
|
||||
vi.mock('dashboard/composables/store', () => ({
|
||||
useMapGetter: () => ({ value: {} }),
|
||||
}));
|
||||
vi.mock('dashboard/composables/useAccount', () => ({
|
||||
useAccount: () => ({ isCloudFeatureEnabled: () => true }),
|
||||
}));
|
||||
vi.mock('../../inbox-setup/useChannelConnect', () => ({
|
||||
useChannelConnect: () => ({
|
||||
connectViaOAuth: vi.fn(),
|
||||
|
||||
+23
-1
@@ -13,6 +13,7 @@ vi.mock('vue-router');
|
||||
// channel_type, social ordering) derived from CHANNEL_LIST.
|
||||
const mountComposable = ({
|
||||
brandInfo,
|
||||
features = { channel_instagram: true },
|
||||
inboxes = [],
|
||||
isOnChatwootCloud = false,
|
||||
} = {}) => {
|
||||
@@ -30,8 +31,11 @@ const mountComposable = ({
|
||||
getters: {
|
||||
getAccount: () => () => ({
|
||||
id: 1,
|
||||
features,
|
||||
custom_attributes: { brand_info: brandInfo },
|
||||
}),
|
||||
isFeatureEnabledonAccount: () => (_accountId, feature) =>
|
||||
Boolean(features[feature]),
|
||||
},
|
||||
},
|
||||
inboxes: {
|
||||
@@ -207,7 +211,7 @@ describe('useDetectedChannels', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('hides Instagram from onboarding on Chatwoot Cloud', () => {
|
||||
it('keeps Instagram available on Chatwoot Cloud when enabled for the account', () => {
|
||||
const { displayedChannels } = mountComposable({
|
||||
isOnChatwootCloud: true,
|
||||
brandInfo: {
|
||||
@@ -218,6 +222,24 @@ describe('useDetectedChannels', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(displayedChannels.value.map(channel => channel.type)).toEqual([
|
||||
'instagram',
|
||||
'tiktok',
|
||||
]);
|
||||
});
|
||||
|
||||
it('hides Instagram when disabled for the account', () => {
|
||||
const { displayedChannels } = mountComposable({
|
||||
features: { channel_instagram: false },
|
||||
isOnChatwootCloud: true,
|
||||
brandInfo: {
|
||||
socials: [
|
||||
{ type: 'instagram', url: 'https://instagram.com/acme' },
|
||||
{ type: 'tiktok', url: 'https://tiktok.com/@acme' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(displayedChannels.value.map(channel => channel.type)).toEqual([
|
||||
'tiktok',
|
||||
]);
|
||||
|
||||
@@ -387,12 +387,10 @@ export default {
|
||||
return this.inbox.provider_config?.source === 'embedded_signup';
|
||||
},
|
||||
whatsappUnauthorized() {
|
||||
// The manual migration banner supersedes the embedded-signup reauthorize flow when the feature is enabled.
|
||||
return (
|
||||
this.isAWhatsAppCloudChannel &&
|
||||
this.isEmbeddedSignupWhatsApp &&
|
||||
this.inbox.reauthorization_required &&
|
||||
!this.showWhatsAppManualMigration
|
||||
this.inbox.reauthorization_required
|
||||
);
|
||||
},
|
||||
whatsappRegistrationIncomplete() {
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import instagramClient from 'dashboard/api/channel/instagramClient';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Banner from 'dashboard/components-next/banner/Banner.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const hasError = ref(false);
|
||||
const errorStateMessage = ref('');
|
||||
const errorStateDescription = ref('');
|
||||
const isRequestingAuthorization = ref(false);
|
||||
const isInstagramConnectionRestricted = computed(() => {
|
||||
return isOnChatwootCloud.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -76,36 +68,11 @@ const requestAuthorization = async () => {
|
||||
class="text-white !rounded-full !px-6 bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCAF45]"
|
||||
lg
|
||||
icon="i-ri-instagram-line"
|
||||
:disabled="
|
||||
isRequestingAuthorization || isInstagramConnectionRestricted
|
||||
"
|
||||
:disabled="isRequestingAuthorization"
|
||||
:is-loading="isRequestingAuthorization"
|
||||
:label="$t('INBOX_MGMT.ADD.INSTAGRAM.CONTINUE_WITH_INSTAGRAM')"
|
||||
@click="requestAuthorization()"
|
||||
/>
|
||||
<Banner
|
||||
v-if="isInstagramConnectionRestricted"
|
||||
color="amber"
|
||||
class="w-full max-w-2xl mt-6"
|
||||
>
|
||||
<div class="flex items-start gap-3 text-left">
|
||||
<Icon
|
||||
icon="i-lucide-triangle-alert"
|
||||
class="flex-shrink-0 size-4 mt-0.5"
|
||||
/>
|
||||
<span>
|
||||
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.RESTRICTED_WARNING') }}
|
||||
<a
|
||||
:href="META_RESTRICTION_STATUS_URL"
|
||||
class="link underline"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.STATUS_LINK') }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</Banner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,12 @@ import CloudWhatsapp from './CloudWhatsapp.vue';
|
||||
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
|
||||
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const { isCloudFeatureEnabled, isOnChatwootCloud } = useAccount();
|
||||
|
||||
const PROVIDER_TYPES = {
|
||||
WHATSAPP: 'whatsapp',
|
||||
@@ -24,10 +24,6 @@ const PROVIDER_TYPES = {
|
||||
THREE_SIXTY_DIALOG: '360dialog',
|
||||
};
|
||||
|
||||
const isWhatsappEmbeddedSignupRestricted = computed(() => {
|
||||
return isOnChatwootCloud.value;
|
||||
});
|
||||
|
||||
const hasWhatsappAppId = computed(() => {
|
||||
return (
|
||||
window.chatwootConfig?.whatsappAppId &&
|
||||
@@ -41,6 +37,17 @@ const showProviderSelection = computed(() => !selectedProvider.value);
|
||||
|
||||
const showConfiguration = computed(() => Boolean(selectedProvider.value));
|
||||
|
||||
const shouldShowWhatsappEmbeddedSignup = computed(() => {
|
||||
return (
|
||||
selectedProvider.value === PROVIDER_TYPES.WHATSAPP &&
|
||||
hasWhatsappAppId.value &&
|
||||
(!isOnChatwootCloud.value ||
|
||||
isCloudFeatureEnabled(
|
||||
FEATURE_FLAGS.WHATSAPP_EMBEDDED_SIGNUP_INBOX_CREATION
|
||||
))
|
||||
);
|
||||
});
|
||||
|
||||
const availableProviders = computed(() => [
|
||||
{
|
||||
key: PROVIDER_TYPES.WHATSAPP,
|
||||
@@ -67,7 +74,8 @@ const selectProvider = providerValue => {
|
||||
const shouldShowCloudWhatsapp = provider => {
|
||||
return (
|
||||
provider === PROVIDER_TYPES.WHATSAPP_MANUAL ||
|
||||
(provider === PROVIDER_TYPES.WHATSAPP && !hasWhatsappAppId.value)
|
||||
(provider === PROVIDER_TYPES.WHATSAPP &&
|
||||
!shouldShowWhatsappEmbeddedSignup.value)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,17 +110,8 @@ const handleManualLinkClick = () => {
|
||||
|
||||
<div v-else-if="showConfiguration">
|
||||
<div class="px-6 py-5 rounded-2xl border border-n-weak">
|
||||
<!-- Show embedded signup if app ID is configured -->
|
||||
<div
|
||||
v-if="
|
||||
hasWhatsappAppId && selectedProvider === PROVIDER_TYPES.WHATSAPP
|
||||
"
|
||||
>
|
||||
<WhatsappEmbeddedSignup
|
||||
:is-disabled="isWhatsappEmbeddedSignupRestricted"
|
||||
:show-restriction-alert="isWhatsappEmbeddedSignupRestricted"
|
||||
:restriction-status-url="META_RESTRICTION_STATUS_URL"
|
||||
/>
|
||||
<div v-if="shouldShowWhatsappEmbeddedSignup">
|
||||
<WhatsappEmbeddedSignup />
|
||||
|
||||
<!-- Manual setup fallback option -->
|
||||
<div class="pt-6 mt-6 border-t border-n-weak">
|
||||
|
||||
+1
-44
@@ -7,7 +7,6 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { useWhatsappEmbeddedSignup } from 'dashboard/composables/useWhatsappEmbeddedSignup';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import NextButton from 'next/button/Button.vue';
|
||||
import Banner from 'next/banner/Banner.vue';
|
||||
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
|
||||
import InboxesAPI from 'dashboard/api/inboxes';
|
||||
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
@@ -18,22 +17,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showRestrictionAlert: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
restrictionStatusUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
restrictionWarningText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
@@ -98,8 +81,6 @@ const handleSignupSuccess = async inboxData => {
|
||||
};
|
||||
|
||||
const launchEmbeddedSignup = async () => {
|
||||
if (props.isDisabled) return;
|
||||
|
||||
let credentials;
|
||||
try {
|
||||
credentials = await runEmbeddedSignup();
|
||||
@@ -193,33 +174,9 @@ const launchEmbeddedSignup = async () => {
|
||||
</I18nT>
|
||||
</div>
|
||||
|
||||
<Banner v-if="showRestrictionAlert" color="amber" class="w-full mb-6">
|
||||
<div class="flex items-start gap-3 text-left">
|
||||
<Icon
|
||||
icon="i-lucide-triangle-alert"
|
||||
class="flex-shrink-0 size-4 mt-0.5"
|
||||
/>
|
||||
<span>
|
||||
{{
|
||||
restrictionWarningText ||
|
||||
$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.RESTRICTED_WARNING')
|
||||
}}
|
||||
<a
|
||||
v-if="restrictionStatusUrl"
|
||||
:href="restrictionStatusUrl"
|
||||
class="link underline"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.STATUS_LINK') }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</Banner>
|
||||
|
||||
<div class="flex mt-4">
|
||||
<NextButton
|
||||
:disabled="isAuthenticating || isDisabled"
|
||||
:disabled="isAuthenticating"
|
||||
:is-loading="isAuthenticating"
|
||||
faded
|
||||
slate
|
||||
|
||||
+5
-6
@@ -7,8 +7,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
const emit = defineEmits(['start']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const WHATSAPP_MANUAL_MIGRATION_GUIDE_URL =
|
||||
'https://www.chatwoot.com/hc/user-guide/articles/1756799850-how-to-setup-a-whats_app-channel-manual-flow';
|
||||
const WHATSAPP_MANUAL_MIGRATION_GUIDE_URL = 'https://chwt.app/migrate-whatsapp';
|
||||
|
||||
const copy = computed(() => ({
|
||||
title: t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANUAL_MIGRATION.BANNER.TITLE'),
|
||||
@@ -21,14 +20,14 @@ const copy = computed(() => ({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Banner color="amber" :action-label="copy.start" @action="emit('start')">
|
||||
<Banner color="blue" :action-label="copy.start" @action="emit('start')">
|
||||
<div class="flex items-start gap-2">
|
||||
<Icon
|
||||
icon="i-lucide-triangle-alert"
|
||||
class="flex-shrink-0 mt-0.5 size-4 text-n-amber-11"
|
||||
icon="i-lucide-info"
|
||||
class="flex-shrink-0 mt-0.5 size-4 text-n-blue-11"
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-medium text-n-amber-12">{{ copy.title }}</span>
|
||||
<span class="font-medium text-n-blue-12">{{ copy.title }}</span>
|
||||
<span>
|
||||
{{ copy.description }}
|
||||
<a
|
||||
|
||||
+3
-4
@@ -23,8 +23,7 @@ const emit = defineEmits(['reconnect']);
|
||||
const { t } = useI18n();
|
||||
const { replaceInstallationName } = useBranding();
|
||||
|
||||
const WHATSAPP_MANUAL_MIGRATION_GUIDE_URL =
|
||||
'https://www.chatwoot.com/hc/user-guide/articles/1756799850-how-to-setup-a-whats_app-channel-manual-flow';
|
||||
const WHATSAPP_MANUAL_MIGRATION_GUIDE_URL = 'https://chwt.app/migrate-whatsapp';
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const currentStep = ref(0);
|
||||
@@ -310,9 +309,9 @@ defineExpose({ open, close });
|
||||
class="flex gap-3 p-3 border rounded-xl border-n-weak bg-n-alpha-2"
|
||||
>
|
||||
<span
|
||||
class="grid flex-shrink-0 rounded-lg size-8 place-content-center bg-n-amber-3 text-n-amber-11"
|
||||
class="grid flex-shrink-0 rounded-lg size-8 place-content-center bg-n-blue-3 text-n-blue-11"
|
||||
>
|
||||
<Icon icon="i-lucide-triangle-alert" class="size-4" />
|
||||
<Icon icon="i-lucide-info" class="size-4" />
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="mt-0 mb-1 text-base font-medium text-n-slate-12">
|
||||
|
||||
+59
-1
@@ -1,5 +1,9 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useWhatsappEmbeddedSignup } from 'dashboard/composables/useWhatsappEmbeddedSignup';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import whatsappChannel from 'dashboard/api/channel/whatsappChannel';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
|
||||
import SettingsToggleSection from 'dashboard/components-next/Settings/SettingsToggleSection.vue';
|
||||
@@ -30,7 +34,8 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
|
||||
return { v$: useVuelidate(), runEmbeddedSignup };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -41,15 +46,29 @@ export default {
|
||||
allowedDomains: '',
|
||||
isUpdatingAllowedDomains: false,
|
||||
isSettingDefaults: false,
|
||||
isReconfiguring: false,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
whatsAppInboxAPIKey: { required },
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
isEmbeddedSignupWhatsApp() {
|
||||
return this.inbox.provider_config?.source === 'embedded_signup';
|
||||
},
|
||||
showWhatsAppReconfigure() {
|
||||
return (
|
||||
this.isEmbeddedSignupWhatsApp &&
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.WHATSAPP_RECONFIGURE
|
||||
)
|
||||
);
|
||||
},
|
||||
isForwardingEnabled() {
|
||||
return !!this.inbox.forwarding_enabled;
|
||||
},
|
||||
@@ -160,6 +179,28 @@ export default {
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
async reconfigureWhatsApp() {
|
||||
this.isReconfiguring = true;
|
||||
try {
|
||||
const credentials = await this.runEmbeddedSignup();
|
||||
// User dismissed the Meta popup without completing signup.
|
||||
if (!credentials) return;
|
||||
|
||||
await whatsappChannel.reauthorizeWhatsApp({
|
||||
inboxId: this.inbox.id,
|
||||
...credentials,
|
||||
});
|
||||
useAlert(
|
||||
this.$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_SUCCESS')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_ERROR')
|
||||
);
|
||||
} finally {
|
||||
this.isReconfiguring = false;
|
||||
}
|
||||
},
|
||||
async syncTemplates() {
|
||||
this.isSyncingTemplates = true;
|
||||
try {
|
||||
@@ -358,6 +399,23 @@ export default {
|
||||
>
|
||||
<woot-code :script="inbox.provider_config.webhook_verify_token" />
|
||||
</SettingsFieldSection>
|
||||
<SettingsFieldSection
|
||||
v-if="showWhatsAppReconfigure"
|
||||
:label="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_EMBEDDED_SIGNUP_TITLE')
|
||||
"
|
||||
:help-text="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION')
|
||||
"
|
||||
>
|
||||
<NextButton
|
||||
:is-loading="isReconfiguring"
|
||||
:disabled="isReconfiguring"
|
||||
@click="reconfigureWhatsApp"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_BUTTON') }}
|
||||
</NextButton>
|
||||
</SettingsFieldSection>
|
||||
</template>
|
||||
|
||||
<!-- Manual Setup Section -->
|
||||
|
||||
@@ -5,9 +5,11 @@ import { useBranding } from 'shared/composables/useBranding';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { BaseTable } from 'dashboard/components-next/table';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import NewWebhook from './NewWebHook.vue';
|
||||
import EditWebhook from './EditWebHook.vue';
|
||||
import WebhookRow from './WebhookRow.vue';
|
||||
import WebhookPaywall from './WebhookPaywall.vue';
|
||||
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../../SettingsLayout.vue';
|
||||
|
||||
@@ -20,6 +22,7 @@ export default {
|
||||
NewWebhook,
|
||||
EditWebhook,
|
||||
WebhookRow,
|
||||
WebhookPaywall,
|
||||
},
|
||||
setup() {
|
||||
const { replaceInstallationName } = useBranding();
|
||||
@@ -39,7 +42,19 @@ export default {
|
||||
...mapGetters({
|
||||
records: 'webhooks/getWebhooks',
|
||||
uiFlags: 'webhooks/getUIFlags',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
}),
|
||||
apiAndWebhooksEnabled() {
|
||||
return (
|
||||
!this.isOnChatwootCloud ||
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.API_AND_WEBHOOKS
|
||||
)
|
||||
);
|
||||
},
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration']('webhook');
|
||||
},
|
||||
@@ -57,9 +72,16 @@ export default {
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
apiAndWebhooksEnabled: {
|
||||
immediate: true,
|
||||
handler(enabled) {
|
||||
if (enabled) this.$store.dispatch('webhooks/get');
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('integrations/get', 'webhook');
|
||||
this.$store.dispatch('webhooks/get');
|
||||
},
|
||||
methods: {
|
||||
openAddPopup() {
|
||||
@@ -105,10 +127,10 @@ export default {
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.fetchingList"
|
||||
:is-loading="apiAndWebhooksEnabled && uiFlags.fetchingList"
|
||||
:loading-message="$t('INTEGRATION_SETTINGS.WEBHOOK.LOADING')"
|
||||
:no-records-message="$t('INTEGRATION_SETTINGS.WEBHOOK.LIST.404')"
|
||||
:no-records-found="!records.length"
|
||||
:no-records-found="apiAndWebhooksEnabled && !records.length"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
@@ -118,19 +140,21 @@ export default {
|
||||
:description="replaceInstallationName(integration.description)"
|
||||
:link-text="$t('INTEGRATION_SETTINGS.WEBHOOK.LEARN_MORE')"
|
||||
:search-placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.SEARCH_PLACEHOLDER')
|
||||
apiAndWebhooksEnabled
|
||||
? $t('INTEGRATION_SETTINGS.WEBHOOK.SEARCH_PLACEHOLDER')
|
||||
: ''
|
||||
"
|
||||
feature-name="webhook"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
>
|
||||
<template v-if="records?.length" #count>
|
||||
<template v-if="apiAndWebhooksEnabled && records?.length" #count>
|
||||
<span class="text-body-main text-n-slate-11">
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.COUNT', { n: records.length })
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<template v-if="apiAndWebhooksEnabled" #actions>
|
||||
<NextButton
|
||||
blue
|
||||
:label="$t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT')"
|
||||
@@ -141,7 +165,9 @@ export default {
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<WebhookPaywall v-if="!apiAndWebhooksEnabled" />
|
||||
<BaseTable
|
||||
v-else
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
@@ -160,11 +186,19 @@ export default {
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
||||
<woot-modal
|
||||
v-if="apiAndWebhooksEnabled"
|
||||
v-model:show="showAddPopup"
|
||||
:on-close="hideAddPopup"
|
||||
>
|
||||
<NewWebhook v-if="showAddPopup" :on-close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
|
||||
<woot-modal v-model:show="showEditPopup" :on-close="hideEditPopup">
|
||||
<woot-modal
|
||||
v-if="apiAndWebhooksEnabled"
|
||||
v-model:show="showEditPopup"
|
||||
:on-close="hideEditPopup"
|
||||
>
|
||||
<EditWebhook
|
||||
v-if="showEditPopup"
|
||||
:id="selectedWebHook.id"
|
||||
@@ -173,6 +207,7 @@ export default {
|
||||
/>
|
||||
</woot-modal>
|
||||
<woot-delete-modal
|
||||
v-if="apiAndWebhooksEnabled"
|
||||
v-model:show="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const accountId = useMapGetter('getCurrentAccountId');
|
||||
|
||||
const openBilling = () => {
|
||||
router.push({
|
||||
name: 'billing_settings_index',
|
||||
params: { accountId: accountId.value },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid place-content-center w-full h-full max-h-[28rem] mx-auto">
|
||||
<BasePaywallModal
|
||||
class="mx-auto"
|
||||
feature-prefix="INTEGRATION_SETTINGS.WEBHOOK"
|
||||
i18n-key="PAYWALL"
|
||||
is-on-chatwoot-cloud
|
||||
@upgrade="openBilling"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,6 +6,7 @@ import ConfirmButton from 'dashboard/components-next/button/ConfirmButton.vue';
|
||||
const props = defineProps({
|
||||
value: { type: String, default: '' },
|
||||
showResetButton: { type: Boolean, default: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onCopy', 'onReset']);
|
||||
@@ -41,12 +42,14 @@ const onReset = () => {
|
||||
}"
|
||||
:type="inputType"
|
||||
:model-value="value"
|
||||
:disabled="disabled"
|
||||
readonly
|
||||
>
|
||||
<template #masked>
|
||||
<button
|
||||
class="absolute top-0 bottom-0 ltr:right-0.5 rtl:left-0.5"
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
@click="toggleMasked"
|
||||
>
|
||||
<fluent-icon :icon="maskIcon" :size="16" />
|
||||
@@ -61,6 +64,7 @@ const onReset = () => {
|
||||
type="button"
|
||||
icon="i-lucide-copy"
|
||||
class="rounded-xl"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
/>
|
||||
<ConfirmButton
|
||||
@@ -73,6 +77,7 @@ const onReset = () => {
|
||||
variant="outline"
|
||||
icon="i-lucide-key-round"
|
||||
class="rounded-xl"
|
||||
:disabled="disabled"
|
||||
@click="onReset"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,24 @@ export default {
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
globalConfig: 'globalConfig/get',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
}),
|
||||
apiAndWebhooksEnabled() {
|
||||
if (!this.isOnChatwootCloud) return true;
|
||||
|
||||
return this.currentUser.accounts.some(
|
||||
account => account.api_and_webhooks
|
||||
);
|
||||
},
|
||||
accessTokenDescription() {
|
||||
if (!this.apiAndWebhooksEnabled) {
|
||||
return this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.PAID_PLAN_NOTE');
|
||||
}
|
||||
|
||||
return this.replaceInstallationName(
|
||||
this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE')
|
||||
);
|
||||
},
|
||||
isMfaEnabled() {
|
||||
return parseBoolean(window.chatwootConfig?.isMfaEnabled);
|
||||
},
|
||||
@@ -191,10 +208,14 @@ export default {
|
||||
useAlert(this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS'));
|
||||
},
|
||||
async onCopyToken(value) {
|
||||
if (!this.apiAndWebhooksEnabled) return;
|
||||
|
||||
await copyTextToClipboard(value);
|
||||
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
async resetAccessToken() {
|
||||
if (!this.apiAndWebhooksEnabled) return;
|
||||
|
||||
const success = await this.$store.dispatch('resetAccessToken');
|
||||
if (success) {
|
||||
useAlert(this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET_SUCCESS'));
|
||||
@@ -339,12 +360,11 @@ export default {
|
||||
<SectionLayout
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
|
||||
:description="
|
||||
replaceInstallationName($t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'))
|
||||
"
|
||||
:description="accessTokenDescription"
|
||||
>
|
||||
<AccessToken
|
||||
:value="currentUser.access_token"
|
||||
:disabled="!apiAndWebhooksEnabled"
|
||||
@on-copy="onCopyToken"
|
||||
@on-reset="resetAccessToken"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user