diff --git a/packages/react/src/components/editor/CodeEditor.tsx b/packages/react/src/components/editor/CodeEditor.tsx index eddd54887f..cfc99e0ee7 100644 --- a/packages/react/src/components/editor/CodeEditor.tsx +++ b/packages/react/src/components/editor/CodeEditor.tsx @@ -1,13 +1,13 @@ import loadable from '@loadable/component'; import classNames from 'classnames'; import type {Editor, EditorConfiguration} from 'codemirror'; -import {ComponentType, createRef, Component} from 'react'; +import {Component, ComponentType, createRef} from 'react'; import type {Controlled} from 'react-codemirror2'; import {connect} from 'react-redux'; import {PlasmaState} from '../../PlasmaState'; import {IDispatch} from '../../utils'; -import {CollapsibleSelectors} from '../collapsible/CollapsibleSelectors'; +import {CollapsibleSelectors} from '../collapsible'; import {CodeEditorActions} from './CodeEditorActions'; import {CodeMirrorGutters} from './EditorConstants'; @@ -81,7 +81,7 @@ class CodeEditorDisconnect extends Component< > { static defaultProps: Partial = { className: 'mod-border', - value: '{}', + value: '', }; static defaultOptions = { @@ -119,9 +119,13 @@ class CodeEditorDisconnect extends Component< this.editor.refresh(); this.setState({numberOfRefresh: this.state.numberOfRefresh + 1}); } - if (prevProps.value !== this.props.value && this.editor) { + + if (prevProps.value !== this.props.value) { this.setState({value: this.props.value}); - this.editor.getDoc().clearHistory(); + + if (this.editor) { + this.editor.getDoc().clearHistory(); + } } } diff --git a/packages/react/src/components/editor/JSONEditor.tsx b/packages/react/src/components/editor/JSONEditor.tsx index cbbd48d671..14c1a5c69f 100644 --- a/packages/react/src/components/editor/JSONEditor.tsx +++ b/packages/react/src/components/editor/JSONEditor.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; -import {FunctionComponent, useState, useEffect} from 'react'; +import {FunctionComponent, useEffect, useState} from 'react'; -import {Svg} from '../svg/Svg'; +import {Svg} from '../svg'; import {CodeEditor} from './CodeEditor'; import {CodeMirrorModes, DEFAULT_JSON_ERROR_MESSAGE} from './EditorConstants'; import {JSONEditorUtils} from './JSONEditorUtils'; @@ -12,11 +12,11 @@ export interface JSONEditorProps { */ id?: string; /** - * @deprecated use defaultValue instead + * The text value of the JSON editor */ value?: string; /** - * The initial value + * @deprecated use value instead */ defaultValue?: string; /** @@ -65,8 +65,8 @@ export interface JSONEditorDispatchProps { export const JSONEditor: FunctionComponent< JSONEditorProps & Partial & Partial > = ({ - value, defaultValue, + value, readOnly, onChange, errorMessage, @@ -77,7 +77,8 @@ export const JSONEditor: FunctionComponent< onUnmount, collapsibleId, }) => { - const [isInError, setIsInError] = useState(!JSONEditorUtils.validateValue(value || defaultValue)); + const editorValue = value || defaultValue || '{}'; + const [isInError, setIsInError] = useState(!JSONEditorUtils.validateValue(editorValue)); useEffect(() => { onMount?.(); @@ -85,17 +86,26 @@ export const JSONEditor: FunctionComponent< return onUnmount; }, []); + const validate = (val: string) => { + const hasError = !JSONEditorUtils.validateValue(val); + setIsInError(hasError); + return hasError; + }; + + useEffect(() => { + validate(editorValue); + }, [editorValue]); + const handleChange = (json: string) => { - const hasError = !JSONEditorUtils.validateValue(json); + const isValid = validate(json); - setIsInError(hasError); - onChange?.(json, hasError); + return onChange?.(json, isValid); }; return (
({ - value: JSONEditorSelectors.getValue(state, ownProps.id), -}); +interface JSONEditorConnectedProps { + /** + * initial value of the component + */ + defaultValue?: string; +} -const mapDispatchToProps = (dispatch: IDispatch, ownProps: JSONEditorProps): JSONEditorDispatchProps => ({ - onMount: () => dispatch(JSONEditorActions.addJSONEditor(ownProps.id, ownProps.defaultValue ?? ownProps.value)), - onUnmount: () => dispatch(JSONEditorActions.removeJSONEditor(ownProps.id)), - onChange: (value: string, inError: boolean) => { - dispatch(JSONEditorActions.updateJSONEditorValue(ownProps.id, value)); - ownProps.onChange?.(value, inError); - }, -}); +export const JSONEditorConnected = (props: JSONEditorProps & JSONEditorConnectedProps) => { + const dispatch: IDispatch = useDispatch(); + const value = useSelector((state) => JSONEditorSelectors.getValue(state, props.id)); -export const JSONEditorConnected = connect(mapStateToProps, mapDispatchToProps)(JSONEditor); + useEffect(() => { + dispatch(JSONEditorActions.addJSONEditor(props.id, props.defaultValue || props.value)); + + return () => { + dispatch(JSONEditorActions.removeJSONEditor(props.id)); + }; + }, []); + + const onChange = (changedValue: string, inError: boolean) => { + dispatch(JSONEditorActions.updateJSONEditorValue(props.id, changedValue)); + props.onChange?.(changedValue, inError); + }; + + return ; +}; diff --git a/packages/react/src/components/editor/tests/JSONEditor.spec.tsx b/packages/react/src/components/editor/tests/JSONEditor.spec.tsx index a6778f9f4e..6925d8e1f4 100644 --- a/packages/react/src/components/editor/tests/JSONEditor.spec.tsx +++ b/packages/react/src/components/editor/tests/JSONEditor.spec.tsx @@ -1,5 +1,4 @@ import {shallow, ShallowWrapper} from 'enzyme'; -import * as _ from 'underscore'; import {CodeEditor} from '../CodeEditor'; import {CodeMirrorModes} from '../EditorConstants'; diff --git a/packages/react/src/components/editor/tests/JSONEditorConnected.spec.tsx b/packages/react/src/components/editor/tests/JSONEditorConnected.spec.tsx index a058d96693..362381285c 100644 --- a/packages/react/src/components/editor/tests/JSONEditorConnected.spec.tsx +++ b/packages/react/src/components/editor/tests/JSONEditorConnected.spec.tsx @@ -11,6 +11,28 @@ describe('', () => { }).not.toThrow(); }); + it('will display brackets if no value/defaultValue is provided as it is a JSON editor', async () => { + const {container} = render(); + + await waitFor(() => expect(screen.getByRole('textbox')).toBeVisible()); + + // eslint-disable-next-line testing-library/no-node-access,testing-library/no-container + const line = container.querySelector('.CodeMirror-line [role="presentation"]') as HTMLElement; + + // the codemirror divide the text in multiple elements, by using textContent we "strip" the html + const matcher = (_: string, element: HTMLElement) => element?.textContent === '{}'; + + expect(within(line).getByText(matcher)).toBeVisible(); + }); + + it('will not display error when rendering with (deprecated) value prop', async() => { + render(); + + await waitFor(() => expect(screen.getByRole('textbox')).toBeVisible()); + + expect(screen.queryByText(/JSON configuration is syntactically invalid/i)).not.toBeInTheDocument(); + }); + it('should not throw when content changes', async () => { render(); @@ -49,10 +71,10 @@ describe('', () => { userEvent.type(screen.getByRole('textbox'), expectedValue); expect(onChangeSpy).toHaveBeenCalledTimes(5); - expect(onChangeSpy).toHaveBeenCalledWith('h', true); - expect(onChangeSpy).toHaveBeenCalledWith('he', true); - expect(onChangeSpy).toHaveBeenCalledWith('hel', true); - expect(onChangeSpy).toHaveBeenCalledWith('hell', true); - expect(onChangeSpy).toHaveBeenCalledWith('hello', true); + expect(onChangeSpy).toHaveBeenCalledWith('{}h', true); + expect(onChangeSpy).toHaveBeenCalledWith('{}he', true); + expect(onChangeSpy).toHaveBeenCalledWith('{}hel', true); + expect(onChangeSpy).toHaveBeenCalledWith('{}hell', true); + expect(onChangeSpy).toHaveBeenCalledWith('{}hello', true); }); });