Commit a0ba1195 authored by Maxime Chaillet's avatar Maxime Chaillet

clean the code. It fixes #149.

parent edb08063
This diff is collapsed.
This diff is collapsed.
import React from 'react'
import PropTypes from 'prop-types'
import HTMLEditor from './HTMLEditor';
import { getContent } from '../../helpers/EventHelpers';
/**
* This component takes care about displaying the provided content
* using a rich text editor or not
* in edition mode or in view mode
*
*/
class EventContentDisplayer extends React.Component {
render() {
let { isEditionMode, user, content, useRichTextEditor, investigationId, canEnableSaveButton } = this.props;
if (content && content instanceof Array && content.length !== 0) {
if (useRichTextEditor !== null && useRichTextEditor !== undefined) {
let HTMLText = this.getContent(content); // can be null
if (useRichTextEditor === true) {
// save the event plainText format to localstorage. This is usefull in case the editor is not changed (when only title is updated)
localStorage.setItem('plainText', getContent(content, 'plainText'));
return (<HTMLEditor
user={user}
text={HTMLText}
isEditionMode={isEditionMode}
investigationId={investigationId}
canEnableSaveButton={canEnableSaveButton}
/>)
} else if (useRichTextEditor === false) {
return (<div dangerouslySetInnerHTML={{ __html: HTMLText }} />)
}
}
}
return null
}
/**
* Gets the content of the event as an html string.
* @param {array} the event content, not null
* @returns {string} the html formatted content if it exists in the content. The plaintext content surrounded by <p> </p> if it does not exists. Null if none of these exist.
*/
getContent(content) {
let HTMLText = getContent(content, 'html');
if (HTMLText) {
return HTMLText;
} else {
let plainText = getContent(content, 'plainText');
if (plainText) {
return "<p>" + plainText + "</p>"
}
}
return null;
}
}
EventContentDisplayer.propTypes = {
/* the event content which will be displayed. An array containing different usable formats of the content */
content: PropTypes.array.isRequired,
/* whether a rich text editor should be used to view the content or not */
useRichTextEditor: PropTypes.bool.isRequired,
/* true when the display prupose is edition or creation of a new event. False when the display purpose is viewing an existing event content. */
isEditionMode: PropTypes.bool.isRequired,
/* the user currently logged in */
user: PropTypes.object,
/** the investigationId of the event being edited. */
investigationId: PropTypes.string,
/** callback function called when editor content changed : from no text to text or vice versa, or when the current text is identical to the original text provided to the editor*/
canEnableSaveButton: PropTypes.func,
}
export default EventContentDisplayer;
\ No newline at end of file
......@@ -41,7 +41,6 @@ class EventListMenu extends React.Component {
}
}
//let defaultSortingOrder = (GUI_CONFIG().DEFAULT_SORTING_FILTER) ? GUI_CONFIG().DEFAULT_SORTING_FILTER.createdAt : 1;
return (
<Navbar
fluid
......@@ -171,4 +170,4 @@ EventListMenu.propTypes = {
sortingFilter: PropTypes.object.isRequired
};
export default EventListMenu;
export default EventListMenu;
\ No newline at end of file
import React, { Component } from 'react';
import _ from 'lodash'
import axios from 'axios';
import PropTypes from 'prop-types'
import { uploadFile } from '../../api/icat/icatPlus.js'
// tinymce editor
import { Editor } from '@tinymce/tinymce-react'
import tinymce from 'tinymce/tinymce';
import { EditionModeConfig, ViewModeConfig } from '../../config/tinymce/tinymce.js'
import { GUI_CONFIG } from '../../config/gui.config.js';
import { getFileByEventId } from "../../api/icat/icatPlus"
/**
* The HTML editor used to read and write the logbook's annotations.
*/
class HTMLEditor extends Component {
constructor(props) {
super(props);
this.originalText = props.text; // Stores original text provided in props, could be undefined
this.state = {
editorContent: this.originalText
}
this.imagesUploadHandler = this.imagesUploadHandler.bind(this);
this.onEditorChange = this.onEditorChange.bind(this);
this.storeToLocalStorage = this.storeToLocalStorage.bind(this);
this.setImageHeightByCSSRule = this.setImageHeightByCSSRule.bind(this);
this.onImageLoaded = this.onImageLoaded.bind(this);
}
render() {
let isEditionMode = this.props.isEditionMode;
const { editorContent } = this.state;
const config = (isEditionMode === true) ? new EditionModeConfig() : new ViewModeConfig();
return (
<div >
<Editor
init={{
plugins: config.plugins,
skin_url: config.skin_url,
branding: config.branding,
readonly: config.readonly,
toolbar: config.toolbar,
menubar: config.menubar,
statusbar: config.statusbar,
images_upload_handler: this.imagesUploadHandler,
paste_data_images: config.paste_data_images,
autoresize_min_height: config.autoresize_min_height,
autoresize_max_height: config.autoresize_max_height,
formats: config.formats,
content_css: config.content_css,
}}
value={editorContent}
onEditorChange={this.onEditorChange}
/>
</div>
);
}
componentDidMount() {
this.storeToLocalStorage()
}
/**
* Callback function triggered when the image has been downloaded
* @param {*} event the event
*/
onImageLoaded(event) {
let element = tinymce.activeEditor.selection.select(event.target);
tinymce.activeEditor.dom.setStyles(element, { 'max-width': '100%', 'height': 'auto' });
}
/**
* Sets a default image height using a CSS rule. This is a trick to make sure the image height
* is applied in the editor (for scrollabar propose) especially because the image is not necessary
* yet downloaded at this step. The height is changed to auto after the image is fully downloaded
* such that image ratio is kept.
*/
setImageHeightByCSSRule() {
if (tinymce && tinymce.activeEditor) {
let selectedNode = tinymce.activeEditor.selection.getNode();
if (selectedNode.nodeName === 'IMG' && selectedNode.style.height !== 'auto') {
let nxElement = selectedNode;
nxElement.style.height = GUI_CONFIG().NEW_EVENT_MAX_HEIGHT;
nxElement.style.width = 'auto'; // a css trick for some browsers IE8 and old iceweasel
nxElement.onload = this.onImageLoaded;
tinymce.activeEditor.dom.replace(nxElement, selectedNode);
}
}
}
/**
* The function executed when the editor state changes (mouse click, key press for example )
* @param {string} editorContent the editor content in html format
*/
onEditorChange(editorContent) {
this.setImageHeightByCSSRule();
// Inform parent component that the current text equals the original text as provided in the props
if (this.props.canEnableSaveButton) {
let hasText = editorContent.length !== 0 ? true : false;
let isCurrentTextEqualsOriginal;
if (this.originalText) {
isCurrentTextEqualsOriginal = (_.isEqual(editorContent, this.originalText)) ? true : false;
}
this.props.canEnableSaveButton({ hasText: hasText, currentTextEqualsOriginal: isCurrentTextEqualsOriginal });
}
this.setState({ editorContent: editorContent });
this.storeToLocalStorage(editorContent);
}
/**
* Defines what to do when the user drag an image onto the dropzone of the editor image plugin. This function must return a promise.
* The value of a fullfilled promise must be an array of the form { data: { link: url } } where url value is the link to the image which
* has just been uoloaded to the ICAT+ server.
* @param {*} file : the image which has just been dropped on the drop zone.
*/
imagesUploadHandler(blobInfo, success, failure) {
let { investigationId } = this.props;
var sessionId = this.props.user.sessionId;
let data = new FormData();
data.append('file', blobInfo.blob(), blobInfo.filename());
data.append('investigationId', investigationId);
data.append('creationDate', Date());
data.append('type', 'attachment');
data.append('category', 'file');
data.append('username', this.props.user.username);
axios({
method: "post",
url: uploadFile(sessionId, investigationId),
data: data,
})
.then(function (value) {
let eventId = value.data._id;
success(getFileByEventId(sessionId, investigationId, eventId));
}, function (error) {
console.log("[ERROR] Retrieval of the image you have just upladed into the editor failed ! ");
failure(error);
});
}
/**
* Store the editor content to localStorage.
* @param {*} editorContent the editor content in HTML format
*/
storeToLocalStorage(editorContent) {
if (editorContent) {
// Editor content has been modified by the user. Save the update to localStorage
if (tinymce && tinymce.activeEditor) {
// Save the plain text format to localstorage
localStorage.setItem('plainText', tinymce.activeEditor.getContent({ format: 'text' }));
}
// Save the HTML format to localstorage
localStorage.setItem('HTMLText', editorContent);
}
else {
// The following is executed on componentDidMount. Usefull when the event title is changed only while the editor content was not modified
localStorage.setItem('HTMLText', this.originalText);
}
}
}
HTMLEditor.defaultProps = {
/** by default, the editor is not in editing mode */
isEditionMode: false
};
HTMLEditor.propTypes = {
/** Determines whether the editor is in editing mode or not. */
isEditionMode: PropTypes.bool,
/** The text provided to the editor. No text indicates that HTMLEditor is begin used for the creation of a new event. */
text: PropTypes.string,
/** the investigationId of the event being edited. */
investigationId: PropTypes.string,
/** the user who is currently logged in */
user: PropTypes.object.isRequired,
/** callback function called when editor content changed : from no text to text or vice versa, or when the current text is identical to the original text provided to the editor*/
canEnableSaveButton: PropTypes.func,
}
export default HTMLEditor;
//React components
import React from 'react'
import PropTypes from 'prop-types';
import { Grid, Row, Col, FormControl, Panel, Collapse, Label, InputGroup } from "react-bootstrap"
import TagContainer from '../../containers/TagContainer'
import HTMLEditor from './HTMLEditor'
import EventHeader from './EventHeader'
import EventFooter from './EventFooter'
import { NEW_EVENT_INVISIBLE, ANNOTATION, NEW_EVENT_CONTEXT, EVENT_CATEGORY_COMMENT } from '../../constants/EventTypes';
/* the possible status of the component */
const IDLE_STATUS = "idle";
//const UPLOADING_STATUS = "uploading"; // the data was successfully uploaded to the server
const UPLOADED_STATUS = "uploaded"; // the data is being uploaded
/* The class represents a new event which is being created */
class NewEvent extends React.Component {
constructor(props) {
super(props)
this.state = {
hasText: false,
status: IDLE_STATUS,
}
this.changeStateToUploaded = this.changeStateToUploaded.bind(this);
this.createEvent = this.createEvent.bind(this);
this.canEnableSaveButton = this.canEnableSaveButton.bind(this);
this.onCancelNewEventClicked = this.onCancelNewEventClicked.bind(this);
this.setTagContainer = this.setTagContainer.bind(this);
}
render() {
const { investigationId, isVisible, user, onNewEventUploaded } = this.props;
if (this.state.status === UPLOADED_STATUS) {
this.setState({ status: IDLE_STATUS });
onNewEventUploaded()
return null;
} else {
return (
<Collapse in={isVisible}>
<Panel bsStyle='primary' >
<EventHeader context={NEW_EVENT_CONTEXT} />
< div id="newEventBox" style={{ display: 'flex' }} >
{/* the left panel */}
< div style={{ flexGrow: '0', maxWidth: '155px' }}>
<div style={{ paddingLeft: '5px' }} >
<div style={{ height: '50px' }} />
<a href={"/investigation/" + this.props.investigationId + "/events/tagManager"} target="_blank" style={{ float: 'right', paddingRight: '10px' }}> manage </a>
<Label> Tags </Label>
<div style={{ paddingTop: '5px', marginRight: '10px' }}>
<TagContainer
canEnableSaveButton={this.canEnableSaveButton}
context={NEW_EVENT_CONTEXT}
investigationId={this.props.investigationId}
setTagContainer={this.setTagContainer}
/>
</div>
</div>
</div >
<div
style={{ flexGrow: '1' }}>
<div>
<HTMLEditor
user={user}
investigationId={investigationId}
isEditionMode={true}
canEnableSaveButton={this.canEnableSaveButton} />
<div style={{ paddingTop: '8px' }}>
<InputGroup>
<InputGroup.Addon style={{ backgroundColor: 'transparent', border: 'none' }}> <Label> Title </Label> </InputGroup.Addon>
<FormControl
type="text"
value={this.state.inputTitleValue}
onChange={this.onChangeInputValue}
placeholder="Optional title here"
inputRef={(FormControl) => this.inputTitle = FormControl} />
</InputGroup>
</div>
</div>
<EventFooter
isSaveButtonEnabled={this.state.hasText}
onCancelButtonClicked={this.onCancelNewEventClicked}
onSaveButtonClicked={this.createEvent} />
</div>
</div >
</Panel>
</Collapse>
)
}
}
changeStateToUploaded() {
this.setState({ status: UPLOADED_STATUS })
}
/** Fuction triggered when the user clicks the 'cancel' button */
onCancelNewEventClicked() {
localStorage.removeItem('plainText');
localStorage.removeItem('HTMLText');
this.props.setNewEventVisibility(NEW_EVENT_INVISIBLE);
}
/**
* Callback method called when the editor content changed from empty to not empty and vice versa.
* @param {*} change the object representing the change
*/
canEnableSaveButton(change) {
if (change) {
if ("hasText" in change) {
this.setState({ hasText: change.hasText })
}
}
}
/**
* Create the new event
*/
createEvent() {
let investigationId = this.props.investigationId;
let currentTagIds = this.tagContainer.state.selectedTags.map((tag) => tag._id);
let newEvent = {
category: EVENT_CATEGORY_COMMENT,
content: [
{
format: "plainText",
text: localStorage.getItem('plainText')
},
{
format: "html",
text: localStorage.getItem('HTMLText')
}
],
creationDate: Date(),
investigationId: investigationId,
title: this.inputTitle.value,
tag: currentTagIds,
type: ANNOTATION,
username: this.props.user.username,
}
this.props.createEvent(newEvent, this.props.user.sessionId, this.props.investigationId, this.changeStateToUploaded)
}
setTagContainer(element) {
this.tagContainer = element
}
}
NewEvent.propTypes = {
/** the investigationId indicating what investigation the new event will belong to*/
investigationId: PropTypes.string.isRequired,
/** whether this component is visible (ie panel is expanded) or not */
isVisible: PropTypes.bool.isRequired,
/** Callback function to reload events from the server */
reloadEvents: PropTypes.func,
// /** the callback function to change the visibility of this component */
setNewEventVisibility: PropTypes.func.isRequired,
/** the user who is currently logged in */
user: PropTypes.object.isRequired,
}
export default NewEvent;
\ No newline at end of file
......@@ -141,9 +141,8 @@ class InvestigationContainer extends Component {
return (<Grid fluid>
<Tab.Container id="tabs" activeKey={this.state.activeTab} onSelect={this.handleSelect}>
<Row >
{/* <Row className="clearfix" style={{height:'3000px'}}> */}
<Col sm={12}>
<TabContainerMenu isLogBookTabDisplayed={!this.props.isDOI} datasetCount={this.props.datasets.data.length} />
<TabContainerMenu isLogBookTabDisplayed={!this.props.isDOI} datasetCount={this.props.datasets.data.length} />
<Tab.Content animation>
<Tab.Pane eventKey={1} mountOnEnter={true}>
<Loader show={this.props.datasets.fetching}>
......@@ -175,25 +174,22 @@ function TabContainerMenu(props) {
logbookClassName = "hidden";
}
return (
// <Col sm={12}>
<Nav bsStyle="tabs">
<NavItem eventKey={1}>
<div>
<Glyphicon glyph="list" />
<span style={{ marginLeft: "2px" }}> Dataset List &nbsp;</span>
<Badge bsClass="ourBadges-m"> {props.datasetCount} </Badge>
</div>
</NavItem>
<NavItem className={logbookClassName} eventKey={3} >
<div>
<Glyphicon glyph="comment" />
<span style={{ marginLeft: "2px" }}> Logbook </span>
</div>
</NavItem>
</Nav>
// </Col>
<Nav bsStyle="tabs">
<NavItem eventKey={1}>
<div>
<Glyphicon glyph="list" />
<span style={{ marginLeft: "2px" }}> Dataset List &nbsp;</span>
<Badge bsClass="ourBadges-m"> {props.datasetCount} </Badge>
</div>
</NavItem>
<NavItem className={logbookClassName} eventKey={3} >
<div>
<Glyphicon glyph="comment" />
<span style={{ marginLeft: "2px" }}> Logbook </span>
</div>
</NavItem>
</Nav>
)
}
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment