# containers/EventContainer.js

## Maintainability

<<<<<<< HEAD:public/report/files/src_containers_EventContainer_js/index.html

65.65

## Lines of code

616

=======

65.76

## Lines of code

615

>>>>>>> d2931edc408681c85895ae016cbd29c396391a41:public/report/files/containers_EventContainer_js/index.html

## Difficulty

<<<<<<< HEAD:public/report/files/src_containers_EventContainer_js/index.html

68.50

## Estimated Errors

5.10

=======

66.00

## Estimated Errors

5.12

>>>>>>> d2931edc408681c85895ae016cbd29c396391a41:public/report/files/containers_EventContainer_js/index.html

## Function weight

### By Complexity

### By SLOC

import React from 'react'; import axios from 'axios'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { getEventsByInvestigationId, getEventCountByInvestigationId, createEvent, updateEvent } from '../api/icat/icatPlus.js'; import _ from 'lodash'; import EventList from '../components/Event/EventList'; import EventActionBar from '../components/Event/EventActionBar.js'; import NewEvent from '../components/Event/NewEvent.js'; import { NEW_EVENT_INVISIBLE, NEW_EVENT_VISIBLE, DOC_VIEW, LIST_VIEW, ERROR_MESSAGE_TYPE } from '../constants/EventTypes.js'; import EventPager from '../components/Event/EventPager.js'; import { convertPageToPageEventIndexes, isRangeAvailableOnTheClient } from '../helpers/PaginationHelper.js'; import { GUI_CONFIG } from '../config/gui.config.js'; import StringConverterForRegexp from 'escape-string-regexp'; import Loading from '../components/Loading.js'; import UserMessage from '../components/UserMessage.js'; /** * This class represents the event container component. It's role is to retrieve events from the server asynchronuously. * Also, it is connected to redux and retrieves user data and sessionId. */ class EventContainer extends React.Component { constructor(props) { super(props); this.state = { activePage: 1, foundEventCount: 0, // count of events found by the server downloadedEvents: [], //events already downloaded errorMessage: "", eventReceptionStatus: "idle", //idle, fetched, countFetched, allFetched; Used to handle asynchronous data retrieval from the server eventCountReceptionStatus: "idle", //idle, fetched, countFetched, allFetched; Used to handle asynchronous data retrieval from the server isNewEventVisible: false, pageEvents: [], // events currently displayed on the page selectedEventId: 0, selectionFilter: getSelectionFilterBySearchCriteria([]), sortingFilter: GUI_CONFIG().DEFAULT_SORTING_FILTER, view: LIST_VIEW, } this.createEvent = this.createEvent.bind(this); this.downloadEvents = this.downloadEvents.bind(this); this.isAppProperlyConfigured = this.isAppProperlyConfigured.bind(this); this.onEventsReceptionFailure = this.onEventsReceptionFailure.bind(this); this.onEventUpdated = this.onEventUpdated.bind(this); this.onNewEventUploaded = this.onNewEventUploaded.bind(this); this.onPageClicked = this.onPageClicked.bind(this); this.reverseEventsSortingByCreationDate = this.reverseEventsSortingByCreationDate.bind(this); this.searchEvents = this.searchEvents.bind(this); this.setNewEventVisibility = this.setNewEventVisibility.bind(this); this.setView = this.setView.bind(this); this.updateEvent = this.updateEvent.bind(this); } render() { const { investigationId, user } = this.props; function isDataFetched(parentThis) { let _this = parentThis; if (_this.state.eventReceptionStatus === "fetched" && _this.state.eventCountReceptionStatus === "fetched") { return true; } return false; } if (this.isAppProperlyConfigured()) { return ( <div style={{ marginBottom: '40px' }}> <UserMessage type={ERROR_MESSAGE_TYPE} message={this.state.errorMessage} /> <EventActionBar investigationId={investigationId} isNewEventVisible={this.state.isNewEventVisible} numberOfMatchingEventsFound={this.state.foundEventCount} reverseEventsSortingByCreationDate={this.reverseEventsSortingByCreationDate} searchEvents={this.searchEvents} sessionId={user.sessionId} setNewEventVisibility={this.setNewEventVisibility} setView={this.setView} sortingFilter={this.state.sortingFilter} view={this.state.view} /> <NewEvent createEvent={this.createEvent} isVisible={this.state.isNewEventVisible} investigationId={investigationId} onNewEventUploaded={this.onNewEventUploaded} setNewEventVisibility={this.setNewEventVisibility} user={user} /> {(!isDataFetched(this)) ? <Loading message="Loading logbook"></Loading> : <div> <EventPager activePage={this.state.activePage} eventCount={this.state.foundEventCount} isFloatingRight={true} onPageClicked={this.onPageClicked} /> <EventList events={this.state.pageEvents} investigationId={investigationId} onEventUpdated={this.onEventUpdated} reloadEvents={this.downloadEvents} reverseEventsSortingByCreationDate={this.reverseEventsSortingByCreationDate} selectedEventId={this.state.selectedEventId} updateEvent={this.updateEvent} user={user} view={this.state.view} /> <EventPager activePage={this.state.activePage} eventCount={this.state.foundEventCount} onPageClicked={this.onPageClicked} /> </div>} </div > ) } else { return (<div style={{ marginBottom: '40px' }}> <UserMessage type={ERROR_MESSAGE_TYPE} message={"Sorry, the application is not properly configured for wall-eLog. Can not start. Check the console."} /> </div> ) } } /** * Executed the first time the component is mounted */ componentWillMount() { this.downloadEvents(0, getSelectionFilterBySearchCriteria([]), true); } /** Callback function triggered when the user starts a new search * @param { Array } searchCriteria the search criteria as provided by the search component */ searchEvents(searchCriteria) { this.downloadEvents(0, getSelectionFilterBySearchCriteria(searchCriteria), true); } /** * Callback function which refreshes the event list because the events the user want to see have not been downloaded yet * @param {integer} offset the offset * @param {object} sortingFilter the sorting filter to use , optional * @param {Object} selectionFilter optional; selection filter in mongodb format * @param {bool} isEventCountRequestNeeded whether the event count request is needed or not */ downloadEvents(offset, selectionFilter, isEventCountRequestNeeded, sortingFilter) { const { investigationId, user } = this.props; sortingFilter = sortingFilter || GUI_CONFIG().DEFAULT_SORTING_FILTER; if (!selectionFilter) { // there is not selectionfilter as parameter when the user has clicked on a page selectionFilter = this.state.selectionFilter; } this.getEventsByInvestigationId(investigationId, user.sessionId, GUI_CONFIG().EVENTS_PER_DOWNLOAD, offset, selectionFilter, sortingFilter, this.onEventsReceptionFailure); this.setState({ eventReceptionStatus: "fetching", isNewEventVisible: false }); if (isEventCountRequestNeeded) { this.getEventCountByInvestigationId(investigationId, user.sessionId, selectionFilter, this.onEventsReceptionFailure); this.setState({ eventCountReceptionStatus: "fetching" }); } } /** * Callback function triggered when event data retreval failed * @param {*} error */ onEventsReceptionFailure(error) { this.setState({ errorMessage: "An error occured while retrieving the events", events: [], eventCount: 0, eventReceptionStatus: "idle", eventCountReceptionStatus: "idle", }) } /** * Change the current view to the requested view * @param {string} view the requested view */ setView(view) { if (view && view !== this.state.view) { if (view === DOC_VIEW) { this.downloadEvents(0, getSelectionFilterForAllComments(), true); } if (view === LIST_VIEW) { this.downloadEvents(0, getSelectionFilterBySearchCriteria(), true); } this.setState({ view: view }) } } /** * Set the visibility of the new event panel * @param {string} visibility the requested visibility */ setNewEventVisibility(visibility) { if (visibility) { if (visibility === NEW_EVENT_VISIBLE) { this.setState({ isNewEventVisible: true }) } else if (visibility === NEW_EVENT_INVISIBLE) { this.setState({ isNewEventVisible: false }) } } } /** * Request events associated to the given investigationId and selectionFilter * @param {integer} investigationId the investigation indentifier * @param {String} sessionId the session identifier * @param {integer} limit the total number of events requested * @param {integer} offset the offset value of events from all events found by the DB (used for pagination) * @param {Object} selectionFilter the selection filter (DB syntax). Example: {"$and":[{"$or":[{"type":"annotation"},{"type":"notification"}]}]} * @param {Object} sortingFilter the sorting filter (DB synthax). Example: {"createdAt":-1} * @param {requestCallback} onError the callback function executed when an error occured during the request. */ getEventsByInvestigationId(investigationId, sessionId, limit, offset, selectionFilter, sortingFilter, onError) { if (investigationId && sessionId) { axios({ method: 'post', <<<<<<< HEAD:public/report/files/src_containers_EventContainer_js/index.html url: URL.getEventsByInvestigationId(sessionId, investigationId), ======= url: getEventsByInvestigationId(sessionId, investigationId), >>>>>>> d2931edc408681c85895ae016cbd29c396391a41:public/report/files/containers_EventContainer_js/index.html data: { find: selectionFilter, sort: sortingFilter, limit: limit, skip: offset } }) .then(data => { this.setState({ downloadedEvents: data.data, pageEvents: _.slice(data.data, 0, GUI_CONFIG().EVENTS_PER_PAGE), downloadedEventIndexes: { start: offset, end: offset + data.data.length - 1 }, selectionFilter: selectionFilter, sortingFilter: sortingFilter, eventReceptionStatus: "fetched", }); }) .catch((error) => { console.log("[ERROR] An error occured while retrieving the event list.") onError(error) }) } else { onError("[ERROR] Missing query parameters in the request handled by getEventsByInvestigationId()") } } /** * Request total number of events associated to the given investigationId * @param {string} investigationId the investigation id * @param {string} sessionId the session id * @param {object} selectionFilter the selection filter * @param {func} onError the callback function executed when an error occured during the request. */ getEventCountByInvestigationId(investigationId, sessionId, selectionFilter, onError) { if (investigationId && sessionId) { axios({ method: 'post', <<<<<<< HEAD:public/report/files/src_containers_EventContainer_js/index.html url: URL.getEventCountByInvestigationId(sessionId, investigationId), ======= url: getEventCountByInvestigationId(sessionId, investigationId), >>>>>>> d2931edc408681c85895ae016cbd29c396391a41:public/report/files/containers_EventContainer_js/index.html data: { find: selectionFilter } }) .then(data => { this.setState({ foundEventCount: parseInt(data.data, 10), eventCountReceptionStatus: "fetched" }); }) .catch((error) => { console.log("[ERROR] An error occured while retrieving the event count."); onError(error) }) } else { onError("[ERROR] Missing query parameters in the request handled by getEventCountByInvestigationId()") } } /** * Callback function triggered when a new event has been successfully uploaded */ onNewEventUploaded() { this.setState({ isNewEventVisible: false, activePage: 1, }); if (this.state.view === LIST_VIEW) { this.downloadEvents(0, getSelectionFilterBySearchCriteria(), true); } if (this.state.view === DOC_VIEW) { this.downloadEvents(0, getSelectionFilterForAllComments(), true); } } /** * Callback function triggered when the user clicks on a page link * @param {integer} page the page the user has clicked */ onPageClicked(page) { if (page) { let pageEventIndexes = convertPageToPageEventIndexes(page, GUI_CONFIG().EVENTS_PER_PAGE, this.state.foundEventCount); let areEventsAlreadyDownloaded = isRangeAvailableOnTheClient(pageEventIndexes, this.state.downloadedEventIndexes); if (areEventsAlreadyDownloaded) { this.setState({ pageEvents: _.slice(this.state.downloadedEvents, pageEventIndexes.start - this.state.downloadedEventIndexes.start, pageEventIndexes.end - this.state.downloadedEventIndexes.start + 1) }); } else { this.downloadEvents(pageEventIndexes.start, false, false, this.state.sortingFilter); } this.setState({ activePage: page, selectedEventId: 0 }); } } /** * Callback function which reverses the sorting of events by creation date * @param {string} selectedEventId the id of the selected event */ reverseEventsSortingByCreationDate(selectedEventId) { let newSortingFilter = {}; /* temporary fix , in the future we will use creationDate instead of createdAt*/ newSortingFilter.createdAt = this.state.sortingFilter.createdAt * (-1); if (selectedEventId !== undefined) { // An event is selected, sort and show the page where the selected event is let indexOfEventOnThePage = _.findIndex(this.state.pageEvents, function (event) { return event._id === selectedEventId; }); if (indexOfEventOnThePage !== -1) { let indexOfEventOnAllEventsFound = indexOfEventOnThePage + (this.state.activePage - 1) * GUI_CONFIG().EVENTS_PER_PAGE; let rankInFutureList = this.state.foundEventCount - indexOfEventOnAllEventsFound; let newPage = Math.ceil(rankInFutureList / GUI_CONFIG().EVENTS_PER_PAGE); let offset = (newPage - 1) * GUI_CONFIG().EVENTS_PER_PAGE; this.downloadEvents(offset, this.state.selectionFilter, false, newSortingFilter); this.setState({ activePage: newPage, selectedEventId: selectedEventId }); return; } } this.downloadEvents(0, this.state.selectionFilter, false, newSortingFilter); this.setState({ activePage: 1, selectedEventId: 0 }); } /** * Checks whether the app is properly configured for wall-eLog * @returns {boolean} true when the app is properly configured */ isAppProperlyConfigured() { if (!GUI_CONFIG()) { console.log('[ERROR] [CONFIG] File gui.config.js is missing'); return false; } if (!GUI_CONFIG().DEFAULT_SORTING_FILTER) { console.log('[ERROR] [CONFIG] DEFAULT_SORTING_FILTER object is missing'); return false; } /* temporary workaround. We will need to use creationDate in fact in the future */ if (!GUI_CONFIG().DEFAULT_SORTING_FILTER.createdAt) { console.log('[ERROR] [CONFIG] DEFAULT_SORTING_FILTER.creationDate value is missing'); return false; } if (!GUI_CONFIG().EVENTS_PER_PAGE) { console.log('[ERROR] [CONFIG] EVENTS_PER_PAGE value is missing'); return false; } return true; } /** Callback function triggered when an event as been updated. * @param {Object} event the updated event as provided by the server */ onEventUpdated(updatedEvent) { let copyPageEvents = this.state.pageEvents.map(event => event) let foundIndex1 = _.findIndex(copyPageEvents, (event) => event._id === updatedEvent._id); copyPageEvents.splice(foundIndex1, 1, updatedEvent); let copyDownloadedEvents = this.state.downloadedEvents.map(event => event) let foundIndex2 = _.findIndex(copyDownloadedEvents, (event) => event._id === updatedEvent._id); copyDownloadedEvents.splice(foundIndex2, 1, updatedEvent); this.setState({ downloadedEvents: copyDownloadedEvents, pageEvents: copyPageEvents }) } /** * Create the event on ICAT+ server * @param {*} newEvent event object to be created * @param {String} sessionId session Id required for HTTP request authentication * @param {*} investigationId investigation Id to which the event will be added to. * @param {*} onSuccess callback function executed after the asymchronous event creation process succeeded. */ createEvent(newEvent, sessionId, investigationId, onSuccess) { axios({ method: 'post', <<<<<<< HEAD:public/report/files/src_containers_EventContainer_js/index.html url: URL.createEvent(sessionId, investigationId), ======= url: createEvent(sessionId, investigationId), >>>>>>> d2931edc408681c85895ae016cbd29c396391a41:public/report/files/containers_EventContainer_js/index.html data: newEvent, }) .then(function (response) { console.log("The new event was created successfully on ICAT+ server."); localStorage.removeItem('plainText'); localStorage.removeItem('HTMLText'); onSuccess(); }) .catch(function (error) { console.log(error); }); } /** * Update the event on ICAT+ server * @param {*} newEvent event object to be created * @param {String} sessionId session Id required for HTTP request authentication * @param {*} investigationId investigation Id to which the event will be added to. * @param {*} onSuccess callback function executed after the asymchronous event creation process succeeded. */ updateEvent(newEvent, sessionId, investigationId, onSuccess) { axios({ method: 'put', <<<<<<< HEAD:public/report/files/src_containers_EventContainer_js/index.html url: URL.updateEvent(sessionId, investigationId), ======= url: updateEvent(sessionId, investigationId), >>>>>>> d2931edc408681c85895ae016cbd29c396391a41:public/report/files/containers_EventContainer_js/index.html data: { event: newEvent }, }) .then(function (response) { console.log("the event was modified successfully on ICAT+ server."); localStorage.removeItem('plainText'); localStorage.removeItem('HTMLText'); onSuccess(response.data); }) .catch(function (error) { console.log(error); }); } } /** * Get the selection filter which selects all comments in the database (annotations and commented notifications). * @returns {Object} selection filter in the mongoDB format */ export function getSelectionFilterForAllComments() { return { $or: [ { type: "annotation" }, { $and: [ { type: "notification" }, { previousVersionEvent: { $ne: null } } ] } ] } } /** * Returns the corresponding selection filter for mongoDB * @param {array} criteria the search criteria as provided by the combosearch component * @returns {Object} the selection query to be used for the mongoDB query */ export function getSelectionFilterBySearchCriteria(criteria) { const REGEXP_FILTER_TYPE = "regexpFilterType"; const EQUALITY_FILTER_TYPE = "equalityFilterType"; /** * A filter used to search a single word inside text, using regexp, not dates * @param {*} criterion the single criterion */ function regexpFilterOnSingleCriterion(criterion) { return { $regex: '.*' + StringConverterForRegexp(criterion.search) + '.*', $options: 'i' }; } function getDateFilterOnSingleCriterion(criterion) { if (criterion.momentDate) { if (criterion.search === "before") { return { $lt: criterion.momentDate.format("YYYY-MM-DDTHH:mm:ss.SSS") + 'Z' }; } if (criterion.search === "after") { return { $gte: criterion.momentDate.format("YYYY-MM-DDTHH:mm:ss.SSS") + 'Z' }; } } return null; } function equalityFilterOnSingleCriterion(criterion) { let filter = {}; if (criterion && criterion.criteria && criterion.search) { filter[criterion.criteria.toLowerCase()] = criterion.search; return filter; } } /** * pop a filter inside a array oof filters. Used for $and and $or filters * @param {Array} filter the array to pop the filter in * @param {*} criterion the criterion which is searched * @param {*} filterType the type of filter, 'regexpFilter' or 'equalityFilter' */ function popSingleFilter(filter, criterion, filterType) { if (criterion.criteria.toLowerCase() === "author") { filter['username'] = regexpFilterOnSingleCriterion(criterion); } else if (criterion.criteria.toLowerCase() === "creation date") { if (getDateFilterOnSingleCriterion(criterion)) { filter['creationDate'] = getDateFilterOnSingleCriterion(criterion); } else { return null; } } else if (criterion.criteria.toLowerCase() === "content") { filter['content.text'] = regexpFilterOnSingleCriterion(criterion); } else { if (filterType === REGEXP_FILTER_TYPE) { filter[criterion.criteria.toLowerCase()] = regexpFilterOnSingleCriterion(criterion); } else if (filterType === EQUALITY_FILTER_TYPE) { filter = equalityFilterOnSingleCriterion(criterion); } } return filter; } /** * Create the or filter which is used in all requests to get only event which type is annotation of * notification but not attachment. */ function getNotificationOrAnnotationFilter() { let filter = {} let orFilter = []; let orFilterItem = popSingleFilter({}, { criteria: 'type', search: 'annotation' }, EQUALITY_FILTER_TYPE); orFilter.push(orFilterItem); orFilterItem = popSingleFilter({}, { criteria: 'type', search: 'notification' }, EQUALITY_FILTER_TYPE); orFilter.push(orFilterItem); filter['$or'] = orFilter; return filter } let filter = {}; // the final filter let andExpressions = []; // the first part of the AND filter which selects annotation and notification systematically andExpressions.push(getNotificationOrAnnotationFilter()); // the second part of the AND filter which selects according to user search let secondANDexpression = {}; let userSearchANDArray = []; if (criteria && criteria instanceof Array) { criteria.forEach(criterion => { if (criterion.criteria && criterion.search) { if (criterion.criteria.toLowerCase() === "everywhere") { let orFilter = []; let orFilterItem = popSingleFilter({}, { criteria: 'title', search: criterion.search }, REGEXP_FILTER_TYPE); orFilter.push(orFilterItem); orFilterItem = popSingleFilter({}, { criteria: 'type', search: criterion.search }, REGEXP_FILTER_TYPE); orFilter.push(orFilterItem); orFilterItem = popSingleFilter({}, { criteria: 'category', search: criterion.search }, REGEXP_FILTER_TYPE); orFilter.push(orFilterItem); orFilterItem = popSingleFilter({}, { criteria: 'username', search: criterion.search }, REGEXP_FILTER_TYPE); orFilter.push(orFilterItem); orFilterItem = popSingleFilter({}, { criteria: 'content.text', search: criterion.search }, REGEXP_FILTER_TYPE); orFilter.push(orFilterItem); userSearchANDArray.push({ '$or': orFilter }); } else { let singleFilter = popSingleFilter({}, criterion, REGEXP_FILTER_TYPE); if (singleFilter) { userSearchANDArray.push(singleFilter); } } } }) if (userSearchANDArray.length !== 0) { secondANDexpression['$and'] = userSearchANDArray; andExpressions.push(secondANDexpression); } } filter['$and'] = andExpressions; return filter; } function mapStateToProps(state) { return { user: state.user, }; } EventContainer.propTypes = { /** the investigationId */ investigationId: PropTypes.string.isRequired } export default connect( mapStateToProps, )(EventContainer);