Merge remote-tracking branch 'origin/feat/voice-call-model' into feat/whatsapp-call

# Conflicts:
#	config/features.yml
#	db/schema.rb
#	enterprise/app/models/enterprise/concerns/account.rb
This commit is contained in:
root
2026-04-12 17:03:14 +00:00
532 changed files with 23290 additions and 16591 deletions
@@ -23,6 +23,12 @@ const meta = {
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
};
const metaCustomTools = {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS,
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
};
const metaV2 = {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
@@ -46,7 +52,7 @@ const assistantRoutes = [
path: frontendURL('accounts/:accountId/captain/:assistantId/tools'),
component: CustomToolsIndex,
name: 'captain_tools_index',
meta: metaV2,
meta: metaCustomTools,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/scenarios'),
@@ -4,9 +4,13 @@ import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useRoute } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useAccount } from 'dashboard/composables/useAccount';
import { usePolicy } from 'dashboard/composables/usePolicy';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
import DocumentCard from 'dashboard/components-next/captain/assistant/DocumentCard.vue';
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
import Policy from 'dashboard/components/policy.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
import RelatedResponses from 'dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue';
@@ -14,9 +18,12 @@ import CreateDocumentDialog from 'dashboard/components-next/captain/pageComponen
import DocumentPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue';
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
import LimitBanner from 'dashboard/components-next/captain/pageComponents/document/LimitBanner.vue';
import { useI18n } from 'vue-i18n';
const route = useRoute();
const store = useStore();
const { t } = useI18n();
const { checkPermissions } = usePolicy();
const { isOnChatwootCloud } = useAccount();
const uiFlags = useMapGetter('captainDocuments/getUIFlags');
@@ -25,9 +32,13 @@ const isFetching = computed(() => uiFlags.value.fetchingList);
const documentsMeta = useMapGetter('captainDocuments/getMeta');
const selectedAssistantId = computed(() => Number(route.params.assistantId));
const canManageDocuments = computed(() => checkPermissions(['administrator']));
const selectedDocument = ref(null);
const deleteDocumentDialog = ref(null);
const bulkDeleteDialog = ref(null);
const bulkSelectedIds = ref(new Set());
const hoveredCard = ref(null);
const handleDelete = () => {
deleteDocumentDialog.value.dialogRef.open();
@@ -78,7 +89,14 @@ const fetchDocuments = (page = 1) => {
store.dispatch('captainDocuments/get', filterParams);
};
const onPageChange = page => fetchDocuments(page);
const onPageChange = page => {
const hadSelection = bulkSelectedIds.value.size > 0;
fetchDocuments(page);
if (hadSelection) {
bulkSelectedIds.value = new Set();
}
};
const onDeleteSuccess = () => {
if (documents.value?.length === 0 && documentsMeta.value?.page > 1) {
@@ -86,6 +104,58 @@ const onDeleteSuccess = () => {
}
};
const buildSelectedCountLabel = computed(() => {
const count = documents.value?.length || 0;
const isAllSelected = bulkSelectedIds.value.size === count && count > 0;
return isAllSelected
? t('CAPTAIN.DOCUMENTS.UNSELECT_ALL', { count })
: t('CAPTAIN.DOCUMENTS.SELECT_ALL', { count });
});
const selectedCountLabel = computed(() => {
return t('CAPTAIN.DOCUMENTS.SELECTED', {
count: bulkSelectedIds.value.size,
});
});
const hasBulkSelection = computed(() => bulkSelectedIds.value.size > 0);
const shouldShowSelectionControl = docId => {
return (
canManageDocuments.value &&
(hoveredCard.value === docId || hasBulkSelection.value)
);
};
const handleCardHover = (isHovered, id) => {
hoveredCard.value = isHovered ? id : null;
};
const handleCardSelect = id => {
if (!canManageDocuments.value) return;
const selected = new Set(bulkSelectedIds.value);
selected[selected.has(id) ? 'delete' : 'add'](id);
bulkSelectedIds.value = selected;
};
const fetchDocumentsAfterBulkAction = () => {
const hasNoDocumentsLeft = documents.value?.length === 0;
const currentPage = documentsMeta.value?.page;
if (hasNoDocumentsLeft) {
const pageToFetch = currentPage > 1 ? currentPage - 1 : currentPage;
fetchDocuments(pageToFetch);
} else {
fetchDocuments(currentPage);
}
bulkSelectedIds.value = new Set();
};
const onBulkDeleteSuccess = () => {
fetchDocumentsAfterBulkAction();
};
onMounted(() => {
fetchDocuments();
});
@@ -106,6 +176,21 @@ onMounted(() => {
@update:current-page="onPageChange"
@click="handleCreateDocument"
>
<template #subHeader>
<Policy :permissions="['administrator']">
<BulkSelectBar
v-model="bulkSelectedIds"
:all-items="documents"
:select-all-label="buildSelectedCountLabel"
:selected-count-label="selectedCountLabel"
:delete-label="$t('CAPTAIN.DOCUMENTS.BULK_DELETE_BUTTON')"
class="w-fit"
:class="{ 'mb-2': bulkSelectedIds.size > 0 }"
@bulk-delete="bulkDeleteDialog.dialogRef.open()"
/>
</Policy>
</template>
<template #knowMore>
<FeatureSpotlightPopover
:button-label="$t('CAPTAIN.HEADER_KNOW_MORE')"
@@ -138,7 +223,13 @@ onMounted(() => {
:external-link="doc.external_link"
:assistant="doc.assistant"
:created-at="doc.created_at"
:is-selected="canManageDocuments && bulkSelectedIds.has(doc.id)"
:selectable="canManageDocuments"
:show-selection-control="shouldShowSelectionControl(doc.id)"
:show-menu="!bulkSelectedIds.has(doc.id)"
@action="handleAction"
@select="handleCardSelect"
@hover="isHovered => handleCardHover(isHovered, doc.id)"
/>
</div>
</template>
@@ -162,5 +253,12 @@ onMounted(() => {
type="Documents"
@delete-success="onDeleteSuccess"
/>
<BulkDeleteDialog
v-if="bulkSelectedIds"
ref="bulkDeleteDialog"
:bulk-ids="bulkSelectedIds"
type="AssistantDocument"
@delete-success="onBulkDeleteSuccess"
/>
</PageLayout>
</template>
@@ -316,7 +316,7 @@ onMounted(() => {
v-if="bulkSelectedIds"
ref="bulkDeleteDialog"
:bulk-ids="bulkSelectedIds"
type="Responses"
type="AssistantResponse"
@delete-success="onBulkDeleteSuccess"
/>
@@ -361,7 +361,7 @@ onMounted(() => {
v-if="bulkSelectedIds"
ref="bulkDeleteDialog"
:bulk-ids="bulkSelectedIds"
type="Responses"
type="AssistantResponse"
@delete-success="onBulkDeleteSuccess"
/>
@@ -2,6 +2,7 @@
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { usePolicy } from 'dashboard/composables/usePolicy';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
@@ -11,12 +12,20 @@ import CustomToolCard from 'dashboard/components-next/captain/pageComponents/cus
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
const store = useStore();
const { isFeatureFlagEnabled, shouldShowPaywall } = usePolicy();
const SOFT_LIMIT = 10;
const isV2 = computed(() => isFeatureFlagEnabled(FEATURE_FLAGS.CAPTAIN_V2));
const uiFlags = useMapGetter('captainCustomTools/getUIFlags');
const customTools = useMapGetter('captainCustomTools/getRecords');
const isFetching = computed(() => uiFlags.value.fetchingList);
const customToolsMeta = useMapGetter('captainCustomTools/getMeta');
const showSoftLimitWarning = computed(
() => !isV2.value && customToolsMeta.value.totalCount > SOFT_LIMIT
);
const createDialogRef = ref(null);
const deleteDialogRef = ref(null);
const selectedTool = ref(null);
@@ -72,7 +81,9 @@ const onDeleteSuccess = () => {
};
onMounted(() => {
fetchCustomTools();
if (!shouldShowPaywall(FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS)) {
fetchCustomTools();
}
});
</script>
@@ -81,18 +92,18 @@ onMounted(() => {
:header-title="$t('CAPTAIN.CUSTOM_TOOLS.HEADER')"
:button-label="$t('CAPTAIN.CUSTOM_TOOLS.ADD_NEW')"
:button-policy="['administrator']"
:feature-flag="FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS"
:total-count="customToolsMeta.totalCount"
:current-page="customToolsMeta.page"
:show-pagination-footer="!isFetching && !!customTools.length"
:is-fetching="isFetching"
:is-empty="!customTools.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN_V2"
:show-know-more="false"
@update:current-page="onPageChange"
@click="openCreateDialog"
>
<template #paywall>
<CaptainPaywall />
<CaptainPaywall feature-prefix="CAPTAIN.CUSTOM_TOOLS" />
</template>
<template #emptyState>
@@ -101,6 +112,13 @@ onMounted(() => {
<template #body>
<div class="flex flex-col gap-4">
<div
v-if="showSoftLimitWarning"
class="flex items-center gap-2 px-4 py-3 text-sm rounded-lg bg-n-amber-2 text-n-amber-11"
>
<span class="i-lucide-triangle-alert size-4 shrink-0" />
{{ $t('CAPTAIN.CUSTOM_TOOLS.SOFT_LIMIT_WARNING') }}
</div>
<CustomToolCard
v-for="tool in customTools"
:id="tool.id"
@@ -36,27 +36,27 @@ export default {
{
id: null,
name: this.$t('CONVERSATION.PRIORITY.OPTIONS.NONE'),
thumbnail: `/assets/images/dashboard/priority/none.svg`,
icon: 'i-woot-priority-empty',
},
{
id: CONVERSATION_PRIORITY.URGENT,
name: this.$t('CONVERSATION.PRIORITY.OPTIONS.URGENT'),
thumbnail: `/assets/images/dashboard/priority/${CONVERSATION_PRIORITY.URGENT}.svg`,
icon: 'i-woot-priority-urgent',
},
{
id: CONVERSATION_PRIORITY.HIGH,
name: this.$t('CONVERSATION.PRIORITY.OPTIONS.HIGH'),
thumbnail: `/assets/images/dashboard/priority/${CONVERSATION_PRIORITY.HIGH}.svg`,
icon: 'i-woot-priority-high',
},
{
id: CONVERSATION_PRIORITY.MEDIUM,
name: this.$t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'),
thumbnail: `/assets/images/dashboard/priority/${CONVERSATION_PRIORITY.MEDIUM}.svg`,
icon: 'i-woot-priority-medium',
},
{
id: CONVERSATION_PRIORITY.LOW,
name: this.$t('CONVERSATION.PRIORITY.OPTIONS.LOW'),
thumbnail: `/assets/images/dashboard/priority/${CONVERSATION_PRIORITY.LOW}.svg`,
icon: 'i-woot-priority-low',
},
],
};
@@ -39,7 +39,7 @@ const createNewArticle = async ({ title, content }) => {
if (title) article.value.title = title;
if (content) article.value.content = content;
if (!article.value.title || !article.value.content) return;
if (!article.value.title) return;
isUpdating.value = true;
try {
@@ -103,7 +103,10 @@ export default {
const { name, locale, id, domain, support_email, features } =
this.getAccount(this.accountId);
this.$root.$i18n.locale = this.uiSettings?.locale || locale;
const effectiveLocale = this.uiSettings?.locale || locale;
if (effectiveLocale) {
this.$root.$i18n.locale = effectiveLocale;
}
this.name = name;
this.locale = locale;
this.id = id;
@@ -129,11 +132,9 @@ export default {
support_email: this.supportEmail,
});
// If user locale is set, update the locale with user locale
if (this.uiSettings?.locale) {
this.$root.$i18n.locale = this.uiSettings?.locale;
} else {
// If user locale is not set, update the locale with account locale
this.$root.$i18n.locale = this.locale;
const updatedLocale = this.uiSettings?.locale || this.locale;
if (updatedLocale) {
this.$root.$i18n.locale = updatedLocale;
}
this.getAccount(this.id).locale = this.locale;
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
@@ -47,6 +47,7 @@ const formState = reactive({
const [showAccessToken, toggleAccessToken] = useToggle();
const accessToken = ref('');
const botSecret = ref('');
const v$ = useVuelidate(
{
@@ -179,15 +180,21 @@ const handleSubmit = async () => {
: t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE');
useAlert(alertKey);
// Show access token after creation
// Show access token and secret after creation
if (isCreate) {
const { access_token: responseAccessToken, id } = response || {};
const {
access_token: responseAccessToken,
secret: responseSecret,
id,
} = response || {};
if (id && responseAccessToken) {
accessToken.value = responseAccessToken;
botSecret.value = responseSecret || '';
toggleAccessToken(true);
} else {
accessToken.value = '';
botSecret.value = '';
dialogRef.value.close();
}
} else {
@@ -212,14 +219,16 @@ const initializeForm = () => {
thumbnail,
bot_config: botConfig,
access_token: botAccessToken,
secret: botSecretValue,
} = props.selectedBot;
formState.botName = name || '';
formState.botDescription = description || '';
formState.botUrl = botUrl || botConfig?.webhook_url || '';
formState.botAvatarUrl = thumbnail || '';
if (botAccessToken && props.type === MODAL_TYPES.EDIT) {
accessToken.value = botAccessToken;
if (props.type === MODAL_TYPES.EDIT) {
if (botAccessToken) accessToken.value = botAccessToken;
if (botSecretValue) botSecret.value = botSecretValue;
}
} else {
resetForm();
@@ -231,6 +240,24 @@ const onCopyToken = async value => {
useAlert(t('AGENT_BOTS.ACCESS_TOKEN.COPY_SUCCESSFUL'));
};
const onCopySecret = async value => {
await copyTextToClipboard(value || botSecret.value);
useAlert(t('AGENT_BOTS.SECRET.COPY_SUCCESS'));
};
const onResetSecret = async () => {
const response = await store.dispatch(
'agentBots/resetSecret',
props.selectedBot.id
);
if (response) {
botSecret.value = response.secret;
useAlert(t('AGENT_BOTS.SECRET.RESET_SUCCESS'));
} else {
useAlert(t('AGENT_BOTS.SECRET.RESET_ERROR'));
}
};
const onResetToken = async () => {
const response = await store.dispatch(
'agentBots/resetAccessToken',
@@ -247,6 +274,7 @@ const onResetToken = async () => {
const closeModal = () => {
if (!showAccessToken.value) v$.value?.$reset();
accessToken.value = '';
botSecret.value = '';
toggleAccessToken(false);
};
@@ -318,6 +346,20 @@ defineExpose({ dialogRef });
/>
</div>
<div
v-if="botSecret && type === MODAL_TYPES.EDIT"
class="flex flex-col gap-1"
>
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ $t('AGENT_BOTS.SECRET.LABEL') }}
</label>
<AccessToken
:value="botSecret"
@on-copy="onCopySecret"
@on-reset="onResetSecret"
/>
</div>
<div v-if="showAccessTokenInput" class="flex flex-col gap-1">
<label
v-if="type === MODAL_TYPES.EDIT"
@@ -339,6 +381,23 @@ defineExpose({ dialogRef });
/>
</div>
<div
v-if="botSecret && showAccessToken && type === MODAL_TYPES.CREATE"
class="flex flex-col gap-1"
>
<p class="text-sm text-n-slate-11">
{{ $t('AGENT_BOTS.SECRET.CREATED_DESC') }}
</p>
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ $t('AGENT_BOTS.SECRET.LABEL') }}
</label>
<AccessToken
:value="botSecret"
:show-reset-button="false"
@on-copy="onCopySecret"
/>
</div>
<div class="flex items-center justify-end w-full gap-2 px-0 py-2">
<NextButton
faded
@@ -81,7 +81,7 @@ const formData = computed(() => ({
...(selectedPolicy.value?.exclusionRules?.excludedLabels || []),
],
excludeOlderThanHours:
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours || 10,
selectedPolicy.value?.exclusionRules?.excludeOlderThanHours ?? null,
},
inboxCapacityLimits:
selectedPolicy.value?.inboxCapacityLimits?.map(limit => ({
@@ -17,7 +17,7 @@ const props = defineProps({
enabled: false,
exclusionRules: {
excludedLabels: [],
excludeOlderThanHours: 10,
excludeOlderThanHours: null,
},
inboxCapacityLimits: [],
}),
@@ -84,7 +84,7 @@ const state = reactive({
description: '',
exclusionRules: {
excludedLabels: [],
excludeOlderThanHours: 10,
excludeOlderThanHours: null,
},
inboxCapacityLimits: [],
});
@@ -120,7 +120,7 @@ const resetForm = () => {
description: '',
exclusionRules: {
excludedLabels: [],
excludeOlderThanHours: 10,
excludeOlderThanHours: null,
},
inboxCapacityLimits: [],
});
@@ -1,4 +1,5 @@
<script setup>
import { useAdmin } from 'dashboard/composables/useAdmin';
import Icon from 'next/icon/Icon.vue';
import ButtonV4 from 'next/button/Button.vue';
@@ -22,6 +23,11 @@ defineProps({
});
const emit = defineEmits(['upgrade']);
// Cloud agents land on this modal too, but billing is admin-only — they need
// the escalation message instead of a button they cannot use. Mirrors the
// pattern in UpgradePage.vue.
const { isAdmin } = useAdmin();
</script>
<template>
@@ -47,11 +53,14 @@ const emit = defineEmits(['upgrade']);
/>
<p class="text-sm font-normal text-n-slate-11">
{{ $t(`${featurePrefix}.${i18nKey}.UPGRADE_PROMPT`) }}
<span v-if="!isOnChatwootCloud && !isSuperAdmin">
<span v-if="isOnChatwootCloud && !isAdmin">
{{ $t('GENERAL_SETTINGS.LIMIT_MESSAGES.NON_ADMIN') }}
</span>
<span v-else-if="!isOnChatwootCloud && !isSuperAdmin">
{{ $t(`${featurePrefix}.ENTERPRISE_PAYWALL.ASK_ADMIN`) }}
</span>
</p>
<template v-if="isOnChatwootCloud">
<template v-if="isOnChatwootCloud && isAdmin">
<ButtonV4 blue solid md @click="emit('upgrade')">
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
</ButtonV4>
@@ -59,7 +68,7 @@ const emit = defineEmits(['upgrade']);
{{ $t(`${featurePrefix}.PAYWALL.CANCEL_ANYTIME`) }}
</span>
</template>
<template v-else-if="isSuperAdmin">
<template v-else-if="!isOnChatwootCloud && isSuperAdmin">
<a href="/super_admin" class="block w-full">
<ButtonV4 solid blue md class="w-full">
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
@@ -38,6 +38,8 @@ import Editor from 'dashboard/components-next/Editor/Editor.vue';
import ColorPicker from 'dashboard/components-next/colorpicker/ColorPicker.vue';
import SelectInput from 'dashboard/components-next/select/Select.vue';
import Widget from 'dashboard/modules/widget-preview/components/Widget.vue';
import AccessToken from 'dashboard/routes/dashboard/settings/profile/AccessToken.vue';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
export default {
components: {
@@ -69,6 +71,7 @@ export default {
SelectInput,
AccountHealth,
Widget,
AccessToken,
},
mixins: [inboxMixin],
setup() {
@@ -362,6 +365,33 @@ export default {
this.fetchSharedData();
},
methods: {
async copyWebhookSecret(value) {
await copyTextToClipboard(value);
useAlert(
this.$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_SECRET.COPY_SUCCESS'
)
);
},
async resetWebhookSecret() {
const response = await this.$store.dispatch(
'inboxes/resetSecret',
this.inbox.id
);
if (response) {
useAlert(
this.$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_SECRET.RESET_SUCCESS'
)
);
} else {
useAlert(
this.$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_SECRET.RESET_ERROR'
)
);
}
},
fetchSharedData() {
this.$store.dispatch('agents/get');
this.$store.dispatch('teams/get');
@@ -714,6 +744,21 @@ export default {
/>
</SettingsFieldSection>
<SettingsFieldSection
v-if="isAPIInbox && inbox.secret"
:label="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_SECRET.LABEL'
)
"
>
<AccessToken
:value="inbox.secret"
@on-copy="copyWebhookSecret"
@on-reset="resetWebhookSecret"
/>
</SettingsFieldSection>
<SettingsFieldSection
v-if="isAWebWidgetInbox"
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL')"
@@ -57,7 +57,10 @@ export default {
},
});
} catch (error) {
useAlert(this.$t('INBOX_MGMT.ADD.API_CHANNEL.API.ERROR_MESSAGE'));
useAlert(
error.message ||
this.$t('INBOX_MGMT.ADD.API_CHANNEL.API.ERROR_MESSAGE')
);
}
},
},
@@ -128,7 +128,7 @@ const saveMacro = async macroData => {
</script>
<template>
<div class="flex flex-col gap-6 mb-8 max-w-7xl mx-auto w-full !px-6">
<div class="flex flex-col gap-6 mb-8 max-w-7xl mx-auto h-full w-full !px-6">
<woot-loading-state
v-if="uiFlags.isFetchingItem"
:message="t('MACROS.EDITOR.LOADING')"
@@ -1,5 +1,8 @@
<script setup>
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { stripInlineBase64Images } from 'dashboard/helper/editorHelper';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
@@ -11,7 +14,9 @@ const props = defineProps({
});
const emit = defineEmits(['updateSignature']);
const signature = ref(props.messageSignature);
const { t } = useI18n();
const signature = ref(props.messageSignature ?? '');
watch(
() => props.messageSignature ?? '',
newValue => {
@@ -20,6 +25,15 @@ watch(
);
const updateSignature = () => {
const { sanitizedContent, hasInlineImages } = stripInlineBase64Images(
signature.value || ''
);
signature.value = sanitizedContent.trim();
if (hasInlineImages) {
useAlert(
t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.INLINE_IMAGE_WARNING')
);
}
emit('updateSignature', signature.value);
};
</script>