Commit 02467665 authored by Maxime Chaillet's avatar Maxime Chaillet

Merge branch 'issue152' into 'master'

Issue152

Closes #152

See merge request !135
parents a2c0b2a9 25498af6
/* stylesheet which overrides default browser css styles. CSS below are used in tinyMCE only. */
body {
overflow-y: auto!important;
padding-bottom: 0px!important;
}
ul, ol {
display: inline-block;
}
\ No newline at end of file
......@@ -11,6 +11,7 @@ export function fetchDatasetsByDOI(sessionId, doi) {
}
}
export function fetchDatasetsByInvestigationId(sessionId, investigationId) {
return function (dispatch) {
dispatch({
......
......@@ -4,7 +4,7 @@ import { getOriginalEvent, getEventIcon, getLastCommentContent, getEventCreation
import EventContentDisplayer from './EventContentDisplayer';
import { ANNOTATION, NOTIFICATION, EDIT_MODE, DOC_VIEW, BASIC_EVENT_CONTEXT } from '../../constants/EventTypes';
import { OverlayTrigger, Tooltip, Dropdown, MenuItem } from 'react-bootstrap';
import { OverlayTrigger, Tooltip, Dropdown, MenuItem, Button } from 'react-bootstrap';
import TagContainer from '../../containers/TagContainer';
require('./event.css');
......@@ -41,8 +41,8 @@ class BasicEvent extends React.Component {
return (
<div
id='contentDocList'
onDoubleClick={() => { toggleMode(EDIT_MODE); }}
style={{ display: 'flex' }} >
<Button onClick={() => { this.props.onEventClicked(event); }} />
<div
id={(this.props.isSelected === true) ? 'selectedEvent' : ''}
style={{ flexGrow: '0', flexShrink: '0' }}
......@@ -83,11 +83,11 @@ class BasicEvent extends React.Component {
<div style={{ flexGrow: '1', marginLeft: '30px' }}>
{showContent()}
</div>
<div style={{ flexGrow: '1', flexShrink: '0', flexBasis: '100px' }}>
{/* <div style={{ flexGrow: '1', flexShrink: '0', flexBasis: '100px' }}>
<div style={{ textAlign: 'right' }}>
<TagContainer context={BASIC_EVENT_CONTEXT} investigationId={investigationId} event={event} />
</div>
</div>
</div> */}
</div >
);
};
......
This diff is collapsed.
This diff is collapsed.
import React from 'react';
import { READ_MODE, EDIT_MODE, ANNOTATION, NOTIFICATION } from '../../constants/EventTypes';
import BasicEvent from './BasicEvent';
import DetailedEvent from './DetailedEvent';
import EditEvent from './EditEvent';
import PropTypes from 'prop-types'
/**
......@@ -26,20 +26,21 @@ class Event extends React.Component {
event={event}
investigationId={investigationId}
isSelected={this.props.isSelected}
onEventClicked={this.props.onEventClicked}
toggleMode={this.toggleMode}
user={user}
reverseEventsSortingByCreationDate={this.props.reverseEventsSortingByCreationDate}
view={view}
/>
} else if (this.state.mode === EDIT_MODE)
return <DetailedEvent
event={event}
investigationId={investigationId}
onEventUpdated={onEventUpdated}
toggleMode={this.toggleMode}
user={user}
updateEvent={this.props.updateEvent}
/>
} else if (this.state.mode === EDIT_MODE){}
// return <DetailedEvent
// event={event}
// investigationId={investigationId}
// onEventUpdated={onEventUpdated}
// toggleMode={this.toggleMode}
// user={user}
// updateEvent={this.props.updateEvent}
// />
} else {
console.log("[ERROR] the view is not set. This should not happen.");
return null;
......
......@@ -2,6 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import HTMLEditor from './HTMLEditor';
import { getContent } from '../../helpers/EventHelpers';
import { LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_HTML_FORMAT, LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_PLAINTEXT_FORMAT, EDIT_EVENT_CONTEXT, PLAINTEXT_CONTENT_FORMAT } from '../../constants/EventTypes';
/**
......@@ -12,20 +13,20 @@ import { getContent } from '../../helpers/EventHelpers';
*/
class EventContentDisplayer extends React.Component {
render() {
let { isEditionMode, user, content, useRichTextEditor, investigationId, canEnableSaveButton } = this.props;
let { isEditionMode, user, content, useRichTextEditor, investigationId, onEventModified, storeToLocalStorage } = 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'));
storeToLocalStorage(EDIT_EVENT_CONTEXT, getContent(content, 'plainText'), PLAINTEXT_CONTENT_FORMAT);
return (<HTMLEditor
user={user}
text={HTMLText}
onEventModified={onEventModified}
isEditionMode={isEditionMode}
investigationId={investigationId}
canEnableSaveButton={canEnableSaveButton}
storeToLocalStorage={storeToLocalStorage}
text={HTMLText}
user={user}
/>)
} else if (useRichTextEditor === false) {
return (<div dangerouslySetInnerHTML={{ __html: HTMLText }} />)
......
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 { EDIT_EVENT_CONTEXT, ANNOTATION, NOTIFICATION, NEW_EVENT_CONTEXT, PLAINTEXT_CONTENT_FORMAT, LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_PLAINTEXT_FORMAT, HTML_CONTENT_FORMAT, LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_HTML_FORMAT, LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_HTML_FORMAT, LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_PLAINTEXT_FORMAT } from '../../constants/EventTypes';
import { Label, FormControl, InputGroup } from 'react-bootstrap';
import EventContentDisplayer from './EventContentDisplayer';
import PropTypes from 'prop-types';
import { getOriginalEvent, getPreviousVersionNumber, } from '../../helpers/EventHelpers';
import HTMLEditor from './HTMLEditor';
class EventContentPanel extends React.Component {
constructor(props) {
super(props);
if (props.event) { this.eventId = props.event._id; }; //required for component will unmount in EDIT_EVENT_CONTEXT
}
render() {
let { event, context, getEditorHeight, investigationId, user, onEventModified } = this.props;
if (context === EDIT_EVENT_CONTEXT) {
if (event) {
if (event.type === ANNOTATION) {
return (<EventContentDisplayer
onEventModified={onEventModified}
content={event.content}
eventId={event._id}
investigationId={investigationId}
isEditionMode={true}
storeToLocalStorage={this.storeToLocalStorage}
useRichTextEditor={true}
user={user}
/>);
}
if (event.type === NOTIFICATION) {
return (<NotificationContent
onEventModified={onEventModified}
event={event}
investigationId={investigationId}
storeToLocalStorage={this.storeToLocalStorage}
user={user}
/>);
}
}
} else if (context === NEW_EVENT_CONTEXT) {
return (<HTMLEditor
onEventModified={onEventModified}
investigationId={investigationId}
isEditionMode={true}
storeToLocalStorage={this.storeToLocalStorage}
user={user}
/>
);
}
return null;
}
componentWillUnmount() {
if (this.props.context === NEW_EVENT_CONTEXT) {
localStorage.removeItem(LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_PLAINTEXT_FORMAT);
localStorage.removeItem(LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_HTML_FORMAT);
}
if (this.eventId && this.props.context === EDIT_EVENT_CONTEXT) {
localStorage.removeItem(LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_PLAINTEXT_FORMAT + this.eventId);
localStorage.removeItem(LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_HTML_FORMAT + this.eventId);
}
}
/**
* Callback function used to store data to the localstorage
* @param {string} context
* @param {string} content content data to store
* @param {string} contentFormat data format
*/
storeToLocalStorage = (context, content, contentFormat) => {
if (context && contentFormat) {
if (context === NEW_EVENT_CONTEXT) {
if (contentFormat === PLAINTEXT_CONTENT_FORMAT) {
localStorage.setItem(LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_PLAINTEXT_FORMAT, content);
} else if (contentFormat === HTML_CONTENT_FORMAT) {
localStorage.setItem(LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_HTML_FORMAT, content);
}
}
if (context === EDIT_EVENT_CONTEXT) {
if (contentFormat === PLAINTEXT_CONTENT_FORMAT) {
localStorage.setItem(LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_PLAINTEXT_FORMAT + this.props.event._id, content);
} else if (contentFormat === HTML_CONTENT_FORMAT) {
localStorage.setItem(LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_HTML_FORMAT + this.props.event._id, content);
}
}
}
}
}
export default EventContentPanel;
EventContentPanel.proptype = {
/** Callback function which returns the min or max editor height of the editor. */
getEditorHeight: PropTypes.func,
/* investigation identifier */
investigationId: PropTypes.string,
/* user who is using this component */
user: PropTypes.object,
}
// /**
// * React component which represents the content of an annotation
// * @param {*} props the props passed to this component
// */
// class AnnotationContent extends React.Component {
// constructor(props) {
// super(props);
// // this.state = {
// // inputTitleValue: this.props.event.title || ''
// // };
// //this.onChangeInputValue = this.onChangeInputValue.bind(this);
// }
// // onChangeInputValue(e) {
// // this.setState({ inputTitleValue: e.target.value });
// // }
// render() {
// let { event, investigationId, storeToLocalStorage, user } = this.props;
// if (event.type && event.type === ANNOTATION) {
// return (
// <div>
// <EventContentDisplayer
// onEventModified={this.props.onEventModified}
// content={event.content}
// eventId={event._id}
// investigationId={investigationId}
// isEditionMode={true}
// storeToLocalStorage={storeToLocalStorage}
// useRichTextEditor={true}
// user={user}
// />
// {/* <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'
// ref={this.props.setTitleInput}
// />
// </InputGroup>
// </div> */}
// </div>
// );
// };
// }
// componentDidUpdate() {
// this.props.onEventModified();
// }
// }
// AnnotationContent.propTypes = {
// /* the callback function which activates the save button */
// onEventModified: PropTypes.func.isRequired,
// /* The event to be shown */
// event: PropTypes.object.isRequired,
// /* the investigationId */
// investigationId: PropTypes.string.isRequired,
// /* the callback function which adds a ref to this */
// setTitleInput: PropTypes.func.isRequired,
// /* the user */
// user: PropTypes.object.isRequired
// };
/**
* React component which represents the content of an notification
* @param {*} props the props passed to this component
*/
const NotificationContent = (props) => {
let { event, getEditorHeight, investigationId, user } = props;
let notificationMessage = getOriginalEvent(event);
if (event.type && event.type === NOTIFICATION) {
let editorContent;
if (getPreviousVersionNumber(event) === 0) {
let fakeContent = [{ format: 'html', text: '<p> </p>' }];
editorContent = (<EventContentDisplayer
onEventModified={props.onEventModified}
content={fakeContent}
eventId={event._id}
investigationId={investigationId}
isEditionMode={true}
storeToLocalStorage={props.storeToLocalStorage}
useRichTextEditor={true}
user={user}
/>);
} else {
editorContent = (<EventContentDisplayer
onEventModified={props.onEventModified}
content={event.content}
eventId={event._id}
investigationId={investigationId}
isEditionMode={true}
storeToLocalStorage={props.storeToLocalStorage}
useRichTextEditor={true}
user={user}
/>);
};
return (
<div>
<div style={{ marginLeft: '10px' }} >
<div id='divContainingHTMLNotificationInDocView'>
<EventContentDisplayer
content={notificationMessage.content}
eventId={event._id}
isEditionMode={false}
storeToLocalStorage={props.storeToLocalStorage}
useRichTextEditor={false}
/>
</div>
</div>
{editorContent}
</div>
);
};
};
NotificationContent.propTypes = {
/* the callback function which activates the save button */
onEventModified: PropTypes.func.isRequired,
/* The event to be shown */
event: PropTypes.object.isRequired,
/* the investigationId */
investigationId: PropTypes.string.isRequired,
/* the user */
user: PropTypes.object.isRequired
};
\ No newline at end of file
......@@ -8,11 +8,11 @@ import PropTypes from 'prop-types'
class EventFooter extends React.Component {
render() {
return (
<div style={{ position: 'relative', height:'37px' }}>
<div style={{ position: 'relative', height: '37px' }}>
&nbsp;
<div className="noUnderlinedLink" style={{ position: 'absolute', right: '10px', top: '5px' }}>
<ButtonToolbar>
<ButtonGroup>
<ButtonGroup style={{ height: '30px' }}>
{this.props.isSaveButtonEnabled === true ?
<OverlayTrigger
placement="left"
......
import React from 'react'
import { Row, Glyphicon, Panel } from 'react-bootstrap'
import { Row, Panel } from 'react-bootstrap'
import PropTypes from 'prop-types'
import { getEventIcon, getEventHistoryCreationDate } from '../../helpers/EventHelpers'
import { NEW_EVENT_CONTEXT, EVENT_HISTORY_ORIGINAL_VERSION_CONTEXT, EVENT_HISTORY_MIDDLE_VERSION_CONTEXT, EVENT_HISTORY_LATEST_VERSION_CONTEXT } from '../../constants/EventTypes';
import { NEW_EVENT_CONTEXT, EVENT_HISTORY_ORIGINAL_VERSION_CONTEXT, EVENT_HISTORY_MIDDLE_VERSION_CONTEXT, EVENT_HISTORY_LATEST_VERSION_CONTEXT, EDIT_EVENT_CONTEXT } from '../../constants/EventTypes';
/**
* A class which represents the event header. This component renders something different in the header based on the context prop.
......@@ -22,9 +21,7 @@ class EventHeader extends React.Component {
if (context === EVENT_HISTORY_ORIGINAL_VERSION_CONTEXT) {
return (<Panel.Heading >
<Row>
<div className="pull-right">
{this.showTags(event)}
</div>
{/* <div className="pull-right"> <p> Tags not shown </p> </div> */}
<div style={{ paddingLeft: '10px' }}> {getEventIcon(event.category)}
&nbsp; On {getEventHistoryCreationDate(event)}, the original message was:
</div>
......@@ -33,20 +30,16 @@ class EventHeader extends React.Component {
} else if (context === EVENT_HISTORY_LATEST_VERSION_CONTEXT) {
return (<Panel.Heading >
<Row>
<div className="pull-right">
{this.showTags(event)}
</div>
{/* <div className="pull-right"> <p> Tags not shown </p> </div> */}
<div style={{ paddingLeft: '10px' }}> {getEventIcon("previousVersion")}
&nbsp; Written on {getEventHistoryCreationDate(event)} (latest version), the current comment is:
</div>
</Row>
</Panel.Heading>)
</Panel.Heading>)
} else if (context === EVENT_HISTORY_MIDDLE_VERSION_CONTEXT) {
return (<Panel.Heading >
<Row>
<div className="pull-right">
{this.showTags(event)}
</div>
{/* <div className="pull-right"> <p> Tags not shown </p> </div> */}
<div style={{ paddingLeft: '10px' }}> {getEventIcon("previousVersion")}
&nbsp; On {getEventHistoryCreationDate(event)}, the comment was:
</div>
......@@ -59,6 +52,8 @@ class EventHeader extends React.Component {
return (<Panel.Heading >
<b> New comment </b>
</Panel.Heading>)
} else if (context === EDIT_EVENT_CONTEXT) {
return (<Panel.Heading> <b> Edit </b> </Panel.Heading>);
}
}
}
......
import React from 'react';
import { Row, Col, OverlayTrigger, Popover, Button, Well, Label } from 'react-bootstrap';
import { getPreviousVersionNumber, } from '../../helpers/EventHelpers';
import Popup from 'reactjs-popup';
import Moment from 'moment';
import EventVersion from './EventVersion';
import LabeledElement from './LabeledElement';
/**
* React component which renders event history label and the correwsponding modal.
*/
class EventHistory extends React.Component {
render() {
if (this.props.event) {
return (
<div >
<OverlayTrigger
trigger='click' placement='top' overlay={eventVersionsPopover(this.props.event)} rootClose={true} >
<div>
<LabeledElement data={null} type='text' labelText={<span> History <div className='arrow-down' /> </span>} />
</div>
</OverlayTrigger>
</div>);
}
return null;
}
}
/**
* Render the popup which displays all event versions of the event
* @param {*} event the event under investigation
*/
function eventVersionsPopover(event) {
let e = event;
/* build the different lines representing the previous versions */
let lines = [];
while (e.previousVersionEvent != null) {
if (lines.length === 0) {
lines.push(<EventVersionItem event={e} type='edition' mostRecent={true} />);
} else {
lines.push(<EventVersionItem event={e} type='edition' mostRecent={false} />);
};
e = e.previousVersionEvent;
}
lines.push(<EventVersionItem event={e} type='creation' />);
/* build the popover */
return (<Popover id='mypopover'>
<div>
<p style={{ marginBottom: '5px' }}> This log has {getPreviousVersionNumber(event) + 1} versions. </p>
<hr style={{ marginTop: '5px' }} />
{lines.map((item, index) => <Popup
key={index}
trigger={<div> {item} </div>}
modal
closeOnDocumentClick={false}
contentStyle={{ width: '90%', height: '90%' }}
>
{close => (
<div className='fullPage'>
<a className='close' onClick={close}> &times; </a>
<div className='content'>
<EventVersion
event={item.props.event}
isLatestVersion={(index === 1) ? true : false}
isOriginalVersion={(index === lines.length - 1) ? true : false}
/>
</div>
</div>
)}
</Popup>
)}
<hr style={{ marginBottom: '5px' }} />
<Popup
trigger={<div style={{ display: 'inline' }}> <Button bsStyle='link'> See all versions </Button> </div>}
modal
closeOnDocumentClick={false}
contentStyle={{ width: '90%', height: '90%' }}
>
{close => (
<div className='fullPage'>
<a className='close' onClick={close}> &times; </a>
<div className='content'>
<CompleteEventHistory event={event} />
</div>
</div>
)}
</Popup>
</div>
</Popover>);
}
/**
* React component which represents on item of the event history
* @param {*} props the props passed to this component
*/
class EventVersionItem extends React.Component {
render() {
function buildTheDisplay(event, type, mostRecent) {
return (
<Well bsSize='small' style={{ marginBottom: '5px', cursor: 'pointer' }}>
<b> {event.username} </b>
{type === 'creation' ? 'created on ' : ''}
{type === 'edition' ? 'edited on ' : ''}
{Moment(event.creationDate).format('MMMM DD HH:mm')}
{mostRecent === true ? ' (most recent)' : ''}
{event.machine ? <span style={{ color: '#777777', fontStyle: 'italic' }}> from {event.machine} </span> : ''}
</Well>
);
};
return (
buildTheDisplay(this.props.event, this.props.type, this.props.mostRecent)
);
};
};
/**
* React component which displays the complete event history
*/
const CompleteEventHistory = (props) => {
let { event } = props;
let eventVersions = [];
/* first push the current event which is the latest version of the event*/
//eventHistoryItems.push(<EventListViewExpanded event={event} isFullPage={true} />)
eventVersions.push(<EventVersion event={event} isLatestVersion={true} />);
/* then display the previous version of this event */
while (event.previousVersionEvent != null) {
event = event.previousVersionEvent;
eventVersions.push(<div class='margin-bottom-10'>
<Row>
<Col xs={1}> </Col>
<Col xs={11}>
<EventVersion event={event} isOriginalVersion={event.previousVersionEvent === null ? true : false} />
</Col>
</Row>
</div>);
};
return eventVersions;
};
export default EventHistory;
\ No newline at end of file
......@@ -50,6 +50,7 @@ class EventList extends React.Component {
events,