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

Work on #512

parent 103e6313
Pipeline #51131 failed with stage
in 2 minutes and 50 seconds
......@@ -15,10 +15,10 @@ import UserManagementPage from './containers/UserManagementPage';
import CalendarPage from './containers/Calendar/CalendarPage';
import SampleTrackingStatsPage from './containers/Stats/SampleTracking/SampleTrackingStatsPage';
import SearchPage from './containers/SearchPage';
import OpenDataPage from './containers/OpenData/OpenDataPage';
import OpenDataPage from './containers/Logbook/Tags/TagsPanel';
import ClosedDataPage from './containers/ClosedData/ClosedDataPage';
import DatasetsPage from './containers/DatasetsPage';
import EventTagPage from './containers/EventTagPage';
import TagPage from './containers/TagPage';
import EventsPage from './containers/EventsPage';
import InstrumentLogbookPage from './containers/InstrumentLogbookPage';
import SelectionPage from './containers/Selection/SelectionPage';
......@@ -155,6 +155,12 @@ function App() {
</LoadingBoundary>
</Route>
<Route path="/tag">
<LoadingBoundary message="Loading tags..." spacedOut>
<TagPage />
</LoadingBoundary>
</Route>
<Route path="/investigation">
<LoadingBoundary message="Loading investigation..." spacedOut>
<Route
......@@ -170,10 +176,6 @@ function App() {
}
/>
<Route path="/investigation/:investigationId/events/tagManager">
<EventTagPage />
</Route>
<Route exact path="/investigation/:investigationId/events">
<EventsPage />
</Route>
......
import ICATPLUS from '../../config/icatPlus';
import { getURLParamsByDictionary } from '../../helpers/url';
/**
* Get URL used to update an event on a given investigation on ICAT+
* @param {String} sessionId the session identifier
......@@ -19,16 +19,17 @@ export function getEventURL(
instrumentName,
filterInvestigation
) {
const params = new URLSearchParams();
if (investigationId) params.set('investigationId', investigationId);
if (limit) params.set('limit', limit);
if (sortBy) params.set('sortBy', sortBy);
if (sortOrder) params.set('sortOrder', sortOrder);
if (types) params.set('types', types);
if (skip) params.set('skip', skip);
if (format) params.set('format', format);
if (search) params.set('search', search);
if (instrumentName) params.set('instrumentName', instrumentName);
const params = getURLParamsByDictionary({
investigationId,
limit,
sortBy,
sortOrder,
types,
skip,
format,
search,
instrumentName,
});
params.set('filterInvestigation', filterInvestigation);
return `${ICATPLUS.server}/logbook/${sessionId}/event?${params.toString()}`;
}
......@@ -61,18 +62,14 @@ export function getCountLogbookStatisticsURL(
* @param {string} sessionId session identifier
* @param {*} investigationId investigation identifier
*/
export function getTagsByInvestigationId(sessionId, investigationId) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/tag`;
}
/** Create a given tag associated to a given investigation */
export function createTagsByInvestigationId(sessionId) {
return `${ICATPLUS.server}/logbook/${sessionId}/tag/create`;
}
export function getTagURL(sessionId, investigationId, instrumentName, id) {
const params = getURLParamsByDictionary({
investigationId,
instrumentName,
id,
});
/** Update a given tag associated to a given investigation */
export function updateTagsByInvestigationId(sessionId, investigationId, tagId) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/tag/id/${tagId}/tag/update`;
return `${ICATPLUS.server}/logbook/${sessionId}/tag?${params.toString()}`;
}
export function createEventFromBase64(sessionId, investigationId) {
......@@ -81,7 +78,7 @@ export function createEventFromBase64(sessionId, investigationId) {
/** Get the statistics of the logbook usage
* @param {string} sessionId session identifier
* @param {string} startDate the startDate of the period
* @param {string} startDate the startDate of the periodr
* @param {string} endDate endDate of the period
*/
export function getStats(sessionId, startDate, endDate) {
......
import React, { useState } from 'react';
import { useFetcher } from 'rest-hooks';
import { useHistory } from 'react-router';
import { useQuery } from '../../helpers/hooks';
import { useQuery, useURLParams } from '../../helpers/hooks';
import Loader from '../Loader';
import {
Button,
......@@ -69,6 +69,11 @@ function NewOrEditEventPanel(props) {
const history = useHistory();
const query = useQuery();
/* URL to the TAG Manager page */
const tagPageURL = `/tag?${useURLParams({
investigationId,
instrumentName,
})}`;
/** State */
const [selectedTags, setSelectedTags] = useState([]);
const [error, setError] = useState();
......@@ -237,7 +242,7 @@ function NewOrEditEventPanel(props) {
}
>
<a
href={`/investigation/${investigationId}/events/tagManager`}
href={tagPageURL}
target="_blank"
rel="noopener noreferrer"
>
......
import React, { useState } from 'react';
import {
Alert,
Button,
Col,
Form,
Glyphicon,
Grid,
Modal,
Panel,
Row,
} from 'react-bootstrap';
import TextFieldGroup from '../../Form/TextFieldGroup';
import { useForm, FormProvider } from 'react-hook-form';
function TagFormModal(props) {
const { tag, instrumentName, investigationId } = props;
const { onSubmitAsync, onCloseModal } = props;
const methods = useForm({ defaultValues: tag });
const { handleSubmit, register, formState } = methods;
console.log(tag);
const { isSubmitting } = formState;
const [submitError, setSubmitError] = useState();
async function handleFormSubmit(values) {
try {
setSubmitError(undefined);
await onSubmitAsync(values);
} catch (error) {
setSubmitError(
`An error occurred while ${
tag ? 'saving' : 'creating'
} the address: ${error}`
);
}
}
const getBsStyleByTagType = (tag) => {
if (!tag && investigationId) return 'success';
if (!tag && instrumentName) return 'warning';
if (tag.investigationId) return 'success';
if (tag.instrumentName) return 'warning';
return 'danger';
};
return (
<Modal
show
backdrop="static"
bsSize="large"
aria-labelledby="address-modal-title"
onHide={onCloseModal}
>
<Modal.Header closeButton>
<Modal.Title id="address-modal-title">
{tag ? 'Edit' : 'Create'} tag
</Modal.Title>
</Modal.Header>
<Modal.Body>
{submitError && <Alert bsStyle="danger">{submitError}</Alert>}
<Form>
<FormProvider {...methods}>
{tag && <input name="_id" type="hidden" ref={register} />}
<Panel bsStyle={getBsStyleByTagType(tag)}>
<Panel.Heading>
<Glyphicon glyph="tag" /> Tag
</Panel.Heading>
<Panel.Body>
<Grid fluid>
<Row>
<Col xs={12} md={3}>
<TextFieldGroup
registerOptions={{ required: true }}
name="name"
label="Label"
type="text"
//initialValue={tag ? tag.name : ''}
/>
</Col>
</Row>
</Grid>
</Panel.Body>
</Panel>
</FormProvider>
</Form>
</Modal.Body>
<Modal.Footer>
<Button bsStyle="link" onClick={onCloseModal}>
Cancel
</Button>
<Button
bsStyle="success"
disabled={isSubmitting}
onClick={handleSubmit(handleFormSubmit)}
>
{`${tag ? 'Sav' : 'Creat'}${isSubmitting ? 'ing...' : 'e'}`}
</Button>
</Modal.Footer>
</Modal>
);
}
export default TagFormModal;
import React from 'react';
import { Glyphicon, Button, Grid, Row, Col, Label } from 'react-bootstrap';
import styles from '../../Address/MyAddressesSummary.module.css';
const isEditionDisabled = (tag, isAdministrator, isInstrumentScientist) => {
if (isAdministrator) return false;
if (isInstrumentScientist && tag.instrumentName) return false;
if (tag.investigationId) return false;
return true;
};
function TagPanel(props) {
const {
tag,
onEdit,
onDelete,
isAdministrator,
isInstrumentScientist,
} = props;
return (
<Grid>
<Row>
<Col xs={3}>
<div className={styles.row}>
<Glyphicon
className={styles.glyph}
glyph="tag"
style={{ color: tag.color, margin: '5px' }}
/>
{tag.name.toUpperCase()}
</div>
</Col>
<Col xs={1}></Col>
<Col xs={3}>
{!isEditionDisabled(tag, isAdministrator, isInstrumentScientist) ? (
<>
<Button
bsSize="xsmall"
bsStyle="primary"
aria-label="Edit"
onClick={() => {
onEdit(tag);
}}
>
<Glyphicon glyph="pencil" />
<span className={styles.actionLabel}> Edit</span>
</Button>
<Button
style={{ margin: '0px 0px 0px 5px' }}
bsSize="xsmall"
bsStyle="danger"
disabled
aria-label="Remove"
onClick={() => onDelete(tag)}
>
<Glyphicon glyph="trash" />
<span className={styles.actionLabel}> Remove</span>
</Button>
</>
) : (
<Label>PROTECTED</Label>
)}
</Col>
</Row>
</Grid>
);
}
export default TagPanel;
......@@ -12,6 +12,7 @@ class TagSelector extends React.Component {
this.getCustomStyles = this.getCustomStyles.bind(this);
}
render() {
debugger;
const options = this.props.availableTags
? this.props.availableTags.map((tag) => ({
value: tag._id,
......
import React from 'react';
import TagsManagerContainer from './Logbook/Tags/TagsManagerContainer';
import { useParams } from 'react-router';
function EventTagPage() {
const { investigationId } = useParams();
return <TagsManagerContainer investigationId={investigationId} />;
}
export default EventTagPage;
......@@ -4,7 +4,7 @@ import { Col, Grid, Row, Alert } from 'react-bootstrap';
import { useParams } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import moment from 'moment';
import { useScrollToHash, useQuery } from '../helpers/hooks';
import { useScrollToHash, useQueryParams } from '../helpers/hooks';
import TabContainerMenu from '../components/TabContainerMenu/TabContainerMenu';
import PageNotFound from './PageNotFound';
import InvestigationResource from '../resources/investigation';
......@@ -26,11 +26,8 @@ function EventsPage() {
const { investigationId } = useParams();
const user = useSelector((state) => state.user);
const { sessionId } = user;
const query = useQuery();
const edit = query.get('edit');
const search = query.get('search');
const page = parseInt(query.get('page')) || 1;
const { edit, search, page = 1 } = useQueryParams();
const { categoryTypes } = useSelector((state) => state.logbook);
const investigation = useResource(InvestigationResource.detailShape(), {
......
import axios from 'axios';
import { cloneDeep, isEqual } from 'lodash-es';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Col, Grid, Row } from 'react-bootstrap';
import { connect } from 'react-redux';
import {
createTagsByInvestigationId,
getTagsByInvestigationId,
updateTagsByInvestigationId,
} from '../../../api/icat-plus/logbook';
import Loader from '../../../components/Loader';
import NewOrEditTagPanel from '../../../components/Logbook/Tag/NewOrEditTagPanel';
import TagListMenu from '../../../components/Logbook/Tag/TagListMenu';
import TagListPanel from '../../../components/Logbook/Tag/TagListPanel';
import UserMessage from '../../../components/UserMessage';
/**
* Container which holds the logic to list, create and edit tag.
*/
class TagsManagerContainer extends Component {
constructor(props) {
super(props);
this.state = {
availableTags: [],
errorMessge: null,
isEditingOrCreatingTag: false, // false when listing tags. True when editing or creacting tag
isFetching: true,
isFetched: false,
tag: null, // tag being edited, null otherwise
userMessage: null, // an object which structure is { type: XXX, text:ZZZ }
};
this.createTag = this.createTag.bind(this);
this.getTags = this.getTags.bind(this);
this.onEditTagButtonClicked = this.onEditTagButtonClicked.bind(this);
this.onNewTagButtonClicked = this.onNewTagButtonClicked.bind(this);
this.onSaveButtonClicked = this.onSaveButtonClicked.bind(this);
this.requestTags = this.requestTags.bind(this);
this.updateTag = this.updateTag.bind(this);
}
render() {
if (this.state.isFetching) {
return <Loader message="Loading tags..." spacedOut />;
}
if (this.state.isEditingOrCreatingTag) {
return (
<WrapperWithUserMessage userMessage={this.state.userMessage}>
<NewOrEditTagPanel
onSaveButtonClicked={this.onSaveButtonClicked}
investigationId={this.props.investigationId}
panelHeaderText={this.state.tag ? 'Edit tag' : 'New tag'}
onCancelButtonClicked={() =>
this.setState({ isEditingOrCreatingTag: false })
}
tag={this.state.tag}
/>
</WrapperWithUserMessage>
);
}
if (this.state.availableTags.length === 0) {
return (
<WrapperWithUserMessage userMessage={this.state.userMessage}>
<NoTagGrid />
<TagListMenu
onNewTagButtonClicked={this.onNewTagButtonClicked}
logbookContext={this.props.logbookContext}
/>
</WrapperWithUserMessage>
);
}
return (
<WrapperWithUserMessage userMessage={this.state.userMessage}>
<TagListMenu
onNewTagButtonClicked={this.onNewTagButtonClicked}
logbookContext={this.props.logbookContext}
/>
<TagListPanel
availableTags={this.state.availableTags}
isTagEditionEnabled
onEditButtonClicked={this.onEditTagButtonClicked}
/>
</WrapperWithUserMessage>
);
}
componentDidMount() {
this.requestTags().then((tags) => this.setState({ availableTags: tags }));
}
/**
* Function Triggered when the user clicks the New button in the menu
*/
onNewTagButtonClicked() {
this.setState({ isEditingOrCreatingTag: true, userMessage: null });
}
/**
* Function triggered when the use clicks on the EDit button for a given tag
* @param {*} tag tag to be edited
*/
onEditTagButtonClicked(tag) {
if (tag) {
this.setState({ isEditingOrCreatingTag: true, tag, userMessage: null });
}
}
/**
* Function triggered when the user clicks on the save button during tag creation or edition
* @param {object} tag tag to be created or edited
*/
onSaveButtonClicked(tag) {
if (this.state.tag) {
// the tag is to be edited
this.updateTag(tag)
.then((tag) => {
const availableTags = cloneDeep(this.state.availableTags);
const tagToBeUpdatedIndex = availableTags.findIndex((availableTag) =>
isEqual(availableTag._id, tag._id)
);
availableTags.splice(tagToBeUpdatedIndex, 1, tag);
this.setState({
availableTags,
isEditingOrCreatingTag: false,
tag: null,
userMessage: { type: 'info', text: 'Tag was successfully updated' },
});
})
.catch(() => {
const userMessage = {
type: 'error',
text: 'Error. The tag was not edited.',
};
this.setState({ userMessage });
});
} else {
// the tag is to be created
this.createTag(tag)
.then(() => this.requestTags())
.then((tags) => {
this.setState({
availableTags: tags,
isEditingOrCreatingTag: false,
userMessage: { type: 'info', text: 'Tag was successfully created' },
});
})
.catch(() => {
const userMessage = {
type: 'error',
text: 'Error. The tag was not created.',
};
this.setState({ userMessage });
});
}
}
/**
* Request available tags. This function does not perform the axios call itself. The latter is isolated in the function getTags()
*/
requestTags() {
return new Promise((resolve, reject) => {
if (this.props.investigationId && this.props.user.sessionId) {
this.setState({ isFetching: true, errorMessage: null });
this.getTags()
.then((response) => {
this.setState({ isFetching: false, isFetched: true });
resolve(response.data.tags);
})
.catch((error) => {
this.setState({
isFetching: false,
isFetched: false,
userMessage: {
type: 'error',
text: 'An error occured while retrieving the tag list.',
},
});
reject(error);
});
} else {
console.log('requestTags() Missing props');
this.setState({
availableTags: [],
isFetching: false,
isFetched: false,
});
reject();
}
});
}
/**
* Get available tags
* @returns {Promise}
*/
getTags() {
return axios({
method: 'get',
url: getTagsByInvestigationId(
this.props.user.sessionId,
this.props.investigationId
),
});
}
/**
* Create a tag
* @param {object} tag tag to be created
* @returns {Promise}
*/
createTag(tag) {
if (tag) {
return new Promise((resolve, reject) => {
axios({
method: 'post',
url: createTagsByInvestigationId(
this.props.user.sessionId,
this.props.investigationId
),
data: tag,
})
.then((data) => resolve(data.data))
.catch(() => {
reject();
});
});
}
}
/**
* Update a tag
* @param {object} tag tag to be updated
* @returns {Promise}