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:
@@ -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"
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -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>
|
|
||||||
{{
|
|
||||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
|
|
||||||
}}
|
|
||||||
<input
|
|
||||||
v-model="channelWelcomeTagline"
|
v-model="channelWelcomeTagline"
|
||||||
type="text"
|
:label="
|
||||||
|
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
|
||||||
|
"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
$t(
|
$t(
|
||||||
'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"
|
||||||
|
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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %>,
|
||||||
|
|||||||
Reference in New Issue
Block a user