Commit c2f8df4e authored by Maxime Chaillet's avatar Maxime Chaillet

Merge branch 'issue149' into 'master'

Issue149

Closes #149

See merge request !149
parents 86b376a3 750176ef
Pipeline #11497 passed with stages
in 6 minutes and 30 seconds
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
import React from 'react'
import { Button, Glyphicon, Tooltip, OverlayTrigger } from 'react-bootstrap'
import PropTypes from 'prop-types';
/* A React component which renders a button in the event list menu. It does not handle event associated to the button.*/
class EventListMenuButton extends React.Component {
render() {
let { isEnabled, text, glyph, tooltipText } = this.props;
if (isEnabled === false) {
return (<Button bsSize="small" bsStyle="primary" disabled > <Glyphicon glyph={glyph} />{text} </Button>);
}
// default behavior
return (<OverlayTrigger placement="bottom" overlay={<Tooltip id="tooltip"> {tooltipText} </Tooltip>}>
<Button bsSize="small" bsStyle="primary" disabled={false} > <Glyphicon glyph={glyph} /> {text} </Button>
</OverlayTrigger>
)
}
}
export default EventListMenuButton;
EventListMenuButton.propTypes = {
/** the text displayed in the button */
text: PropTypes.string.isRequired,
/** glyphicon added on the left side of the displayed text */
glyph: PropTypes.string.isRequired,
/** text of the tooltip when the user hover the mouse over the button */
tooltipText: PropTypes.string,
/** Whether the button is enabled or disabled */
isEnabled: PropTypes.bool.isRequired
}
\ 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;
...@@ -115,7 +115,7 @@ class Event extends React.Component { ...@@ -115,7 +115,7 @@ class Event extends React.Component {
</td> </td>
<td classNmae='eventActionBox' style={{ width: '50px', border: 0 }}> <td classNmae='eventActionBox' style={{ width: '50px', border: 0 }}>
<Button bsStyle="link" bsSize="small" style={{ padding: '0px' }} onClick={() => this.props.onEventClicked(event)}> <Button bsStyle="link" bsSize="small" style={{ padding: '0px' }} onClick={() => this.props.onEventClicked(event)}>
<Glyphicon glyph='edit' style={{ width: '40px' }} /> <Glyphicon glyph='edit' style={{ width: '40px', position:'static' }} />
</Button> </Button>
</td> </td>
</tr>; </tr>;
......
import React from 'react'
import { Well, Button, ButtonToolbar, Glyphicon, Tooltip, OverlayTrigger, Grid, Row, Col, ToggleButtonGroup, ToggleButton } from 'react-bootstrap'
import { URL } from '../../api/icat/icatPlus'
import { ComboSearch } from 'react-combo-search'
import PropTypes from 'prop-types';
import { NEW_EVENT_VISIBLE, } from '../../constants/EventTypes';
/* A React component which renders the 'New' button */
class NewButton extends React.Component {
render() {
if (this.props.isNewComponentVisible === undefined || this.props.isNewComponentVisible === true) {
return (<Button bsSize="small" bsStyle="primary" disabled >
<Glyphicon glyph="plus" />New
</Button>)
}
const tooltip = (text) => (
<Tooltip id="tooltip">
{text}
</Tooltip>
);
// default behavior
return (<OverlayTrigger placement="bottom" overlay={tooltip("Create a new event")}>
<Button bsSize="small" bsStyle="primary" onClick={() => this.props.onClick(NEW_EVENT_VISIBLE)} disabled={false} >
<Glyphicon glyph="plus" /> New
</Button>
</OverlayTrigger>
)
}
}
export default NewButton;
NewButton.propTypes = {
/** True when the user has clicked the button which in turn is displaying the new component */
isNewComponentVisible: PropTypes.bool,
/** Callback function triggered when the user clicks on the button */
onClick: PropTypes.func,
}
\ No newline at end of file
//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
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ButtonToolbar, Grid, Row, Col } from 'react-bootstrap'; import { ButtonToolbar, Grid, Row, Col } from 'react-bootstrap';
import NewButton from '../NewButton'; import EventListMenuButton from '../EventListMenuButton';
/** /**
* The menu displayed above the tag list * The menu displayed above the tag list
...@@ -19,9 +19,16 @@ class TagActionBar extends React.Component { ...@@ -19,9 +19,16 @@ class TagActionBar extends React.Component {
<Row> <Row>
<Col xs={2}> <Col xs={2}>
<ButtonToolbar> <ButtonToolbar>
<NewButton <EventListMenuButton
isNewComponentVisible={false} isNewComponentVisible={false}
onClick={this.showTagEditor} onClick={this.showTagEditor}
/> />
</ButtonToolbar> </ButtonToolbar>
</Col> </Col>
......
...@@ -268,6 +268,10 @@ div.collapsedEventPanel-heading:hover { ...@@ -268,6 +268,10 @@ div.collapsedEventPanel-heading:hover {
/* Logbook menu related */ /* Logbook menu related */
.logbookNavItem{
display:inline-block!important;
}
.logbookNavItem>a { .logbookNavItem>a {
padding-top: 8px!important; padding-top: 8px!important;
padding-bottom: 5px!important; padding-bottom: 5px!important;
......
...@@ -140,9 +140,9 @@ class InvestigationContainer extends Component { ...@@ -140,9 +140,9 @@ class InvestigationContainer extends Component {
return (<Grid fluid> return (<Grid fluid>
<Tab.Container id="tabs" activeKey={this.state.activeTab} onSelect={this.handleSelect}> <Tab.Container id="tabs" activeKey={this.state.activeTab} onSelect={this.handleSelect}>
<Row className="clearfix"> <Row >
<TabContainerMenu isLogBookTabDisplayed={!this.props.isDOI} datasetCount={this.props.datasets.data.length} />
<Col sm={12}> <Col sm={12}>
<TabContainerMenu isLogBookTabDisplayed={!this.props.isDOI} datasetCount={this.props.datasets.data.length} />
<Tab.Content animation> <Tab.Content animation>
<Tab.Pane eventKey={1} mountOnEnter={true}> <Tab.Pane eventKey={1} mountOnEnter={true}>
<Loader show={this.props.datasets.fetching}> <Loader show={this.props.datasets.fetching}>
...@@ -174,25 +174,22 @@ function TabContainerMenu(props) { ...@@ -174,25 +174,22 @@ function TabContainerMenu(props) {
logbookClassName = "hidden"; logbookClassName = "hidden";
} }
return ( return (
<Col sm={12}> <Nav bsStyle="tabs">
<Nav bsStyle="tabs"> <NavItem eventKey={1}>
<NavItem eventKey={1}> <div>
<div> <Glyphicon glyph="list" />
<Glyphicon glyph="list" /> <span style={{ marginLeft: "2px" }}> Dataset List &nbsp;</span>
<span style={{ marginLeft: "2px" }}> Dataset List &nbsp;</span> <Badge bsClass="ourBadges-m"> {props.datasetCount} </Badge>
<Badge bsClass="ourBadges-m"> {props.datasetCount} </Badge> </div>
</div> </NavItem>
</NavItem>
<NavItem className={logbookClassName} eventKey={3} >
<div>
<NavItem className={logbookClassName} eventKey={3} > <Glyphicon glyph="comment" />
<div> <span style={{ marginLeft: "2px" }}> Logbook </span>
<Glyphicon glyph="comment" /> </div>
<span style={{ marginLeft: "2px" }}> Logbook </span> </NavItem>
</div> </Nav>
</NavItem>
</Nav>
</Col>
) )
} }
......
This diff is collapsed.
...@@ -26,13 +26,13 @@ export function getEventIcon(category, size) { ...@@ -26,13 +26,13 @@ export function getEventIcon(category, size) {
if (category === 'comment') { if (category === 'comment') {
return (<img src='/comment.png' width={iconSize} alt='comment.png' />) return (<img src='/comment.png' width={iconSize} alt='comment.png' />)
} else if (category === 'commandLine') { } else if (category === 'commandLine') {
return <Glyphicon glyph='expand' style={{ fontSize: iconSize }} /> return <Glyphicon glyph='expand' style={{ fontSize: iconSize, position:'static' }} />
} else if (category === 'error') { } else if (category === 'error') {
return <Glyphicon glyph='alert' style={{ fontSize: iconSize, color: '#d03436' }} /> return <Glyphicon glyph='alert' style={{ fontSize: iconSize, color: '#d03436', position:'static' }} />
} else if (category === 'info') { } else if (category === 'info') {
return <Glyphicon glyph='info-sign' style={{ fontSize: iconSize, color: '#3366CC' }} /> return <Glyphicon glyph='info-sign' style={{ fontSize: iconSize, color: '#3366CC', position:'static' }} />
} else if (category === 'debug') { } else if (category === 'debug') {
return <Glyphicon glyph='exclamation-sign' style={{ fontSize: iconSize, color: 'gray' }} /> return <Glyphicon glyph='exclamation-sign' style={{ fontSize: iconSize, color: 'gray', position:'static' }} />
} else if (category === 'previousVersion') { } else if (category === 'previousVersion') {
return (<img src='/previousVersion.png' width={iconSize} alt='previousVersion.png' />) return (<img src='/previousVersion.png' width={iconSize} alt='previousVersion.png' />)
} }
......
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