Commit c2f8df4e authored by Maxime Chaillet's avatar Maxime Chaillet
Browse files

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
import React from 'react';
import { Row, Col, OverlayTrigger, Tooltip, Popover, Button, Well, Label, FormControl, InputGroup, Glyphicon, Panel } from 'react-bootstrap';
import { getEventCreationDate, getEventHistoryCreationDate, getOriginalEvent, getPreviousVersionNumber, getEventIcon } from '../../helpers/EventHelpers';
import EventFooter from './EventFooter';
import EventHeader from './EventHeader';
import { ANNOTATION, NOTIFICATION, READ_MODE, DETAILED_EVENT_CONTEXT, EDIT_EVENT_CONTEXT } from '../../constants/EventTypes';
import PropTypes from 'prop-types';
import EventContentDisplayer from './EventContentDisplayer';
import Popup from 'reactjs-popup';
import Moment from 'moment';
import EventVersion from './EventVersion';
import _ from 'lodash';
import TagContainer from '../../containers/TagContainer';
/**
* A detailed event of the logbook.
*/
class EditEvent extends React.Component {
constructor(props) {
super(props);
// Initialize state
this.state = {
isSaveButtonEnabled: false, // whether the event is valid ie can be saved
isEditorContentValid: false
};
this.canEnableSaveButton = this.canEnableSaveButton.bind(this);
this.onCancelButtonClicked = this.onCancelButtonClicked.bind(this);
this.onSuccessfullyUpdated = this.onSuccessfullyUpdated.bind(this);
this.setTitleInput = this.setTitleInput.bind(this);
this.setTagContainer = this.setTagContainer.bind(this);
this.updateEvent = this.updateEvent.bind(this);
}
render() {
let { event, user } = this.props;
if (event.type === ANNOTATION || event.type === NOTIFICATION) {
return (
<div id='editionBox' style={{ display: 'flex' }} >
{/* the left panel */}
<div style={{ flexGrow: '0', maxWidth: '155px' }}>
<div className='pull-right' style={{ paddingRight: '8px' }}>
{getEventIcon(event.category, '20')}
</div>
<div style={{ paddingLeft: '5px' }} >
<div style={{ height: '50px' }} />
{/* <OverlayTrigger
placement='top'
overlay={<Tooltip id='tooltip'> <p> Manage tags </p> </Tooltip>}>
<a href={'/investigation/' + this.props.investigationId + '/events/tagManager'}
target='_blank'
style={{ float: 'right', paddingRight: '10px', color: '#777', marginTop: '2px' }}>
<Glyphicon glyph='cog' />
</a>
</OverlayTrigger> */}
{/* <Label> Tags </Label>
<div style={{ paddingTop: '5px', marginRight: '10px' }}>
<TagContainer
canEnableSaveButton={this.canEnableSaveButton}
context={DETAILED_EVENT_CONTEXT}
event={event}
investigationId={this.props.investigationId}
setTagContainer={this.setTagContainer}
/>
</div> */}
{/* <hr id='hrEvents' /> */}
{/* <Label> Creation date </Label>
<OverlayTrigger
placement='right'
overlay={
<Tooltip id='tooltip'>
<p> Event created on {getEventHistoryCreationDate(getOriginalEvent(event))} </p>
</Tooltip>
}>
<div>
<div style={{ paddingTop: '5px', paddingLeft: '10px', color: '#666666' }}>
{getEventCreationDate(getOriginalEvent(event))}
</div>
</div>
</OverlayTrigger> */}
<hr id='hrEvents' />
{/* <CommentBy event={event} /> */}
{/* <div style={{ position: 'relative' }}>
<div >
<OverlayTrigger
trigger='click'
placement='top'
overlay={eventVersionsPopover(event)}
rootClose={true}
>
<Label> History <div className='arrow-down' /> </Label>
</OverlayTrigger>
</div>
</div> */}
</div>
</div>
{/* <div
style={{ flexGrow: '1' }}>
{(event.type === ANNOTATION) ? <AnnotationContent
canEnableSaveButton={this.canEnableSaveButton}
event={event}
investigationId={this.props.investigationId}
setTitleInput={this.setTitleInput}
user={user}
/> : null}
{(event.type === NOTIFICATION) ? <NotificationContent
canEnableSaveButton={this.canEnableSaveButton}
event={event}
investigationId={this.props.investigationId}
user={user}
/> : null}
<EventFooter
onCancelButtonClicked={this.onCancelButtonClicked}
onSaveButtonClicked={this.updateEvent}
isSaveButtonEnabled={this.state.isSaveButtonEnabled} />
</div> */}
</div >);
} else {
console.log('[ERROR] Event (' + event._id + ') not shown because its type is not annotation and not notification. ');
return null;
};
}
// updateEvent() {
// let { investigationId, event, user } = this.props;
// // get tags
// let currentTagIds = this.tagContainer.state.selectedTags.map((tag) => tag._id);
// let updatedEvent = {
// _id: event._id,
// category: event.category,
// content: [
// {
// format: 'plainText',
// text: localStorage.getItem('plainText')
// },
// {
// format: 'html',
// text: localStorage.getItem('HTMLText')
// }
// ],
// creationDate: Date(),
// type: event.type,
// tags: currentTagIds,
// title: (this.inputTitle && this.inputTitle.props) ? this.inputTitle.props.value : null,
// username: user.username,
// previousVersionEvent: event._id
// };
// this.props.updateEvent(updatedEvent, user.sessionId, investigationId, this.onSuccessfullyUpdated);
// }
// /** Callback executed when the event has been successfully updated on the server side*/
// onSuccessfullyUpdated(updatedEvent) {
// this.props.toggleMode(READ_MODE); //this makes sure the mode is changed in case the event is updated.
// this.props.onEventUpdated(updatedEvent);
// }
// /**
// * Callback function triggered when the user click the cancel button
// * @param {*} e the browser's event
// */
// onCancelButtonClicked(e) {
// e.stopPropagation();
// this.props.toggleMode(READ_MODE);
// }
/**
* Callback function called when the text of the editor changed from empty to something or vice versa
* @param {object} change an object containing the properties:
* -1- hasText boolean which indicates whether the editor content has text or not; not null
* -2- currentTextEqualsOriginal, boolean, which indicates whether the current content is the same as the original content; not null
*/
// canEnableSaveButton(change) {
// let hasEventTitleChangedFromItsOriginalValue =
// this.inputTitle &&
// this.props.event &&
// this.inputTitle.props.value !== this.props.event.title;
// let hasEventTagsChangedFromItsOriginalValue =
// this.tagContainer &&
// this.props.event &&
// !(_.isEqual(this.tagContainer.state.selectedTags.map((tag) => tag._id), this.props.event.tag));
// if (hasEventTitleChangedFromItsOriginalValue || hasEventTagsChangedFromItsOriginalValue) {
// /* title is not the same as original. can save (even with no content) */
// if (this.state.isSaveButtonEnabled !== true) { this.setState({ isSaveButtonEnabled: true }); }
// } else {
// /* title is the same as original */
// if (change) {
// /* triggered from the editor */
// let hasText = null;
// let currentTextEqualsOriginal = null;
// if ('hasText' in change) { hasText = change.hasText; };
// if ('currentTextEqualsOriginal' in change) {
// currentTextEqualsOriginal = change.currentTextEqualsOriginal;
// };
// if (!hasText) {
// // there is no text in the editor
// if (this.state.isEditorContentValid !== false) {
// this.setState({
// isEditorContentValid: false,
// isSaveButtonEnabled: false
// });
// };
// } else {
// // there is text in the editor
// if (currentTextEqualsOriginal === undefined || currentTextEqualsOriginal === null) {
// if (this.state.isEditorContentValid !== false) {
// this.setState({
// isEditorContentValid: false,
// isSaveButtonEnabled: false
// });
// };
// } else if (currentTextEqualsOriginal) {
// // current text in the editor equals the original text
// if (this.state.isEditorContentValid !== false) {
// this.setState({
// isEditorContentValid: false,
// isSaveButtonEnabled: false
// });
// };
// } else {
// if (this.state.isEditorContentValid !== true) {
// this.setState({
// isEditorContentValid: true,
// isSaveButtonEnabled: !hasEventTitleChangedFromItsOriginalValue
// });
// };
// };
// }
// } else {
// if (!this.state.isEditorContentValid) {
// /* Editor content is not valid. Can not save */
// if (this.state.isSaveButtonEnabled !== false) { this.setState({ isSaveButtonEnabled: false }); }
// };
// };
// };
// }
setTitleInput(element) {
this.inputTitle = element;
}
// setTagContainer(element) {
// this.tagContainer = element;
// }
}
EditEvent.propTypes = {
/** the event object as received from the ICAT+ server */
event: PropTypes.object.isRequired,
/** the investigationId which the events belong to. */
investigationId: PropTypes.string.isRequired,
/**Callback function triggered when the user updated an existing event */
onEventUpdated: PropTypes.func,
/* Callback function used to change from one mode to the other READ_MODE vs EDIT_MODE*/
toggleMode: PropTypes.func.isRequired,
/** the user who is currently logged in */
user: PropTypes.object.isRequired,
};
/**
* 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 the commentBy part.
// * @param {*} props
// */
// const CommentBy = (props) => {
// let { event } = props;
// if (getPreviousVersionNumber(event) === 0) {
// return (
// <div>
// <Label> Created by </Label>
// <div style={{ paddingTop: '5px', paddingLeft: '10px' }}>
// <p style={{ color: '#666666' }}> {props.event.username} </p>
// </div>
// <hr id='hrEvents' />
// </div>
// );
// };
// if (getPreviousVersionNumber(event) !== 0) {
// return (
// <div>
// <Label> Commented by </Label>
// <div style={{ paddingTop: '5px', paddingLeft: '10px' }}>
// <p style={{ color: '#666666' }}> {props.event.username} </p>
// </div>
// <hr id='hrEvents' />
// </div>
// );
// };
// return null;
// };
// /**
// * 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 EditEvent;
\ No newline at end of file
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 { Well, Button, ButtonToolbar, Glyphicon, Tooltip, OverlayTrigger, ToggleButtonGroup, ToggleButton, Navbar, Nav, NavItem, NavDropdown, MenuItem } from 'react-bootstrap';
import NewButton from './NewButton';
import { Well, Navbar, Nav, NavItem, MenuItem, DropdownButton, Radio } from 'react-bootstrap';
import EventListMenuButton from './EventListMenuButton';
import { getPDF } from '../../api/icat/icatPlus';
import { ComboSearch } from 'react-combo-search';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { DOC_VIEW, LIST_VIEW, SORT_EVENTS_FROM_YOUNGEST, SORT_EVENTS_FROM_OLDEST } from '../../constants/EventTypes';
import { GUI_CONFIG } from '../../config/gui.config';
import { DOC_VIEW, LIST_VIEW, SORT_EVENTS_FROM_YOUNGEST, SORT_EVENTS_FROM_OLDEST, NEW_EVENT_VISIBLE } from '../../constants/EventTypes';
// styles
require('./react-datetime.css');
......@@ -16,19 +15,13 @@ require('./react-combo-search.css');
/**
* The menu displayed above the event list
*/
class EventActionBar extends React.Component {
class EventListMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
isNavbarExpanded: true
isNavbarExpanded: false
};
this.getSelectPickerData = this.getSelectPickerData.bind(this);
this.onSearch = this.onSearch.bind(this);
this.onSelectNavbar = this.onSelectNavbar.bind(this);
this.onToggleNavbar = this.onToggleNavbar.bind(this);
this.sortByDate = this.sortByDate.bind(this);
}
render() {
......@@ -39,7 +32,8 @@ class EventActionBar extends React.Component {
sessionId,
setNewEventVisibility,
setView,
sortingFilter } = this.props;
sortingFilter,
view } = this.props;
function getCurrentlyPressedSortButton() {
if (sortingFilter && sortingFilter.createdAt) {
......@@ -47,94 +41,49 @@ class EventActionBar extends React.Component {
}
}
let defaultSortingOrder = (GUI_CONFIG().DEFAULT_SORTING_FILTER) ? GUI_CONFIG().DEFAULT_SORTING_FILTER.createdAt : 1;
return (
<Navbar
fluid
expanded={this.state.isNavbarExpanded}
onSelect={this.onSelectNavbar}
onToggle={this.onToggleNavbar}
style={{ background: 'none', border: 'none', WebkitBoxShadow: 'none', boxShadow: 'none' }}>
style={{ background: 'none', border: 'none', WebkitBoxShadow: 'none', boxShadow: 'none', position: 'sticky', top: '0px', backgroundColor:'white' }}>
<Navbar.Header>
<Navbar.Toggle />
<Nav style={{ marginLeft: '0px' }}>
<NavItem eventKey={1} href="#" className="logbookNavItem" onClick={() => setNewEventVisibility(NEW_EVENT_VISIBLE)}>
<EventListMenuButton text='New' glyph='plus' tooltipText='Create a new event' isEnabled={!isNewEventVisible} />
</NavItem>
<NavItem eventKey={2} href={'/investigation/' + this.props.investigationId + '/camera'} target='_blank' className="logbookNavItem" >
<EventListMenuButton text='Take a photo' glyph='camera' tooltipText='Take a photo' isEnabled={true} />
</NavItem>
</Nav>
</Navbar.Header>
<Navbar.Collapse>
<Nav>
<NavItem eventKey={1} href="#" className="logbookNavItem" >
<NewButton
isNewComponentVisible={isNewEventVisible}
onClick={setNewEventVisibility}
/>
<NavItem eventKey={2} href="#" className="logbookNavItem" >
<DropdownButton onClick={(e) => { e.stopPropagation() }}
bsStyle={'primary'}
title={'View'}
bsSize='small'>
<MenuItem eventKey="1" onSelect={() => setView(DOC_VIEW)} > {view === DOC_VIEW ? <Radio checked> Classic (user's commented logs)</Radio> : <Radio checked={false}> Classic (user's commented logs)</Radio>} </MenuItem>
<MenuItem eventKey="2" onSelect={() => setView(LIST_VIEW)} > {view === LIST_VIEW ? <Radio checked> Full (all logs)</Radio> : <Radio checked={false}> Full (all logs)</Radio>} </MenuItem>
<MenuItem divider />
<MenuItem eventKey="3" onSelect={() => this.sortByDate(SORT_EVENTS_FROM_OLDEST)} > {getCurrentlyPressedSortButton() === SORT_EVENTS_FROM_OLDEST ? <Radio checked> Oldest log on top</Radio> : <Radio checked={false}> Oldest log on top</Radio>} </MenuItem>
<MenuItem eventKey="4" onSelect={() => this.sortByDate(SORT_EVENTS_FROM_YOUNGEST)} > {getCurrentlyPressedSortButton() === SORT_EVENTS_FROM_YOUNGEST ? <Radio checked> Latest log on top</Radio> : <Radio checked={false}> Latest log on top</Radio>} </MenuItem>
</DropdownButton>
</NavItem>
<NavDropdown eventKey={2} title="More" className="logbookNavItem" id="basic-nav-dropdown" style={{ paddingTop: '5px' }}>