Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#9254 Add parametric image parsing to template viewer #9260

Merged
merged 13 commits into from
Jul 5, 2023
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
"leaflet.nontiledlayer": "1.0.7",
"lodash": "4.17.21",
"lrucache": "1.0.3",
"markdown-it": "13.0.1",
"md5": "2.3.0",
"moment": "2.21.0",
"node-geo-distance": "1.2.0",
Expand Down
148 changes: 75 additions & 73 deletions web/client/components/TOC/fragments/settings/FeatureInfoEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,97 +6,99 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import ReactQuill from '../../../../libs/quill/react-quill-suspense';

import Message from '../../../I18N/Message';
import Portal from '../../../misc/Portal';
import ResizableModal from '../../../misc/ResizableModal';
import CompactRichTextEditor from '../../../mapviews/settings/CompactRichTextEditor';
import withDebounceOnCallback from '../../../misc/enhancers/withDebounceOnCallback';
import { htmlToDraftJSEditorState, draftJSEditorStateToHtml } from '../../../../utils/EditorUtils';
import MarkDownImage from './MarkDownImage';

const DescriptionEditor = withDebounceOnCallback('onEditorStateChange', 'editorState')(CompactRichTextEditor);
/**
* Component for rendering FeatureInfoEditor a modal editor to modify format template
* @memberof components.TOC.fragments.settings
* @name FeatureInfoEditor
* @class
* @prop {object} element data of the current selected node
* @prop {bool} showEditor show/hide modal
* @prop {funciotn} onShowEditor called when click on close buttons
* @prop {function} onChange called when text in editor has been changed
* @prop {bool} enableIFrameModule enable iframe in editor, default true
* @prop {Object} element data of the current selected node
* @prop {Boolean} showEditor show/hide modal
* @prop {Function} onShowEditor called when click on close buttons
* @prop {Function} onChange called when text in editor has been changed
* @prop {Boolean} enableIFrameModule enable iframe in editor, default true
MV88 marked this conversation as resolved.
Show resolved Hide resolved
*/

