feat: Add rich text support for widget welcome tagline (#11666)

# Pull Request Template

## Description

This PR adds rich text support for the widget welcome tagline, enabling
users to format the message using basic text styling (bold, italics,
links, etc.)
https://github.com/chatwoot/chatwoot/pull/11629#issuecomment-2932448975

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshots
<img width="1100" alt="image"
src="https://github.com/user-attachments/assets/eef2bfd3-7bc9-4aea-b21d-078f6b29f334"
/>
<img width="448" alt="image"
src="https://github.com/user-attachments/assets/713c6b07-d0dc-4c8f-bfba-cd119a858375"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Sivin Varghese
2025-06-06 00:43:46 +05:30
committed by GitHub
parent 2f40f95f77
commit 4c0d096e4d
8 changed files with 67 additions and 58 deletions

View File

@@ -4,38 +4,14 @@ import { computed, ref, watch, useSlots } from 'vue';
import WootEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; import WootEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: { type: String, default: '' },
type: String, label: { type: String, default: '' },
default: '', placeholder: { type: String, default: '' },
}, focusOnMount: { type: Boolean, default: false },
label: { maxLength: { type: Number, default: 200 },
type: String, showCharacterCount: { type: Boolean, default: true },
default: '', disabled: { type: Boolean, default: false },
}, message: { type: String, default: '' },
placeholder: {
type: String,
default: '',
},
focusOnMount: {
type: Boolean,
default: false,
},
maxLength: {
type: Number,
default: 200,
},
showCharacterCount: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
message: {
type: String,
default: '',
},
messageType: { messageType: {
type: String, type: String,
default: 'info', default: 'info',
@@ -43,6 +19,7 @@ const props = defineProps({
}, },
enableVariables: { type: Boolean, default: false }, enableVariables: { type: Boolean, default: false },
enableCannedResponses: { type: Boolean, default: true }, enableCannedResponses: { type: Boolean, default: true },
enabledMenuOptions: { type: Array, default: () => [] },
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@@ -120,6 +97,7 @@ watch(
:disabled="disabled" :disabled="disabled"
:enable-variables="enableVariables" :enable-variables="enableVariables"
:enable-canned-responses="enableCannedResponses" :enable-canned-responses="enableCannedResponses"
:enabled-menu-options="enabledMenuOptions"
@input="handleInput" @input="handleInput"
@focus="handleFocus" @focus="handleFocus"
@blur="handleBlur" @blur="handleBlur"

View File

@@ -33,6 +33,14 @@ export const ARTICLE_EDITOR_MENU_OPTIONS = [
'code', 'code',
]; ];
export const WIDGET_BUILDER_EDITOR_MENU_OPTIONS = [
'strong',
'em',
'link',
'undo',
'redo',
];
export const MESSAGE_EDITOR_IMAGE_RESIZES = [ export const MESSAGE_EDITOR_IMAGE_RESIZES = [
{ {
name: 'Small', name: 'Small',

View File

@@ -1,5 +1,6 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
const props = defineProps({ const props = defineProps({
config: { config: {
@@ -8,6 +9,8 @@ const props = defineProps({
}, },
}); });
const { formatMessage } = useMessageFormatter();
const isDefaultScreen = computed(() => { const isDefaultScreen = computed(() => {
return ( return (
props.config.isDefaultScreen && props.config.isDefaultScreen &&
@@ -53,12 +56,13 @@ const isDefaultScreen = computed(() => {
</div> </div>
</div> </div>
<div v-if="isDefaultScreen" class="overflow-auto max-h-60"> <div v-if="isDefaultScreen" class="overflow-auto max-h-60">
<h2 class="mb-2 text-2xl break-words text-slate-900 dark:text-white"> <h2 class="mb-2 text-2xl break-words text-n-slate-12">
{{ config.welcomeHeading }} {{ config.welcomeHeading }}
</h2> </h2>
<p class="text-sm break-words text-slate-600 dark:text-slate-100"> <p
{{ config.welcomeTagline }} v-dompurify-html="formatMessage(config.welcomeTagline)"
</p> class="text-sm break-words text-n-slate-11 [&_a]:!text-n-slate-11 [&_a]:underline"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -23,6 +23,8 @@ import { FEATURE_FLAGS } from '../../../../featureFlags';
import SenderNameExamplePreview from './components/SenderNameExamplePreview.vue'; import SenderNameExamplePreview from './components/SenderNameExamplePreview.vue';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import { INBOX_TYPES } from 'dashboard/helper/inbox'; import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
export default { export default {
components: { components: {
@@ -43,6 +45,7 @@ export default {
NextButton, NextButton,
InstagramReauthorize, InstagramReauthorize,
DuplicateInboxBanner, DuplicateInboxBanner,
Editor,
}, },
mixins: [inboxMixin], mixins: [inboxMixin],
setup() { setup() {
@@ -70,6 +73,7 @@ export default {
selectedTabIndex: 0, selectedTabIndex: 0,
selectedPortalSlug: '', selectedPortalSlug: '',
showBusinessNameInput: false, showBusinessNameInput: false,
welcomeTaglineEditorMenuOptions: WIDGET_BUILDER_EDITOR_MENU_OPTIONS,
}; };
}, },
computed: { computed: {
@@ -480,10 +484,10 @@ export default {
" "
/> />
<woot-input <Editor
v-if="isAWebWidgetInbox" v-if="isAWebWidgetInbox"
v-model="channelWelcomeTagline" v-model="channelWelcomeTagline"
class="pb-4" class="mb-4"
:label=" :label="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL') $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
" "
@@ -492,6 +496,8 @@ export default {
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER' 'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
) )
" "
:max-length="255"
:enabled-menu-options="welcomeTaglineEditorMenuOptions"
/> />
<label v-if="isAWebWidgetInbox" class="pb-4"> <label v-if="isAWebWidgetInbox" class="pb-4">

View File

@@ -7,13 +7,16 @@ import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators'; import { required } from '@vuelidate/validators';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
export default { export default {
components: { components: {
Widget, Widget,
InputRadioGroup, InputRadioGroup,
NextButton, NextButton,
Editor,
}, },
props: { props: {
inbox: { inbox: {
@@ -71,6 +74,7 @@ export default {
checked: false, checked: false,
}, },
], ],
welcomeTaglineEditorMenuOptions: WIDGET_BUILDER_EDITOR_MENU_OPTIONS,
}; };
}, },
computed: { computed: {
@@ -310,7 +314,7 @@ export default {
) )
" "
/> />
<woot-input <Editor
v-model="welcomeTagline" v-model="welcomeTagline"
:label=" :label="
$t( $t(
@@ -322,6 +326,9 @@ export default {
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WELCOME_TAGLINE.PLACE_HOLDER' 'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WELCOME_TAGLINE.PLACE_HOLDER'
) )
" "
:max-length="255"
:enabled-menu-options="welcomeTaglineEditorMenuOptions"
class="mb-4"
/> />
<label> <label>
{{ {{

View File

@@ -5,12 +5,15 @@ import router from '../../../../index';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import PageHeader from '../../SettingsSubPageHeader.vue'; import PageHeader from '../../SettingsSubPageHeader.vue';
import GreetingsEditor from 'shared/components/GreetingsEditor.vue'; import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
export default { export default {
components: { components: {
PageHeader, PageHeader,
GreetingsEditor, GreetingsEditor,
NextButton, NextButton,
Editor,
}, },
data() { data() {
return { return {
@@ -21,6 +24,7 @@ export default {
channelWelcomeTagline: '', channelWelcomeTagline: '',
greetingEnabled: false, greetingEnabled: false,
greetingMessage: '', greetingMessage: '',
welcomeTaglineEditorMenuOptions: WIDGET_BUILDER_EDITOR_MENU_OPTIONS,
}; };
}, },
computed: { computed: {
@@ -134,22 +138,21 @@ export default {
/> />
</label> </label>
</div> </div>
<div class="w-full"> <Editor
<label> v-model="channelWelcomeTagline"
{{ :label="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL') $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
}} "
<input :placeholder="
v-model="channelWelcomeTagline" $t(
type="text" 'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
:placeholder=" )
$t( "
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER' :max-length="255"
) :enabled-menu-options="welcomeTaglineEditorMenuOptions"
" class="mb-4"
/> />
</label>
</div>
<label class="w-full"> <label class="w-full">
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_GREETING_TOGGLE.LABEL') }} {{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_GREETING_TOGGLE.LABEL') }}
<select v-model="greetingEnabled"> <select v-model="greetingEnabled">

View File

@@ -1,6 +1,7 @@
<script setup> <script setup>
import HeaderActions from './HeaderActions.vue'; import HeaderActions from './HeaderActions.vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
const props = defineProps({ const props = defineProps({
avatarUrl: { avatarUrl: {
@@ -21,6 +22,8 @@ const props = defineProps({
}, },
}); });
const { formatMessage } = useMessageFormatter();
const containerClasses = computed(() => [ const containerClasses = computed(() => [
props.avatarUrl ? 'justify-between' : 'justify-end', props.avatarUrl ? 'justify-between' : 'justify-end',
]); ]);
@@ -47,8 +50,8 @@ const containerClasses = computed(() => [
class="mt-4 text-2xl mb-1.5 font-medium text-n-slate-12" class="mt-4 text-2xl mb-1.5 font-medium text-n-slate-12"
/> />
<p <p
v-dompurify-html="introBody" v-dompurify-html="formatMessage(introBody)"
class="text-lg leading-normal text-n-slate-11" class="text-lg leading-normal text-n-slate-11 [&_a]:underline"
/> />
</header> </header>
</template> </template>

View File

@@ -11,7 +11,7 @@
locale: '<%= @web_widget.account.locale %>', locale: '<%= @web_widget.account.locale %>',
websiteName: '<%= @web_widget.inbox.name %>', websiteName: '<%= @web_widget.inbox.name %>',
websiteToken: '<%= @web_widget.website_token %>', websiteToken: '<%= @web_widget.website_token %>',
welcomeTagline: '<%= @web_widget.welcome_tagline %>', welcomeTagline: <%= @web_widget.welcome_tagline.to_json.html_safe %>,
welcomeTitle: '<%= @web_widget.welcome_title %>', welcomeTitle: '<%= @web_widget.welcome_title %>',
widgetColor: '<%= @web_widget.widget_color %>', widgetColor: '<%= @web_widget.widget_color %>',
portal: <%= @web_widget.inbox.portal.to_json.html_safe %>, portal: <%= @web_widget.inbox.portal.to_json.html_safe %>,