Commit 4c7bac47 authored by Maxime Chaillet's avatar Maxime Chaillet

Merge branch 'issue176' into 'master'

Issue176

Closes #176

See merge request !161
parents a2aad07e 53f43f83
Pipeline #12250 passed with stages
in 6 minutes and 30 seconds
This diff is collapsed.
......@@ -15,7 +15,7 @@ import SelectionContainer from './containers/Selection/SelectionContainer.js';
import MintSelectionContainer from './containers/Selection/MintSelectionContainer.js';
import Footer from './components/Footer.js';
import { BrowserRouter as Router, Route } from "react-router-dom";
import TagContainer from './containers/TagContainer';
import TagContainer from './containers/Logbook/TagContainer';
import { TAG_MANAGER_CONTEXT } from './constants/EventTypes';
import _ from 'lodash';
......@@ -42,12 +42,9 @@ class App extends Component {
<Route exact path="/investigation/:investigationId/events" component={EventsPage} />
<Route exact path="/investigation/:investigationId/camera" component={CameraPage} />
<Route exact path="/selection" component={SelectionPage} />
<Route exact path="/selection/mint" component={MintSelectionPage} />
<Footer></Footer>
</div>
</Router>
......@@ -56,8 +53,6 @@ class App extends Component {
}
class DOIPage extends React.Component {
render() {
return (
......@@ -160,7 +155,6 @@ class ClosedDataPage extends React.Component {
</div>
);
}
}
class OpenDataPage extends React.Component {
......@@ -225,7 +219,6 @@ class OpenDataPage extends React.Component {
</div>
);
}
}
......
......@@ -8,6 +8,7 @@ import { getOriginalEvent } from '../../../helpers/EventHelpers'
import EventIcon from './EventIcon.js';
import LazyLoad from 'react-lazyload';
import { EVENT_CATEGORY_COMMANDLINE, NOTIFICATION } from '../../../constants/EventTypes.js';
import TagListInLine from '../Tag/TagListInLine';
require("./eventList.css");
/**
......@@ -59,9 +60,9 @@ class EventList extends React.Component {
<tbody>
{this.getItems().map((event, index) => {
if (event.type === "date") {
return <tr key={index}><td style={{ borderTop: '1px solid #f2f2f2', textAlign: 'center', fontSize: '18px', fontWeight: 'bold' }} colSpan={4} ><a name={event.anchor}></a> {event.text}</td></tr>;
return <tr key={index}><td style={{ borderTop: '1px solid #f2f2f2', textAlign: 'center', fontSize: '18px', fontWeight: 'bold' }} colSpan={5} ><a name={event.anchor}></a> {event.text}</td></tr>;
}
return <Event key={index} event={event} onEventClicked={this.props.onEventClicked} ></Event>
return <Event key={index} availableTags={this.props.availableTags} event={event} onEventClicked={this.props.onEventClicked} ></Event>
})}
</tbody>
</Table>
......@@ -84,6 +85,7 @@ class Event extends React.Component {
collapsed: true
}
this.handleClick = this.handleClick.bind(this);
this.getTags = this.getTags.bind(this);
}
handleClick(e) {
......@@ -96,6 +98,23 @@ class Event extends React.Component {
})}</tbody>);
}
/**
* Get the corresponding tags objects for that event
*/
getTags(event) {
let eventTags = [];
if (event && event.tag && event.tag.length !== 0) {
event.tag.forEach(tagId => {
let tag = _.find(this.props.availableTags, (availableTag) => _.isEqual(availableTag._id, tagId))
if (tag) { eventTags.push(tag); };
});
return eventTags;
}
return null;
}
render() {
let events = [this.props.event];
if (this.props.event.events && !this.state.collapsed) {
......@@ -105,7 +124,6 @@ class Event extends React.Component {
return <tr key={index} id='contentDocList'>
<td style={{ width: '16px', borderTop: '0' }}>
<EventIcon event={event} />
</td>
<td style={{ width: '16px', borderTop: '0', borderRight: '1px solid #f2f2f2' }}>
<OverlayTrigger placement="right" overlay={<Tooltip id="tooltip"> <p> Events created on {Moment(getOriginalEvent(event).creationDate).format("MMMM Do YYYY, h:mm:ss a")} </p> </Tooltip>}>
......@@ -116,7 +134,10 @@ class Event extends React.Component {
<LazyContentEvent event={event} />
{event.events && this.state.collapsed ? <Label style={{ color: "blue", backgroundColor: "white", cursor: "pointer" }} onClick={this.handleClick} >.... {event.events.length} command lines more</Label> : null}
</td>
<td className='eventActionBox' style={{ width: '50px', border: 0 }}>
<td style={{ width: '200px', border: 0 }}>
<TagListInLine tags={this.getTags(event)} />
</td>
<td style={{ width: '50px', border: 0 }}>
<Button bsStyle="link" bsSize="small" style={{ padding: '0px' }} onClick={() => this.props.onEventClicked(event)}>
<Glyphicon glyph={getPreviousVersionNumber(event) === 0 ? 'plus' : 'edit'} style={{ width: '40px', position: 'static' }} />
</Button>
......
......@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Panel, Label, Glyphicon, OverlayTrigger, Tooltip, Grid, Row, Col, Button, InputGroup, FormControl } from 'react-bootstrap';
import EventHeader from './EventHeader';
import EventFooter from './EventFooter';
import TagContainer from '../../containers/TagContainer';
import TagContainer from '../../containers/Logbook/TagContainer';
import EventContentPanel from './EventContentPanel';
import { EDIT_EVENT_INVISIBLE, NEW_EVENT_CONTEXT, EDIT_EVENT_CONTEXT, NEW_EVENT_INVISIBLE, EVENT_CATEGORY_COMMENT, ANNOTATION, LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_PLAINTEXT_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 EventHistory from './EventHistory';
......
import React from 'react'
import PropTypes from 'prop-types';
import { Label, Glyphicon } from 'react-bootstrap';
/** React component which renders a tag label */
class TagLabel extends React.Component {
render() {
let { tag, showDeleteButton } = this.props;
return (<div style={{ display: 'inline-block', marginRight: '4px' }}>
<Label
style={{
backgroundColor: tag.color,
borderBottomLeftRadius: '10px',
borderBottomRightRadius: showDeleteButton === true ? '0px' : '10px',
borderTopLeftRadius: '10px',
borderTopRightRadius: showDeleteButton === true ? '0px' : '10px',
display: 'inline-block',
marginBottom: '2px'
}}>
{tag.name}
</Label>
{showDeleteButton === true ?
<Label
style={{
backgroundColor: '#777777',
borderTopLeftRadius: '0px',
borderTopRightRadius: '10px',
borderBottomRightRadius: '10px',
borderBottomLeftRadius: '0px',
display: 'inline-block',
marginBottom: '2px',
paddingLeft: '0px',
paddingRight: '1px'
}}>
<Glyphicon
glyph="remove"
bsSize="xsmall"
style={{
backgroundColor: '#777777',
color: '#FFFFFF',
verticalAlign: 'bottom'
}}
onClick={() => this.props.onDeleteTagClicked(tag)}
/>
</Label> : null}
</div>)
}
}
TagLabel.propTypes = {
/* Callback function used to remove a tag from the selection. Used in EVENT context only */
onDeleteTagClicked: PropTypes.func,
/* Whether the delete button is displayed or not */
showDeleteButton: PropTypes.bool,
/* the tag to render */
tag: PropTypes.object.isRequired,
}
TagLabel.defaultProps = {
showDeleteButton: false,
}
export default TagLabel;
\ No newline at end of file
import React from 'react';
import PropTypes from 'prop-types';
import { Grid, Row, Col } from 'react-bootstrap';
import { TAG_MANAGER_CONTEXT, BASIC_EVENT_CONTEXT, EDIT_EVENT_CONTEXT, INFO_MESSAGE_TYPE } from '../../../constants/EventTypes';
import TagViewer from './TagViewer';
import CreatableSelect from 'react-select/lib/Creatable';
import _ from 'lodash';
import { GUI_CONFIG } from '../../../config/gui.config';
import UserMessage from '../../UserMessage';
import ScrollMenu from 'react-horizontal-scrolling-menu';
import './TagList.css';
/**
* React component which handles tags (create, edit, remove)
* @param {*} props tag as provided by the server
*/
class TagList extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
render() {
let { availableTags, context } = this.props;
if (context === EDIT_EVENT_CONTEXT) {
// show tags inside a detailed event
const customStyles = {
dropdownIndicator: () => ({
display: 'none'
}),
indicatorSeparator: () => ({
display: 'none'
}),
control: (provided) => ({
...provided,
maxWidth: 140,
minWidth: 100,
minHeight: 25,
}),
container: (provided) => ({
...provided,
}),
valueContainer: (provided) => ({
...provided,
paddingTop: '0px',
paddingBottom: '0px',
}),
menu: (provided) => ({
...provided,
bottom: '100%',
top: 'unset',
})
};
let selectedTags = this.props.selectedTags ?
this.props.selectedTags.map((tag) => { return (<TagViewer key={tag._id} context={EDIT_EVENT_CONTEXT} tag={tag} removeTag={this.props.removeTagFromSelection} />); })
: null;
let availableTagsForSelect = availableTags ?
availableTags.map((tag) => ({ value: tag._id, label: tag.name }))
: null;
return (
<div style={{ display: 'flex' }}>
<div style={{ flex: '0 0 148px' }}>
<CreatableSelect
isClearable={false}
noOptionsMessage={() => 'No existing tag'}
onChange={this.onChange}
styles={customStyles}
options={availableTagsForSelect}
isSearchable={true} />
</div>
<div style={{ flex: '1 1 100px', overflow: 'hidden' }}>
<ScrollMenu
data={selectedTags}
arrowLeft={<div className='arrow-prev'> &#60; </div>}
arrowRight={<div className='arrow-next'> &#62; </div>}
wrapperClass='customWrapperClass'
innerWrapperClass='customInnerWrapperClass'
/>
</div>
</div>
);
} else if (context === TAG_MANAGER_CONTEXT) {
// show tags for management
if (!availableTags || availableTags.length === 0) {
return (
<Grid fluid={true} >
<h3> Tags currently available in this proposal</h3>
<Row style={{ marginLeft: '30px', marginRight: '30px', marginTop: '20px' }}>
<UserMessage
message='There is no tag in this proposal yet.'
isTextCentered={true}
type={INFO_MESSAGE_TYPE}
/>
</Row>
</Grid >
);
} else {
let detailedTags = [];
let sortedAvailableTags = _.sortBy(availableTags, ['name']);
detailedTags = sortedAvailableTags.map((tag, index) => { return (<TagViewer context={TAG_MANAGER_CONTEXT} tag={tag} editTag={this.props.editTag} key={index} />); });
return (
<div style={{ paddingBottom: '70px' }}>
<Grid fluid={true} >
<h3> Tags currently available in this proposal</h3>
<Row style={{ marginTop: '20px' }}>
<Col xs={0} md={1}> </Col>
<Col xs={12} md={10}>
{detailedTags}
</Col>
<Col xs={0} md={1}> </Col>
</Row>
</Grid >
</div>);
};
} else if (context === BASIC_EVENT_CONTEXT) {
let selectedTags = this.props.selectedTags ?
this.props.selectedTags.map((tag) => { return (<TagViewer key={tag._id} context={BASIC_EVENT_CONTEXT} tag={tag} removeTag={this.props.removeTagFromSelection} />); })
: null;
return (<div> {selectedTags} </div>);
}
return null;
}
/**
* Callback function triggered when the input has changed. A new tag is being created
* @param {*} value the new tag as provided by the react-select component
*/
onChange(option, action) {
if (option && action) {
if (action.action === 'create-option') {
let newTag = {
color: GUI_CONFIG().DEFAULT_TAG_COLOR,
description: null,
name: option.label,
investigationId: this.props.investigationId
};
this.props.createNewTag(newTag);
} else if (action.action === 'select-option') {
this.props.addTagToSelection(_.find(this.props.availableTags, (tag) => { return tag._id === option.value; }));
}
}
}
}
TagList.propTypes = {
/* List of available tags (can be null when there is no available tags yet)*/
availableTags: PropTypes.array,
/* Context defining how tag list is rendered */
context: PropTypes.string.isRequired,
/* investigation identifier */
investigationId: PropTypes.string,
/* List of tags currently selected. Used only in the event context */
selectedTags: PropTypes.array,
/* Callback function trigered when the user selects a tag from the available tags. Used only in the EVENT context.*/
addTagToSelection: PropTypes.func,
/* Callback function used to create a new tag */
createNewTag: PropTypes.func,
/* Callback function trigered when the user click to edit the tag. Used only in the contexct of tag management */
editTag: PropTypes.func,
/* Callback function trigered when the user clicks the X on the tag label. Used only in Event context. */
removeTagFromSelection: PropTypes.func,
};
export default TagList;
import React from 'react';
import Proptype from 'prop-types';
import TagLabel from './TagLabel';
import ScrollMenu from 'react-horizontal-scrolling-menu';
/** React component which renders tags in line */
class TagListInLine extends React.Component {
render() {
let { hasHorizontalScrolls, tags } = this.props;
if (tags && tags instanceof Array && tags.length !== null) {
let tagLabels = tags.map((tag) => {
return (<TagLabel
key={tag._id}
onDeleteTagClicked={this.props.onDeleteTagClicked ? this.props.onDeleteTagClicked : null}
showDeleteButton={this.props.showDeleteButton === true ? true : false}
tag={tag} />);
})
if (hasHorizontalScrolls === true) {
return (
<ScrollMenu
data={tagLabels}
arrowLeft={<div className='arrow-prev'> &#60; </div>}
arrowRight={<div className='arrow-next'> &#62; </div>}
wrapperClass='customWrapperClass'
innerWrapperClass='customInnerWrapperClass'
/>)
} else {
return tagLabels;
}
}
return null;
}
}
TagListInLine.proptype = {
/** whether an horizontal scroll is displayed */
hasHorizontalScrolls: Proptype.bool,
/** Callback function triggered when the user clicks on the delete button of a tag. */
onDeleteTagClicked: Proptype.func,
/** Whether or not the delete button is shown for the tags. */
showDeleteButton: Proptype.bool,
/** all tag objects to be rendered */
tags: Proptype.array
}
TagListInLine.defaultProps = {
hasHorizontalScrolls: false,
}
export default TagListInLine;
\ No newline at end of file
import React from 'react';
import PropTypes from 'prop-types';
import { Grid, Row, Col, Button, Label } from 'react-bootstrap';
import { INFO_MESSAGE_TYPE } from '../../../constants/EventTypes';
import _ from 'lodash';
import UserMessage from '../../UserMessage';
import './TagList.css';
/**
* React component which renders a list of tags in a panel
*/
class TagListPanel extends React.Component {
render() {
let { availableTags, onEditButtonClicked } = this.props;
if (!availableTags || availableTags.length === 0) {
return (
<Grid fluid={true} >
<h3> Tags currently available in this proposal</h3>
<Row style={{ marginLeft: '30px', marginRight: '30px', marginTop: '20px' }}>
<UserMessage
message='There is no tag in this proposal yet.'
isTextCentered={true}
type={INFO_MESSAGE_TYPE}
/>
</Row>
</Grid >
);
} else {
let detailedTags = [];
let sortedAvailableTags = _.sortBy(availableTags, ['name']);
detailedTags = sortedAvailableTags.map((tag, index) => { return (<TagListPanelLine key={index} tag={tag} onEditButtonClicked={onEditButtonClicked} />); });
return (
<div style={{ paddingBottom: '70px' }}>
<Grid fluid={true} >
<h3> Tags currently available in this proposal</h3>
<Row style={{ marginTop: '20px' }}>
<Col xs={0} md={1}> </Col>
<Col xs={12} md={10}>
{detailedTags}
</Col>
<Col xs={0} md={1}> </Col>
</Row>
</Grid >
</div>);
};
}
}
TagListPanel.propTypes = {
/* List of available tags (can be null when there is no available tags yet)*/
availableTags: PropTypes.array,
/* Callback function trigered when the user click to edit the tag. Used only in the contexct of tag management */
editTag: PropTypes.func,
};
class TagListPanelLine extends React.Component {
render() {
let { tag, onEditButtonClicked } = this.props;
//set the tag scope
let tagScope = null;
if (tag.investigationId) {
tagScope = <span style={{ padding: '5px', backgroundColor: '#f2f2f2' }}> proposal </span>
} else if (tag.beamlineName) {
tagScope = <span style={{ padding: '5px', backgroundColor: '#f2f2f2' }}> beamline </span>
}
return (
<div>
<Row>
<div style={{ alignItems: 'center' }}>
<Col xs={12} sm={2} style={{ textAlign: 'center' }}>
<Label
style={{
backgroundColor: tag.color,
borderRadius: '10px',
display: 'inline-block',
paddingBottom: '5px',
fontSize: '14px'
}}>
{tag.name}
</Label>
</Col>
<Col xs={12} sm={6} style={{ alignItems: 'stretch', }}>
{tag.description ? <span> {tag.description} </span> : <span style={{ color: 'gray', fontStyle: 'italic' }}> No description yet</span>}
</Col>
<Col xs={12} sm={3}>
{tagScope}
</Col>
<Col xs={12} sm={1}>
<Button bsStyle="primary" bsSize="small" onClick={() => onEditButtonClicked(tag)}>Edit</Button>
</Col>
</div>
</Row>
<hr />
</div>
)
}
}
TagListPanelLine.proptype = {
/** tag object */
tag: PropTypes.object,
/** callback function when the user click the edit button */
onEditButtonClicked: PropTypes.func
}
export default TagListPanel;
import React from 'react';
import Proptypes from 'prop-types';
import _ from 'lodash';
import CreatableSelect from 'react-select/lib/Creatable';
import { GUI_CONFIG } from '../../../config/gui.config';
class TagSelector extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
render() {
let { availableTags } = this.props;
// show tags inside a detailed event
const customStyles = {
dropdownIndicator: () => ({
display: 'none'
}),
indicatorSeparator: () => ({
display: 'none'
}),
control: (provided) => ({
...provided,
maxWidth: 140,
minWidth: 100,
minHeight: 25,
}),
container: (provided) => ({
...provided,
}),
valueContainer: (provided) => ({
...provided,
paddingTop: '0px',
paddingBottom: '0px',
}),
menu: (provided) => ({
...provided,
bottom: '100%',
top: 'unset',
})
};
let availableTagsForSelect = availableTags ? availableTags.map((tag) => ({ value: tag._id, label: tag.name })) : null;
return (
<CreatableSelect
isClearable={false}
noOptionsMessage={() => 'No existing tag'}
onChange={this.onChange}
styles={customStyles}
options={availableTagsForSelect}
isSearchable={true} />);
}
/**
* Callback function triggered when the input has changed. A new tag is being created
* @param {*} value the new tag as provided by the react-select component
*/
onChange(option, action) {
if (option && action) {
if (action.action === 'create-option') {
let newTag = {
color: GUI_CONFIG().DEFAULT_TAG_COLOR,
description: null,
name: option.label,
investigationId: this.props.investigationId
};
this.props.onTagCreatedInSelect(newTag);
} else if (action.action === 'select-option') {
this.props.onTagSelectedInSelect(_.find(this.props.availableTags, (tag) => { return tag._id === option.value; }));
}
}
}
}
export default TagSelector;
TagSelector.proptype = {
/** list of avialable tags */
availableTags: Proptypes.array.isRequired,
/** callback function triggered when a tag is created in the select element */
onTagCreatedInSelect: Proptypes.func,
/** Callback funnction triggered when a tag is selelcted in the select element */
onTagSelectedInSelect: Proptypes.func
}
import React from 'react'
import PropTypes from 'prop-types';
import { Label, Glyphicon } from 'react-bootstrap';
import { Row, Col, Button } from "react-bootstrap"
import { TAG_MANAGER_CONTEXT, BASIC_EVENT_CONTEXT, EDIT_EVENT_CONTEXT } from '../../../constants/EventTypes';
class TagViewer extends React.Component {
render() {
let { context, tag } = this.props;
if (context === EDIT_EVENT_CONTEXT) {
return (<div style={{ display: 'inline-block', marginRight: '4px' }}>
<Label
style={{
backgroundColor: tag.color,
borderTopLeftRadius: '10px',
borderTopRightRadius: '0px',
borderBottomRightRadius: '0px',
borderBottomLeftRadius: '10px',
display: 'inline-block',
marginBottom: '2px'
}}>
{tag.name}
</Label>
<Label
style={{
backgroundColor: '#777777',
borderTopLeftRadius: '0px',
borderTopRightRadius: '10px',
borderBottomRightRadius: '10px',
borderBottomLeftRadius: '0px',
display: 'inline-block',
marginBottom: '2px',
paddingLeft: '0px',
paddingRight: '1px'
}}>
<Glyphicon
glyph="remove"
bsSize="xsmall"
style={{
backgroundColor: '#777777',
color: '#FFFFFF',
verticalAlign: 'bottom'
}}
onClick={() => this.props.removeTag(tag)}
/>
</Label>
</div>)