class FeatureInfoEditor extends React.Component {
const FeatureInfoEditor = ({
element,
showEditor,
onShowEditor,
onChange,
enableIFrameModule
}) => {

static propTypes = {
showEditor: PropTypes.bool,
element: PropTypes.object,
onChange: PropTypes.func,
onShowEditor: PropTypes.func,
enableIFrameModule: PropTypes.bool,
onReady: PropTypes.func
const [editorState, setEditorState] = useState(htmlToDraftJSEditorState(element?.featureInfo?.template || ''));
const onClose = () => {
onShowEditor(!showEditor);
onChange('featureInfo', {
...(element && element.featureInfo || {}),
template: draftJSEditorStateToHtml(editorState)
});
};
return (
<Portal>
<ResizableModal
fade
show={showEditor}
title={<Message msgId="layerProperties.editCustomFormat"/>}
size="lg"
showFullscreen
clickOutEnabled={false}
onClose={onClose}
buttons={[
{
bsStyle: 'primary',
text: <Message msgId="close"/>,
onClick: onClose
}
]}>
<div id="ms-template-editor" className="ms-editor">
<DescriptionEditor
toolbarOptions={['fontFamily', 'blockType', 'inline', 'textAlign', 'list', 'link', 'colorPicker', 'remove', 'image'].concat(enableIFrameModule ? ['embedded'] : [])}
editorState={editorState}
onEditorStateChange={(newEditorState) => {
const previousHTML = draftJSEditorStateToHtml(editorState);
const newHTML = draftJSEditorStateToHtml(newEditorState);
if (newHTML !== previousHTML) {
onChange({ template: draftJSEditorStateToHtml(newEditorState) });
setEditorState(newEditorState);
}
}}
toolbarCustomButtons={[<MarkDownImage />]}
/>
</div>
</ResizableModal>
</Portal>
);

static defaultProps = {
showEditor: false,
element: {},
enableIFrameModule: false,
onChange: () => {},
onShowEditor: () => {}
};
};

state = {
template: ' '
};
FeatureInfoEditor.propTypes = {
showEditor: PropTypes.bool,
element: PropTypes.object,
onChange: PropTypes.func,
onShowEditor: PropTypes.func,
enableIFrameModule: PropTypes.bool
};

UNSAFE_componentWillMount() {
this.setState({
template: this.props.element && this.props.element.featureInfo && this.props.element.featureInfo.template || ' '
});
}
FeatureInfoEditor.defaultProps = {
showEditor: false,
element: {},
enableIFrameModule: false,
onChange: () => {},
onShowEditor: () => {}
};

render() {
const { showEditor, enableIFrameModule = true, onReady = () => {} } = this.props;
return (
<Portal>
<ResizableModal
fade
show={showEditor}
title={<Message msgId="layerProperties.editCustomFormat"/>}
size="lg"
showFullscreen
clickOutEnabled={false}
onClose={() => this.close()}
buttons={[
{
bsStyle: 'primary',
text: <Message msgId="close"/>,
onClick: () => this.close()
}
]}>
<div id="ms-template-editor" className="ms-editor">
<ReactQuill
bounds="#ms-template-editor"
ref={(quill) => { if (quill) { this.quill = quill; onReady(quill); } } }
modules={(toolbarConfig) => enableIFrameModule ? {
resizeModule: {},
toolbar: toolbarConfig
} : {}}
defaultValue={this.state.template}
onChange={template => this.setState({ template })}/>
</div>
</ResizableModal>
</Portal>
);
}

close = () => {
this.props.onShowEditor(!this.props.showEditor);
this.props.onChange('featureInfo', {
...(this.props.element && this.props.element.featureInfo || {}),
template: this.state.template
});
};
}

export default FeatureInfoEditor;
62 changes: 62 additions & 0 deletions web/client/components/TOC/fragments/settings/MarkDownImage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2023, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import PropTypes from 'prop-types';
import { EditorState, Modifier } from 'draft-js';
import {Glyphicon, Tooltip, OverlayTrigger} from 'react-bootstrap';
import Message from '../../../I18N/Message';

/**
* Component used in the react-draft-wysiwyg for wrapping the selected text with
* @param {Object} editorState state object of the draft-js lib
* @param {Function} onChange triggered to update text content change event in the editor
*/
const MarkDownImage = ({
editorState,
onChange
}) => {

const onClick = () => {

const selectionState = editorState.getSelection();
const anchorKey = selectionState.getAnchorKey();
const currentContent = editorState.getCurrentContent();
const currentContentBlock = currentContent.getBlockForKey(anchorKey);
const start = selectionState.getStartOffset();
const end = selectionState.getEndOffset();
const selectedText = currentContentBlock.getText().slice(start, end);
const contentState = Modifier.replaceText(
currentContent,
selectionState,
`![image](${selectedText})`,
editorState.getCurrentInlineStyle()
);
onChange(EditorState.push(editorState, contentState, 'insert-characters'));
};
const tooltip = (<Tooltip>
<Message msgId="layerProperties.editCustomFormat"/>
</Tooltip>);

return (
<OverlayTrigger placement="bottom" overlay={tooltip}>
<div className="rdw-image-wrapper">
<div className="rdw-option-wrapper">
<Glyphicon glyph="picture" onClick={onClick}/>
</div>
</div>
</OverlayTrigger>
);
};

export default MarkDownImage;

MarkDownImage.propTypes = {
editorState: PropTypes.object,
onChange: PropTypes.func
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import expect from 'expect';
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-dom/test-utils';

import FeatureInfoEditor from '../FeatureInfoEditor';

Expand All @@ -31,71 +30,4 @@ describe("test FeatureInfoEditor", () => {
expect(modalEditor.length).toBe(1);
});

it('test rendering close x', (done) => {

const template = '<p>html</p>';

ReactDOM.render(<FeatureInfoEditor
onReady={(quill) => {
try {
// edit template
const editor = quill.getEditor();
editor.clipboard.dangerouslyPasteHTML(template);
const btns = document.getElementsByClassName('ms-header-btn');
expect(btns.length).toBe(2);
TestUtils.Simulate.click(btns[1]);
} catch (e) {
done(e);
}
}}
onShowEditor={(value) => {
expect(value).toBe(false);
}}
onChange={(key, value) => {
if (value.template === template) {
expect(key).toBe('featureInfo');
expect(value).toEqual({ template });
done();
}
}}
showEditor/>, document.getElementById("container"));

const modalEditor = document.getElementsByClassName('ms-resizable-modal');
expect(modalEditor.length).toBe(1);

});

it('test rendering close button', (done) => {

const template = '<p>html</p>';

ReactDOM.render(<FeatureInfoEditor
onReady={(quill) => {
try {
// edit template
const editor = quill.getEditor();
editor.clipboard.dangerouslyPasteHTML(template);
const btns = document.getElementsByClassName('btn');
expect(btns.length).toBe(1);
TestUtils.Simulate.click(btns[0]);
} catch (e) {
done(e);
}
}}
onShowEditor={(value) => {
expect(value).toBe(false);
}}
onChange={(key, value) => {
if (value.template === template) {
expect(key).toBe('featureInfo');
expect(value).toEqual({ template });
done();
}
}}
showEditor
/>, document.getElementById("container"));
const modalEditor = document.getElementsByClassName('ms-resizable-modal');
expect(modalEditor.length).toBe(1);
});

});
18 changes: 15 additions & 3 deletions web/client/components/data/identify/viewers/TemplateViewer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@
*/

import React from 'react';

import MarkdownIt from 'markdown-it';
import unescape from 'lodash/unescape';
import { template } from 'lodash';
import PropTypes from 'prop-types';

import { getCleanTemplate } from '../../../../utils/TemplateUtils';
import HtmlRenderer from '../../../misc/HtmlRenderer';
import Message from '../../../I18N/Message';

export default ({layer = {}, response}) => (
const md = new MarkdownIt().enable('image');

const TemplateViewer = ({layer = {}, response}) => (
<div className="ms-template-viewer">
{response.features.map((feature, i) => {
const cleanTemplate = getCleanTemplate(layer.featureInfo && layer.featureInfo.template || '', feature, /\$\{.*?\}/g, 2, 1);
let html = "";
try {
html = template(cleanTemplate)(feature);
html = unescape(md.render(template(cleanTemplate)(feature)));
} catch (e) {
console.error(e);
return (<div key={i}>
Expand All @@ -33,3 +38,10 @@ export default ({layer = {}, response}) => (
)}
</div>
);

export default TemplateViewer;

TemplateViewer.propTypes = {
response: PropTypes.object,
layer: PropTypes.object
};
Loading