It references issue #12

parent b0fa999f
package-lock.json
# See https://help.github.com/ignore-files/ for more about ignoring files.
......
This diff is collapsed.
......@@ -26,7 +26,10 @@
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<link rel="stylesheet" href="https://npmcdn.com/react-bootstrap-table/dist/react-bootstrap-table.min.css">
<link rel="stylesheet" href="https://npmcdn.com/react-bootstrap-table/dist/react-bootstrap-table.min.css">
<!-- icons for the editor-->
<!--<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">-->
<title>ESRF Portal</title>
</head>
......
import React, { Component } from 'react';
import './App.css';
import "babel-polyfill";
import LoginContainer from './containers/LoginContainer.js';
import InvestigationsContainer from './containers/InvestigationsContainer.js';
import MenuContainer from './containers/MenuContainer.js';
......@@ -9,6 +10,7 @@ import Footer from './components/Footer.js'
import "babel-polyfill";
import { BrowserRouter as Router, Route } from "react-router-dom";
import EventContainer from "./containers/EventContainer"
import NewEvent from './components/Event/NewEvent'
......@@ -24,7 +26,9 @@ class App extends Component {
<Route exact path="/investigations" component={InvestigationsContainer} />
<Route exact path="/selection" component={SelectionContainer} />
{/*<Route path="/investigation/:id/events/new" component={NewEvent} />*/}
<Route path="/investigation/:investigationId/events" component={EventContainer} />
<Route exact path="/investigation/:investigationId/events" component={EventContainer} />
<Route exact path={"/investigation/:investigationId/events/new"} component={NewEvent} />
<Footer></Footer>
</div>
</Router>
......
......@@ -8,10 +8,17 @@ import { postEventsHTTPRequest } from '../../api/icat/icatPlus.js'
import { GET_EVENTS_AT } from '../../constants/ActionTypes.js';
export function postEvents(newEvent, sessionId) {
/**
* Post the event to ICAT+ server
* @param {*} newEvent the event object to be uploaded
* @param {*} sessionId the session Id required for HTTP request authentication
* @param {*} investigationId the investigation Id to which the event will be added to.
* @param {*} updateEventList the function which updates the event list
*/
export function postEvents(newEvent, sessionId, investigationId, onSuccess) {
axios({
method: 'post',
url: postEventsHTTPRequest(),
url: postEventsHTTPRequest(investigationId),
data: newEvent,
headers: {
sessionId: sessionId
......@@ -21,6 +28,7 @@ export function postEvents(newEvent, sessionId) {
console.log("the new event was posted successfully");
localStorage.removeItem('plainText');
localStorage.removeItem('formattedHTML');
onSuccess();
})
.catch(function (error) {
console.log(error);
......
......@@ -6,11 +6,11 @@ import ICATPLUS from '../../config/icat/icatPlus.js'
* @param {*} investigationId : the investigationId to get the events from
*/
export function getEventsByInvestigationIdHTTPRequest(investigationId) {
return ICATPLUS.server + "/events/" + investigationId;
return ICATPLUS.server + "/investigations/" + investigationId + "/events";
}
export function postEventsHTTPRequest() {
return ICATPLUS.server + "/events";
export function postEventsHTTPRequest(investigationId) {
return ICATPLUS.server + "/investigations/" + investigationId + "/events";
}
/**
......@@ -20,7 +20,7 @@ export function postEventsHTTPRequest() {
* @return {File} the file
*/
export function getFileByEventIdHTTPRequest(investigationId, eventId, sessionId) {
return ICATPLUS.server + '/events/' + investigationId + '/event/' + eventId + '/download' + "?sessionId=" + sessionId;
return ICATPLUS.server + '/investigations/' + investigationId + '/events/' + eventId + '/file' + "?sessionId=" + sessionId;
}
/**
......@@ -28,5 +28,13 @@ export function getFileByEventIdHTTPRequest(investigationId, eventId, sessionId)
* @param {*} investigationId the investigationId
*/
export function uploadFile(investigationId) {
return ICATPLUS.server + "/events/" + investigationId + "/upload"
return ICATPLUS.server + "/investigations/" + investigationId + "/events/file"
}
/**
* Download the PDF file of the whole logbook for a given investigation
* @param {*} investigationId the given investigation ID
*/
export function getPDF(investigationId, sessionId) {
return ICATPLUS.server + "/investigations/" + investigationId + "/events/pdf" + "?sessionId=" + sessionId;
}
\ No newline at end of file
.dataset-table-container{
margin-top: 120px;
margin-top: 20px;
margin-bottom: 100px;
margin-right: 10px;
margin-left: 10px;
......
......@@ -2,12 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import {Glyphicon, Grid, Row, Col, Tab, Tabs, Panel } from 'react-bootstrap';
import './DatasetTable.css';
import Moment from 'react-moment';
import DatasetHeader from "./DatasetHeader/DatasetHeader.js"
import DatasetFooter from "./DatasetFooter/DatasetFooter.js"
import DatasetSummary from "./DatasetSummary/DatasetSummary.js"
import DatasetMetadataTab from "./DatasetMetadataTab/DatasetMetadataTab.js"
import DatasetTechniqueTab from "./DatasetTechniqueTab/DatasetTechniqueTab.js"
import Moment from 'react-moment';
import TECHNIQUES from "../../config/techniques/techniques.js"
import DatasetFileTree from "../../components/File/DatasetFileTree.js"
......
This diff is collapsed.
import React from 'react';
//bootstrap
import { DropdownButton, MenuItem } from 'react-bootstrap';
import { Editor, getEventRange, getEventTransfer } from 'slate-react'
import { Block, Value } from 'slate'
import EditTable from 'slate-edit-table'
import Plain from 'slate-plain-serializer'
import imageExtensions from 'image-extensions'
import { LAST_CHILD_TYPE_INVALID } from 'slate-schema-violations'
import isUrl from 'is-url'
import propType from 'prop-types'
import { initialValue } from './initialValue'
import { tablePlugin } from './Plugins/table.jsx';
import { imagePlugin } from './Plugins/image.jsx';
import { marksHelper } from './marks';
//import Html from 'slate-html-serializer';
//import { rules } from './htmlSerializerRules'
// Create a new serializer instance with our `rules` from above.
//const html = new Html({ rules })
const tablePluginInstance = EditTable({
typeTable: 'table',
typeRow: 'table_row',
typeCell: 'table_cell',
typeContent: 'paragraph',
})
const plugins = [
tablePluginInstance,
tablePlugin(),
imagePlugin(),
marksHelper(),
]
/**
* A change function to standardize inserting images.
*
* @param {Change} change
* @param {String} src
* @param {Range} target
*/
function insertImage(change, src, target) {
if (target) {
change.select(target)
}
change.insertBlock({
type: 'image',
isVoid: true,
data: { src },
})
}
function isImage(url) {
return !!imageExtensions.find(url.endsWith)
}
/**
* A schema to enforce that there's always a paragraph as the last block.
*
* @type {Object}
*/
const schema = {
document: {
last: { types: ['paragraph'] },
normalize: (change, reason, { node, child }) => {
switch (reason) {
case LAST_CHILD_TYPE_INVALID: {
const paragraph = Block.create('paragraph')
return change.insertNodeByKey(node.key, node.nodes.size, paragraph)
}
}
},
},
}
/**
* The class representing my editor.
* @prop {}
*
*/
class MyEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
value: initialValue
}
this.renderEditor = this.renderEditor.bind(this);
this.renderToolbar = this.renderToolbar.bind(this);
this.onChange = this.onChange.bind(this);
}
renderToolbar = () => {
if (this.props.formattedText == undefined) {
return (
<div className="menu toolbar-menu" style={{ height: '30px', backgroundColor: '#EEEEEE' }}>
<span onMouseDown={this.onClickBoldText}>
<span className="glyphicon glyphicon-bold" ></span>
</span>
<span> &nbsp; </span>
<span onMouseDown={this.onClickItalicText}>
<span className="glyphicon glyphicon-italic" ></span>
</span>
<span> &nbsp; | &nbsp; </span>
<span onMouseDown={this.onClickImage}>
<span className="glyphicon glyphicon-picture" ></span>
</span>
<span> &nbsp; | &nbsp; </span>
<DropdownButton
bsStyle={'default'}
bsSize="xsmall"
title={'Table'}
key={1}
id={`dropdown-basic-${1}`}
>
<MenuItem eventKey="1" onMouseDown={this.onClickNewTable}>New</MenuItem>
<MenuItem eventKey="2" onMouseDown={this.onClickDeleteTable} >Delete</MenuItem>
</DropdownButton>
</div >
)
}
}
renderEditor = () => {
return (
<div style={{ height: '80%' }}>
<Editor
plugins={plugins}
placeholder="Enter some text..."
value={this.state.value}
schema={schema}
onChange={this.onChange}
onDrop={this.onDropOrPaste}
onPaste={this.onDropOrPaste}
style={{ overflow: 'auto', height: '100%' }}
readOnly={(this.props.formattedText == undefined) ? false : true}
/>
</div>
)
}
onChange = ({ value }) => {
//localStorage.setItem("content", JSON.stringify(value));
// When the document changes, save the serialized HTML to Local Storage.
//if (value.document != this.state.value.document) {
// desactivate serialization because table support not written yet
// const htmlContent = html.serialize(value)
// localStorage.setItem('formattedHTML', htmlContent)
const plainContent = Plain.serialize(value)
localStorage.setItem('plainText', plainContent)
//Store the text in slatejs format
localStorage.setItem('formattedText', JSON.stringify(value))
if (this.props.formattedText == undefined) {
// a new event is created with slate
this.setState({ value })
} else {
// slate is used to display events
if (this.props.formattedText !== JSON.stringify(value.toJSON())) {
this.setState({ value: Value.fromJSON(JSON.parse(this.props.formattedText)) })
}
}
}
onClickImage = event => {
event.preventDefault()
const src = window.prompt('Enter the URL of the image:')
if (!src) return
const change = this.state.value.change().call(insertImage, src)
this.onChange(change)
}
onClickNewTable = event => {
event.preventDefault()
const change = this.state.value.change().call(tablePluginInstance.changes.insertTable, 2, 2)
this.onChange(change);
}
onClickDeleteTable = event => {
event.preventDefault()
const change = this.state.value.change().call(tablePluginInstance.changes.removeTable)
this.onChange(change);
}
onClickBoldText = event => {
debugger
const change = this.state.value.change().toggleMark('bold');
this.onChange(change);
}
onClickItalicText = event => {
debugger
const change = this.state.value.change().toggleMark('italic');
this.onChange(change);
}
/**
* On drop, insert the image wherever it is dropped.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
onDropOrPaste = (event, change, editor) => {
const target = getEventRange(event, change.value)
if (!target && event.type === 'drop') return
const transfer = getEventTransfer(event)
const { type, text, files } = transfer
if (type === 'files') {
for (const file of files) {
const reader = new FileReader()
const [mime] = file.type.split('/')
if (mime != 'image') continue
reader.addEventListener('load', () => {
editor.change(c => {
c.call(insertImage, reader.result, target)
})
})
reader.readAsDataURL(file)
}
}
if (type === 'text') {
if (!isUrl(text)) return
if (!isImage(text)) return
change.call(insertImage, text, target)
}
}
render() {
if (this.props.formattedText == undefined) {
return (
<div style={{ height: '100%', border: "1px solid", borderColor: "#EEEEEE" }}>
{this.renderToolbar()}
{this.renderEditor()}
</div>)
} else {
return (
<div style={{ height: '100%' }}>
{this.renderToolbar()}
{this.renderEditor()}
</div>)
}
}
}
MyEditor.propType = {
/** formattedText optionnaly pased to the editor. When this prop is passed, the editor is in readonly mode and the editor menu is not visible */
formattedText: propType.string,
}
export default MyEditor;
This diff is collapsed.
......@@ -58,7 +58,7 @@ class Event extends React.Component {
</td>
<td style={{ 'border-style': 'none' }}>
<EventHistoryItem creationDate={creationDate} type={this.props.event.type} item={this.props.event} toggleHistory={this.toggleHistory} toggleHistoryButton={true} expanded={true}> </EventHistoryItem>
<EventHistoryItem creationDate={creationDate} type={this.props.event.type} event={this.props.event} toggleHistory={this.toggleHistory} toggleHistoryButton={true} expanded={true}> </EventHistoryItem>
</td>
</tr>
</tbody>
......
import React from 'react';
import { Panel, Button } from 'react-bootstrap';
import { Col, Row } from 'react-bootstrap';
import PropTypes from 'prop-types';
import _ from 'lodash';
import EventFileViewer from './EventFileViewer.jsx'
import MyEditor from './Editor/MyEditor';
/**
* This class displays a event history item. As such it does not have any knowledge of history.
......@@ -19,6 +22,28 @@ class EventHistoryItem extends React.Component {
this.state = {
expanded: this.props.expanded ? true : false,
}
}
/** Choose the proper viewer and display the event using it.
* @param {Array} content the content of the event to be displayed
*/
displayContent(content) {
let draftjsContent = _.find(content, function (item) {
return item.format === 'draftjs'
})
if (draftjsContent != undefined) {
return (<div> <MyEditor text={draftjsContent.text} /> </div >)
}
let plainContent = _.find(content, function (item) {
return item.format === 'plain'
})
if (plainContent) {
return (<div> {plainContent.text} </div >)
}
}
render() {
......@@ -32,7 +57,7 @@ class EventHistoryItem extends React.Component {
</Col>
<Col xs={6}>
<div className="pull-right">
<span className="glyphicon glyphicon-user"> </span> {this.props.item.username} | &nbsp;
<span className="glyphicon glyphicon-user"> </span> {this.props.event.username} | &nbsp;
<Button className="noPadding" bsStyle="link" onClick={() => this.setState({ expanded: !this.state.expanded })}>
<span className="glyphicon glyphicon-zoom-in"></span>
</Button>
......@@ -46,7 +71,6 @@ class EventHistoryItem extends React.Component {
< Button className="noPadding" bsStyle="link" onClick={() => this.props.toggleHistory()}>
<span className="glyphicon glyphicon-remove-circle noPadding"></span>
</Button > : ""}
</div>
</Col>
</Row>
......@@ -55,11 +79,7 @@ class EventHistoryItem extends React.Component {
<Panel.Body>
<Row>
<Col xs={12}>
{/*<p> {this.props.item.content} </p> */}
{(this.props.item.category === "commandLine" || this.props.item.category === 'error') ? this.props.item.text : ""}
{this.props.item.category === "comment" ? (<div> {this.props.item.text} </div>) : ""}
{this.props.item.category === "file" ? <EventFileViewer event={this.props.item} /> : ""}
{this.displayContent(this.props.event.content)}
</Col>
</Row>
</Panel.Body>
......@@ -70,4 +90,8 @@ class EventHistoryItem extends React.Component {
}
}
EventHistoryItem.propTypes = {
/** the event object to be displayed */
event: PropTypes.object
}
export default EventHistoryItem;
import React from 'react';
import { Grid, Row, Col } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import Event from './Event.jsx';
import NewEvent from './NewEvent.jsx';
import PropTypes from 'prop-types';
import _ from 'lodash';
import Moment from 'moment';
import ListMenu from './ListMenu';
/**
* The list of the all events
*/
class List extends React.Component {
render() {
const { eventList, investigationId } = this.props;
const sessionId = this.props.sessionId;
let path = "/investigation/" + this.props.investigationId + "/events/new";
let sortedEvents = _.sortBy(eventList, [function (o) {
return -1 * Moment(o.creationDate).format("x");
}]);
let events = sortedEvents.map((event) => <Event event={event}> </Event>);
return (
<div>
<ListMenu sessionId={sessionId} investigationId={investigationId} />
<Grid fluid={true}>
<Row>
<Col>
{events}
</Col>
</Row>
</Grid>
</div>
);
}
}
List.propTypes = {
/** the array of unsorted events as provided by the ICAT+ server */
eventList: PropTypes.array,
/** the investigationId the provided events belong to. */
investigationId: PropTypes.string,
}
export default List;
import React from 'react';
import { Button, ButtonToolbar, Glyphicon, Tooltip, OverlayTrigger } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { getPDF } from '../../api/icat/icatPlus';
class ListMenu extends React.Component {
render() {
const { investigationId } = this.props;
const sessionId = this.props.sessionId;
const tooltip = (text) => (
<Tooltip id="tooltip">
{text}
</Tooltip>
);
let path = "/investigation/" + investigationId + "/events/";
return (
<div style={{ marginBottom: "10px", marginTop: "10px", marginLeft: '12px' }} >
<ButtonToolbar>
<OverlayTrigger placement="bottom" overlay={tooltip("Create a new event")}>
<Button bsSize="small" bsStyle="primary" href={path + "new"}>
<Glyphicon glyph="plus" /> New
</Button>
</OverlayTrigger>
<OverlayTrigger placement="bottom" overlay={tooltip("Download the logbook as PDF")}>
<Button bsSize="small" bsStyle="primary" href={getPDF(investigationId, sessionId)} target="_blank">
<Glyphicon glyph="download" style={{ marginRight: "3px" }} /> PDF
</Button>
</OverlayTrigger>
</ButtonToolbar>
</div>
)
}
}
export default ListMenu;
\ No newline at end of file
......@@ -2,98 +2,134 @@
//React components
import React from 'react'
import { Link } from 'react-router-dom';
import { Grid, Row, Col, Button, FormControl } from "react-bootstrap";
import { Redirect } from 'react-router';
import { connect } from 'react-redux';
import MyEditor from './Editor/MyEditor.jsx';
import NewEventUpload from './NewEventUpload.jsx';
import NewEventText from './NewEventText.jsx';
import { postEvents } from '../../actions/Event/index.js'
class NewEvent extends React.Component {
constructor(props) {
super(props)
// this.onClickNewTable = this.onClickNewTable.bind(this);
this.state = {
category: 'none' //default value displayed in the selet button
progress: "waiting" //waiting, uploading, uploaded
}
this.selectCategoryChange = this.selectCategoryChange.bind(this);
this.changeStateToUploaded = this.changeStateToUploaded.bind(this);
}
selectCategoryChange(event) {
this.setState({ category: event.target.value });
postEvents = () => {
let investigationId = this.props.match.params.investigationId;
let newEvent = {
investigationId: investigationId,
//text: localStorage.getItem('plainText'),
//formattedText: localStorage.getItem('formattedText'),
//formattedTextType: "draftjs",
content: [
{
format: "plain",
text: localStorage.getItem('plainText')
},
{
format: "draftjs",
text: localStorage.getItem('draftJSText')
},
{
format: "html",
text: localStorage.getItem('HTMLText')
}
],
creationDate: Date(),
type: 'annotation',
category: 'comment',
title: this.inputTitle.value,
username: this.props.user.username
}
postEvents(newEvent, this.props.user.sessionId, investigationId, this.changeStateToUploaded)
}
changeStateToUploaded() {
this.setState({ progress: "uploaded" })
}
render() {
let investigationId = this.props.match.params.investigationId;
let path = "/investigation/" + investigationId + "/events";
return (
<div style={{
position: "fixed",
top: "0px",
height: "100%",
width: "100%",