From 3ebc1933cc81a2e75da4d50b05ef45cd718fe89e Mon Sep 17 00:00:00 2001 From: Devanshu Rastogi Date: Wed, 4 Dec 2024 14:40:57 +0530 Subject: [PATCH] Enhance: Added 'Set Value' functionality and support for HTML with hyperlinks in Rich Text Editor component (#11406) * Added HTML support for Placeholder and Default Value property. Added CSA for loading state. * Exposed isDisabled, isVisible and isLoading variables. Also exposed setValue, setDisable, setVisibility and setLoading functions. * Fixed issue where loading state functionality was not visible in the properties section on the initial render of rich text editor. --- .../Inspector/Components/DefaultComponent.jsx | 1 + .../WidgetManager/widgets/richtextarea.js | 32 ++++++++++ .../src/Editor/Components/DraftEditor.jsx | 60 +++++++++++++++++-- .../src/Editor/Components/RichTextEditor.jsx | 45 +++++++++++--- .../WidgetManager/configs/richtextarea.js | 32 ++++++++++ .../src/helpers/widget-config/richtextarea.js | 32 ++++++++++ 6 files changed, 190 insertions(+), 12 deletions(-) diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx index c183a644c0..8ab5c9356a 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx @@ -21,6 +21,7 @@ const SHOW_ADDITIONAL_ACTIONS = [ 'DropdownV2', 'MultiselectV2', 'Button', + 'RichTextEditor', ]; const PROPERTIES_VS_ACCORDION_TITLE = { Text: 'Data', diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/richtextarea.js b/frontend/src/AppBuilder/WidgetManager/widgets/richtextarea.js index eacee66529..c964d53d6b 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/richtextarea.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/richtextarea.js @@ -28,6 +28,15 @@ export const richtextareaConfig = { defaultValue: 'Default text', }, }, + loadingState: { + type: 'toggle', + displayName: 'Show loading state', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, }, events: {}, styles: { @@ -55,6 +64,28 @@ export const richtextareaConfig = { exposedVariables: { value: '', }, + actions: [ + { + handle: 'setValue', + displayName: 'Set value', + params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }], + }, + { + handle: 'setDisable', + displayName: 'Set disable', + params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }], + }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set loading', + params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }], + }, + ], definition: { others: { showOnDesktop: { value: '{{true}}' }, @@ -63,6 +94,7 @@ export const richtextareaConfig = { properties: { placeholder: { value: 'Placeholder text' }, defaultValue: { value: '' }, + loadingState: { value: `{{false}}` }, }, events: [], styles: { diff --git a/frontend/src/Editor/Components/DraftEditor.jsx b/frontend/src/Editor/Components/DraftEditor.jsx index 1df78ee1bf..b2c8b61994 100644 --- a/frontend/src/Editor/Components/DraftEditor.jsx +++ b/frontend/src/Editor/Components/DraftEditor.jsx @@ -1,8 +1,10 @@ /* eslint-disable react/no-string-refs */ import React from 'react'; -import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState } from 'draft-js'; +import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState, convertFromHTML } from 'draft-js'; import 'draft-js/dist/Draft.css'; import { stateToHTML } from 'draft-js-export-html'; +import Loader from '@/ToolJetUI/Loader/Loader'; +import DOMPurify from 'dompurify'; // Custom overrides for "code" style. const styleMap = { @@ -148,8 +150,11 @@ const InlineStyleControls = (props) => { class DraftEditor extends React.Component { constructor(props) { super(props); + const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue)); this.state = { - editorState: EditorState.createWithContent(ContentState.createFromText(this.props.defaultValue)), + editorState: EditorState.createWithContent( + ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap) + ), }; this.editorContainerRef = React.createRef(); @@ -181,6 +186,38 @@ class DraftEditor extends React.Component { if (this.controlsRef.current) { this.resizeObserver.observe(this.controlsRef.current); } + + const exposedVariables = { + value: this.props.defaultValue, + isDisabled: this.props.isDisabled, + isVisible: this.props.isVisible, + isLoading: this.props.isLoading, + setValue: async (text) => { + const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(text)); + const newContentState = ContentState.createFromBlockArray( + blocksFromHTML.contentBlocks, + blocksFromHTML.entityMap + ); + const newEditorState = EditorState.createWithContent(newContentState); + const html = stateToHTML(newContentState); + this.props.handleChange(html); + this.setState({ editorState: newEditorState }); + }, + setDisable: async (value) => { + this.props.setExposedVariable('isDisabled', value); + this.props.setIsDisabled(value); + }, + setVisibility: async (value) => { + this.props.setExposedVariable('isVisible', value); + this.props.setIsVisible(value); + }, + setLoading: async (value) => { + this.props.setExposedVariable('isLoading', value); + this.props.setIsLoading(value); + }, + }; + this.props.setExposedVariables(exposedVariables); + this.props.isInitialRender.current = false; } componentWillUnmount() { @@ -191,7 +228,8 @@ class DraftEditor extends React.Component { componentDidUpdate(prevProps) { if (prevProps.defaultValue !== this.props.defaultValue) { - const newContentState = ContentState.createFromText(this.props.defaultValue); + const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue)); + const newContentState = ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap); const newEditorState = EditorState.createWithContent(newContentState); const html = stateToHTML(newContentState); @@ -242,7 +280,13 @@ class DraftEditor extends React.Component { } } - return ( + return this.props.isLoading ? ( +
+
+ +
+
+ ) : (
@@ -256,7 +300,13 @@ class DraftEditor extends React.Component { handleKeyCommand={this.handleKeyCommand} keyBindingFn={this.mapKeyToEditorCommand} onChange={this.onChange} - placeholder={this.props.placeholder} + placeholder={ +
+ } ref="editor" spellCheck={true} /> diff --git a/frontend/src/Editor/Components/RichTextEditor.jsx b/frontend/src/Editor/Components/RichTextEditor.jsx index 4115d8deb8..2a591afba0 100644 --- a/frontend/src/Editor/Components/RichTextEditor.jsx +++ b/frontend/src/Editor/Components/RichTextEditor.jsx @@ -1,4 +1,5 @@ -import React, { useEffect } from 'react'; +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useEffect, useRef, useState } from 'react'; import 'draft-js/dist/Draft.css'; import { DraftEditor } from './DraftEditor'; @@ -8,17 +9,38 @@ export const RichTextEditor = function RichTextEditor({ properties, styles, setExposedVariable, + setExposedVariables, dataCy, }) { + const isInitialRender = useRef(true); const { visibility, disabledState, boxShadow } = styles; const placeholder = properties.placeholder; const defaultValue = properties?.defaultValue ?? ''; - // exposing the default value at first + const [isDisabled, setIsDisabled] = useState(disabledState); + const [isVisible, setIsVisible] = useState(visibility); + const [isLoading, setIsLoading] = useState(properties?.loadingState); + useEffect(() => { - setExposedVariable('value', defaultValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (isDisabled !== disabledState) setIsDisabled(disabledState); + if (isVisible !== visibility) setIsVisible(visibility); + if (isLoading !== properties.loadingState) setIsLoading(properties.loadingState); + }, [properties.loadingState, styles.visibility, styles.disabledState]); + + useEffect(() => { + if (isInitialRender.current) return; + setExposedVariable('isDisabled', disabledState); + }, [disabledState]); + + useEffect(() => { + if (isInitialRender.current) return; + setExposedVariable('isVisible', visibility); + }, [visibility]); + + useEffect(() => { + if (isInitialRender.current) return; + setExposedVariable('isLoading', isLoading); + }, [isLoading]); function handleChange(html) { setExposedVariable('value', html); @@ -26,16 +48,25 @@ export const RichTextEditor = function RichTextEditor({ return (
); diff --git a/frontend/src/Editor/WidgetManager/configs/richtextarea.js b/frontend/src/Editor/WidgetManager/configs/richtextarea.js index eacee66529..c964d53d6b 100644 --- a/frontend/src/Editor/WidgetManager/configs/richtextarea.js +++ b/frontend/src/Editor/WidgetManager/configs/richtextarea.js @@ -28,6 +28,15 @@ export const richtextareaConfig = { defaultValue: 'Default text', }, }, + loadingState: { + type: 'toggle', + displayName: 'Show loading state', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, }, events: {}, styles: { @@ -55,6 +64,28 @@ export const richtextareaConfig = { exposedVariables: { value: '', }, + actions: [ + { + handle: 'setValue', + displayName: 'Set value', + params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }], + }, + { + handle: 'setDisable', + displayName: 'Set disable', + params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }], + }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set loading', + params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }], + }, + ], definition: { others: { showOnDesktop: { value: '{{true}}' }, @@ -63,6 +94,7 @@ export const richtextareaConfig = { properties: { placeholder: { value: 'Placeholder text' }, defaultValue: { value: '' }, + loadingState: { value: `{{false}}` }, }, events: [], styles: { diff --git a/server/src/helpers/widget-config/richtextarea.js b/server/src/helpers/widget-config/richtextarea.js index eacee66529..c964d53d6b 100644 --- a/server/src/helpers/widget-config/richtextarea.js +++ b/server/src/helpers/widget-config/richtextarea.js @@ -28,6 +28,15 @@ export const richtextareaConfig = { defaultValue: 'Default text', }, }, + loadingState: { + type: 'toggle', + displayName: 'Show loading state', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + section: 'additionalActions', + }, }, events: {}, styles: { @@ -55,6 +64,28 @@ export const richtextareaConfig = { exposedVariables: { value: '', }, + actions: [ + { + handle: 'setValue', + displayName: 'Set value', + params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }], + }, + { + handle: 'setDisable', + displayName: 'Set disable', + params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }], + }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set loading', + params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }], + }, + ], definition: { others: { showOnDesktop: { value: '{{true}}' }, @@ -63,6 +94,7 @@ export const richtextareaConfig = { properties: { placeholder: { value: 'Placeholder text' }, defaultValue: { value: '' }, + loadingState: { value: `{{false}}` }, }, events: [], styles: {