Commit 5616310d authored by Alejandro De Maria Antolinos's avatar Alejandro De Maria Antolinos
Browse files

Refactoring, making it simple and coverting to functional the logbook

parent 16afc2bf
import React from 'react';
import React, { useState } from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router';
import { useQuery } from '../../../helpers/hooks';
import { Button, Glyphicon, Label } from 'react-bootstrap';
import {
getOriginalEvent,
getPreviousVersionNumber,
} from '../../../helpers/eventHelpers';
import { getOriginalEvent } from '../../../helpers/eventHelpers';
import TagListInLine from '../Tag/TagListInLine';
import styles from './EventList.module.css';
import EventTextBox from './EventTextBox';
......@@ -13,124 +11,97 @@ import EventTextBox from './EventTextBox';
/** React component which renders an event. Here 'event can be the classical event as found in the logbook but
* could also be a list of event corresponding to a collapsed line containing several events
*/
class Event extends React.Component {
constructor(props) {
super(props);
this.state = {
collapsed: true,
};
this.handleClick = this.handleClick.bind(this);
}
render() {
let events = [this.props.event];
function Event(props) {
let events = [props.event];
const { isReleased } = props;
if (this.props.event.events && !this.state.collapsed) {
events = events.concat(this.props.event.events);
}
const history = useHistory();
const query = useQuery();
const getButtonIcon = (event) => {
if (this.props.logbookContext.isReleased) {
return <Glyphicon glyph="eye-open" />;
}
if (getPreviousVersionNumber(event) === 0) {
return <Glyphicon glyph="pencil" style={{ width: 10 }} />;
}
return <Glyphicon glyph="pencil" style={{ width: 10 }} />;
};
const [collapsed, setCollapsed] = useState(true);
const getTimeComponent = (event) => {
return (
<a
id={event._id}
href={`events?page=${event.meta.page.currentPage}#${event._id}`}
style={{ fontWeight: 'bold' }}
>
{moment(getOriginalEvent(event).creationDate).format(
moment.HTML5_FMT.TIME_SECONDS
)}
</a>
);
};
return events.map((event, index) => (
<tr key={index} style={{ backgroundColor: '#f0f0f6' }}>
<td
style={{ width: 16 }}
className={styles.borderTopSeparatorBetweenEvents}
>
<Button
bsStyle="default"
bsSize="small"
style={{ width: 25, position: 'static', padding: 0 }}
onClick={() => this.props.onEventClicked(event)}
>
{getButtonIcon(event)}
</Button>
</td>
if (props.event.events && !collapsed) {
events = events.concat(props.event.events);
}
<td
className={styles.borderTopSeparatorBetweenEvents}
style={{ width: 16, backgroundColor: 'white' }}
>
{getTimeComponent(event)}
</td>
const getButtonIcon = () => {
if (isReleased) {
return <Glyphicon glyph="eye-open" />;
}
return <Glyphicon glyph="pencil" style={{ width: 10 }} />;
};
<td
className={styles.borderTopSeparatorBetweenEvents}
style={{
paddingBottom: 0,
backgroundColor: 'white',
const getTimeComponent = (event) => {
return (
<a
id={event._id}
href={`events?page=${event.meta.page.currentPage}#${event._id}`}
style={{ fontWeight: 'bold' }}
>
{moment(getOriginalEvent(event).creationDate).format(
moment.HTML5_FMT.TIME_SECONDS
)}
</a>
);
};
return events.map((event, index) => (
<tr key={index} style={{ backgroundColor: '#f0f0f6' }}>
<td
style={{ width: 16 }}
className={styles.borderTopSeparatorBetweenEvents}
>
<Button
bsStyle="default"
bsSize="small"
style={{ width: 25, position: 'static', padding: 0 }}
onClick={() => {
query.set('edit', event._id);
history.push({ search: query.toString() });
}}
>
<div style={{ marginLeft: 5, backgroundColor: 'white' }}>
<EventTextBox event={event} />
{getButtonIcon(event)}
</Button>
</td>
{event.events && this.state.collapsed && (
<Label
style={{
color: 'blue',
backgroundColor: '#f8f8f8',
cursor: 'pointer',
}}
onClick={this.handleClick}
>
.... {event.events.length} command lines more
</Label>
)}
</div>
</td>
<td
className={styles.borderTopSeparatorBetweenEvents}
style={{ width: 50, backgroundColor: 'white' }}
>
<TagListInLine tags={event.tag} />
</td>
</tr>
));
}
<td
className={styles.borderTopSeparatorBetweenEvents}
style={{ width: 16, backgroundColor: 'white' }}
>
{getTimeComponent(event)}
</td>
handleClick() {
this.setState({ collapsed: !this.state.collapsed });
}
<td
className={styles.borderTopSeparatorBetweenEvents}
style={{
paddingBottom: 0,
backgroundColor: 'white',
}}
>
<div style={{ marginLeft: 5, backgroundColor: 'white' }}>
<EventTextBox event={event} />
getUncollapsedEvents() {
return (
<tbody>
{this.props.event.events.map((event) =>
this.getEventContentBody(event)
)}
</tbody>
);
}
{event.events && collapsed && (
<Label
style={{
color: 'blue',
backgroundColor: '#f8f8f8',
cursor: 'pointer',
}}
onClick={setCollapsed(!collapsed)}
>
.... {event.events.length} command lines more
</Label>
)}
</div>
</td>
<td
className={styles.borderTopSeparatorBetweenEvents}
style={{ width: 50, backgroundColor: 'white' }}
>
<TagListInLine tags={event.tag} />
</td>
</tr>
));
}
Event.protypes = {
/** A classical event or a structure representing a collapsed line containing several similar events */
event: PropTypes.object,
/** Context in which the logbook is run */
logbookContext: PropTypes.object,
/** Callback function triggered which the user clicks a link to edit/consult the detailed event */
onEventClicked: PropTypes.func,
};
export default Event;
......@@ -76,7 +76,7 @@ function getItems(events, automaticCollapsing) {
* The list of the all events
*/
function EventList(props) {
const { events } = props;
const { events, isReleased } = props;
if (!events) {
return null;
......@@ -121,7 +121,7 @@ function EventList(props) {
<Event
key={index}
event={event}
logbookContext={props.logbookContext}
isReleased={isReleased}
onEventClicked={props.onEventClicked}
/>
);
......
import React from 'react';
import { useHistory } from 'react-router';
import { useQuery } from '../../helpers/hooks';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import UI from '../../config/ui';
......@@ -24,6 +25,7 @@ const getTotalPageNumber = (eventCount) => {
/* This class handles the display of pages for the logbook */
function LogbookPager(props) {
const history = useHistory();
const query = useQuery();
const { eventCount = 0, isCentered, activePage } = props;
if (getTotalPageNumber(eventCount) <= 1 || eventCount === 0) {
......@@ -50,7 +52,8 @@ function LogbookPager(props) {
subContainerClassName={'pages pagination pagination-sm'}
activeClassName={'active'}
onPageChange={(data) => {
history.push(`${window.location.pathname}?page=${data.selected + 1}`);
query.set('page', data.selected + 1);
history.push({ search: query.toString() });
}}
/>
</div>
......
import PropTypes from 'prop-types';
import { useHistory } from 'react-router';
import { useQuery } from '../../../helpers/hooks';
import React, { useState } from 'react';
import { Nav, Navbar, NavItem, Well } from 'react-bootstrap';
import { ComboSearch } from 'react-combo-search';
......@@ -13,6 +15,9 @@ import SettingLogbookMenuPanel from './SettingLogbookMenuPanel';
* The menu displayed above the event list
*/
function EventListMenu(props) {
const history = useHistory();
const query = useQuery();
const [isNavbarExpanded, setIsNavBarExpanded] = useState(false);
const [isSettingsDisplayed, setIsSettingsDisplayed] = useState(false);
......@@ -24,11 +29,9 @@ function EventListMenu(props) {
sessionId,
getEvents,
periodicdata,
setNewEventVisibility,
logbookContext,
eventCountSinceLastRefresh,
activePage,
searchEvents,
categoryTypes,
automaticCollapsing,
automaticRefresh,
......@@ -42,10 +45,6 @@ function EventListMenu(props) {
setIsNavBarExpanded(!isNavbarExpanded);
};
const setViewSettings = () => {
setIsSettingsDisplayed(!isSettingsDisplayed);
};
const getSettingsTooltip = () => {
return isSettingsDisplayed ? 'Hide settings' : 'Show settings';
};
......@@ -57,16 +56,20 @@ function EventListMenu(props) {
const onSelectNavbar = (eventKey) => {
if (eventKey === 1) {
if (isNewButtonEnabled) {
setNewEventVisibility(NEW_EVENT_VISIBLE);
query.set('edit', NEW_EVENT_VISIBLE);
history.push({ search: query.toString() });
}
}
if (eventKey === 3) {
setViewSettings();
setIsSettingsDisplayed(!isSettingsDisplayed);
}
};
const onSearch = (data) => {
return searchEvents(data);
if (data?.length > 0) {
query.set('search', data[0].search);
history.push({ search: query.toString() });
}
};
return (
......@@ -242,8 +245,6 @@ EventListMenu.propTypes = {
isNewButtonEnabled: PropTypes.bool,
/** Context in which the logbook is run */
logbookContext: PropTypes.object,
/** Callback function which reloads the events based on search criteria*/
searchEvents: PropTypes.func,
/** Selection filter for mongo request (used for PDF request) */
selectionFilter: PropTypes.object,
/** Session identifier */
......
import React from 'react';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { useFetcher } from 'rest-hooks';
import { useHistory } from 'react-router';
import { useQuery } from '../../helpers/hooks';
import Loader from '../Loader';
import {
Button,
Glyphicon,
......@@ -7,6 +10,7 @@ import {
OverlayTrigger,
Panel,
Tooltip,
Alert,
} from 'react-bootstrap';
import TimeAgo from 'react-timeago';
import {
......@@ -17,57 +21,200 @@ import {
LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_HTML_FORMAT,
LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_PLAINTEXT_FORMAT,
} from '../../constants/eventTypes';
import { isEqual } from 'lodash-es';
import TagsSelectorContainer from '../../containers/Logbook/Tags/TagsSelectorContainer';
import { getText } from '../../helpers/eventHelpers';
import EditorWrapper from './Editor/EditorWrapper';
import EventFooter from './EventFooter';
import EventVersions from './EventVersions';
import EventResource from '../../resources/events';
/** Render author and time */
const AuthorAndTime = (props) => {
if (props.event) {
return (
<h5 style={{ marginTop: 0, marginBottom: 0 }}>
<small style={{ color: 'white' }}>
{' '}
<em>
@{props.event.username} -{' '}
<TimeAgo date={props.event.creationDate} />
<OverlayTrigger
trigger="click"
placement="bottom"
overlay={EventVersions(props.event)}
rootClose
>
<div style={{ display: 'inline-block' }}>
<Button bsStyle="link" bsSize="xs">
{' '}
<Glyphicon glyph="cog" style={{ color: 'white' }} />{' '}
</Button>
</div>
</OverlayTrigger>
</em>{' '}
</small>
</h5>
);
}
return null;
};
/**
*
* React component which renders a Panel for creating of editing an event
*/
class NewOrEditEventPanel extends React.Component {
constructor(props) {
super(props);
function NewOrEditEventPanel(props) {
const { event, user, investigationId } = props;
this.state = {
isSaveButtonEnabled: false, //whether the editor contains some text or not
isEditorContentValid: false,
optionPanelDecollaped: false,
selectedTags: [], // tags which are currently selected
};
const history = useHistory();
const query = useQuery();
this.isSaveButtonEnabled = this.isSaveButtonEnabled.bind(this);
this.onCancelButtonClicked = this.onCancelButtonClicked.bind(this);
this.onEventContentChanged = this.onEventContentChanged.bind(this);
this.onSaveButtonClicked = this.onSaveButtonClicked.bind(this);
this.onTagSelectionChanged = this.onTagSelectionChanged.bind(this);
}
/** State */
const [selectedTags, setSelectedTags] = useState([]);
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
render() {
const { event, user, investigationId } = this.props;
/* Fetchers */
const createEvent = useFetcher(EventResource.createShape());
const updateEvent = useFetcher(EventResource.updateShape());
return (
/* Not sure if we should kept this code, now it is useless but it might be needed **/
const onEventContentChanged = () => {
let isEditorContentValid = false;
if (event) {
//editing an event
const currentTextInEditionMode = localStorage.getItem(
LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_PLAINTEXT_FORMAT + event._id
);
isEditorContentValid =
currentTextInEditionMode !== '' &&
currentTextInEditionMode !== getText(event.content, 'plainText');
} else {
//creating an event
const currentTextInCreationMode = localStorage.getItem(
LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_PLAINTEXT_FORMAT
);
isEditorContentValid = currentTextInCreationMode !== '';
}
return isEditorContentValid;
};
/**
* Callback function triggered when the user click the save button while an event is being created or updated.
*/
const onSaveButtonClicked = async () => {
try {
setLoading(true);
const currentTagIds = selectedTags.map((tag) => tag._id);
if (event) {
await updateEvent(
{ investigationId },
{
_id: event._id,
category: event.category,
content: [
{
format: 'plainText',
text: localStorage.getItem(
LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_PLAINTEXT_FORMAT +
event._id
),
},
{
format: 'html',
text: localStorage.getItem(
LOCALSTORAGE_KEY_EDITED_EVENT_CONTENT_IN_HTML_FORMAT +
event._id
),
},
],
creationDate: Date(),
type: event.type,
tag: currentTagIds,
title: null,
previousVersionEvent: event._id,
}
);
} else {
// creation of an event
await createEvent(
{ investigationId },
{
category: EVENT_CATEGORY_COMMENT,
content: [
{
format: 'plainText',
text: localStorage.getItem(
LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_PLAINTEXT_FORMAT
),
},
{
format: 'html',
text: localStorage.getItem(
LOCALSTORAGE_KEY_NEW_EVENT_CONTENT_IN_HTML_FORMAT
),
},
],
creationDate: Date(),
investigationId,
title: null,
tag: currentTagIds,
type: ANNOTATION,
}
);
}
query.delete('edit');
history.push({ search: query.toString() });
} catch (e) {
setError(`${e.status}:${e.message}`);
}
};
const onCancelButtonClicked = () => {
localStorage.removeItem('plainText');
localStorage.removeItem('HTMLText');
query.delete('edit');
history.push({ search: query.toString() });
};
return (
<>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: '0 0 33px' }}>
<Panel bsStyle="primary" style={{ marginBottom: 0 }}>
<Panel.Heading style={{ padding: '5px 15px' }}>
<b>{this.props.event ? 'Edit' : 'New comment'}</b>
<b>{event ? 'Edit' : 'New comment'}</b>
<div className="pull-right">
<AuthorAndTime event={event} />
</div>
</Panel.Heading>
</Panel>
</div>
{error && (
<div style={{ padding: 2 }}>
<Alert bsStyle="danger">
<strong>Oops</strong> {error}
</Alert>
</div>
)}
<div style={{ flex: '2 2 70%' }}>
<EditorWrapper
event={event}
investigationId={investigationId}
onContentChanged={this.onEventContentChanged}
user={user}
/>
{loading && (
<>
<br />
<Loader message="Saving..."></Loader>
</>
)}
{!loading && (
<EditorWrapper
event={event}
investigationId={investigationId}
onContentChanged={onEventContentChanged}
user={user}
/>
)}
</div>
<div style={{ flex: '1 1 32px', padding: '5px 10px 0 10px' }}>
......@@ -88,7 +235,7 @@ class NewOrEditEventPanel extends React.Component {
}
>
<a
href={`/investigation/${this.props.investigationId}/events/tagManager`}
href={`/investigation/${investigationId}/events/tagManager`}
target="_blank"
rel="noopener noreferrer"
>
......@@ -100,9 +247,9 @@ class NewOrEditEventPanel extends React.Component {
</div>
<div style={{ flex: '1 1 100px' }}>
<TagsSelectorContainer
investigationId={this.props.investigationId}
onChange={this.onTagSelectionChanged}
tags={this