Commit 94adb3c9 authored by Marjolaine Bodin's avatar Marjolaine Bodin
Browse files

Merge branch 'issue_491' into 'milestone-logbook-search-settings'

Issue 491

See merge request !518
parents c7b8313e 3e53ab65
Pipeline #48182 passed with stage
in 3 minutes and 42 seconds
......@@ -14,7 +14,8 @@ export function getEventURL(
sortOrder,
sortBy,
types,
format
format,
search
) {
const params = new URLSearchParams();
params.set('investigationId', investigationId);
......@@ -24,7 +25,7 @@ export function getEventURL(
if (types) params.set('types', types);
if (skip) params.set('skip', skip);
if (format) params.set('format', format);
if (search) params.set('search', search);
return `${ICATPLUS.server}/logbook/${sessionId}/event?${params.toString()}`;
}
......
......@@ -107,7 +107,7 @@ class EventVersionItem extends React.Component {
function buildTheDisplay(event, type, mostRecent) {
return (
<Well bsSize="small" style={{ marginBottom: 5, cursor: 'pointer' }}>
<b> {event.username} </b>
<b> {event.fullName} </b>
{type === 'creation' ? 'created on ' : ''}
{type === 'edition' ? 'edited on ' : ''}
{moment(event.creationDate).format('MMMM DD HH:mm')}
......
......@@ -13,7 +13,7 @@ import EventTextBox from './EventTextBox';
*/
function Event(props) {
let events = [props.event];
const { isReleased } = props;
const { isReleased, search } = props;
const history = useHistory();
const query = useQuery();
......@@ -32,14 +32,14 @@ function Event(props) {
};
const getTimeComponent = (event) => {
const page = event.meta.search
? event.meta.search.page
: event.meta.page.currentPage;
return (
<a
id={event._id}
href={`events?page=${event.meta.page.currentPage}#${event._id}`}
href={`events?page=${page}#${event._id}`}
style={{ fontWeight: 'bold' }}
title={moment(getOriginalEvent(event).creationDate).format(
moment.HTML5_FMT.DATETIME_LOCAL_SECONDS
)}
class="text-muted"
>
{moment(getOriginalEvent(event).creationDate).format(
moment.HTML5_FMT.TIME_SECONDS
......@@ -53,7 +53,7 @@ function Event(props) {
};
return events.map((event, index) => (
<tr key={index} style={{ backgroundColor: '#f0f0f6' }}>
<tr id={event._id} key={index} style={{ backgroundColor: '#f0f0f6' }}>
<td
style={{ width: 16 }}
className={styles.borderTopSeparatorBetweenEvents}
......@@ -86,7 +86,7 @@ function Event(props) {
}}
>
<div style={{ marginLeft: 5, backgroundColor: 'white' }}>
<EventTextBox event={event} />
<EventTextBox event={event} search={search} />
{event.events && collapsed && (
<Label
......
......@@ -76,7 +76,7 @@ function getItems(events, automaticCollapsing) {
* The list of the all events
*/
function EventList(props) {
const { events, isReleased, automaticCollapsing } = props;
const { events, isReleased, search, automaticCollapsing } = props;
if (!events) {
return null;
......@@ -119,6 +119,7 @@ function EventList(props) {
return (
<Event
search={search}
key={index}
event={event}
isReleased={isReleased}
......
......@@ -22,7 +22,7 @@ import LazyLoadedText from './LazyLoadedText';
* For notifications, the box could render 2 texts: the original text and the last comment if it exists
* */
function EventTextBox(props) {
const { event } = props;
const { event, search } = props;
const htmlText = getText(event.content, 'html');
let text = convertImagesToThumbnails(
......@@ -42,7 +42,7 @@ function EventTextBox(props) {
if (event.type === ANNOTATION) {
return (
<div className={getTextColor(EVENT_CATEGORY_COMMENT)}>
<LazyLoadedText text={text} />
<LazyLoadedText text={text} search={search} />
</div>
);
}
......@@ -51,14 +51,8 @@ function EventTextBox(props) {
if (!event.previousVersionEvent) {
return (
<div className={getTextColor(event.category)}>
<pre
className={
event.category.toLowerCase() === EVENT_CATEGORY_COMMANDLINE
? undefined
: 'whitePre'
}
>
<LazyLoadedText text={text} />
<pre className="whitePre">
<LazyLoadedText text={text} search={search} />
</pre>
</div>
);
......@@ -79,11 +73,11 @@ function EventTextBox(props) {
: 'whitePre'
}
>
<LazyLoadedText text={originalText} />
<LazyLoadedText text={originalText} search={search} />
</pre>
</div>
<div className={getTextColor(EVENT_CATEGORY_COMMENT)}>
<LazyLoadedText text={text} />
<LazyLoadedText text={text} search={search} />
</div>
</>
);
......@@ -105,13 +99,13 @@ function getTextColor(category) {
}
if (category.toLowerCase() === EVENT_CATEGORY_INFO) {
return '';
return 'primary';
}
if (category.toLowerCase() === EVENT_CATEGORY_DEBUG) {
return 'text-muted';
}
return 'text-primary';
return '';
}
EventTextBox.propTypes = {
......
......@@ -6,20 +6,41 @@ import LazyLoad from 'react-lazyload';
* least an `img` tag. When there are no images, the text is rendered as such.
*/
function LazyLoadedText(props) {
const { text } = props;
const { search } = props;
let { text } = props;
const withImages = text.indexOf('img') !== -1;
/* If search is enabled then the search is highlighted but not in the images as the base64 code can get corrupted */
if (search && !withImages) {
text = text.replace(
new RegExp(search, 'g'),
`<span style="background-color:yellow;">${search}</span>`
);
}
if (!text) {
return <div />;
}
if (text.indexOf('img') !== -1) {
if (!withImages) {
return (
<LazyLoad once>
<div dangerouslySetInnerHTML={{ __html: text }} />
<div
dangerouslySetInnerHTML={{
__html: text,
}}
/>
</LazyLoad>
);
}
return <div dangerouslySetInnerHTML={{ __html: text }} />;
return (
<div
dangerouslySetInnerHTML={{
__html: text,
}}
/>
);
}
LazyLoadedText.propTypes = {
......
......@@ -2,8 +2,16 @@ 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';
import {
Nav,
Navbar,
NavItem,
Well,
Glyphicon,
FormControl,
InputGroup,
Button,
} from 'react-bootstrap';
import { getEventURL } from '../../../api/icat-plus/logbook';
import { NEW_EVENT_VISIBLE } from '../../../constants/eventTypes';
import LogbookPager from '../LogbookPager';
......@@ -69,13 +77,17 @@ function EventListMenu(props) {
}
};
const onSearch = (data) => {
if (data?.length > 0) {
query.set('search', data[0].search);
history.push({ search: query.toString() });
}
const onSearch = (value) => {
query.set('search', value);
query.delete('page');
history.push({ search: query.toString() });
};
const onKeyUpValue = (event) => {
if (event.keyCode === 13) {
onSearch(event.target.value);
}
};
return (
<Navbar
fluid
......@@ -189,37 +201,28 @@ function EventListMenu(props) {
</NavItem>
)}
<NavItem className="logbookNavItem">
<div className="hidden-xs hidden-sm hidden-md">
<LogbookPager
activePage={activePage}
eventCount={eventCountBySelectionFilter}
<NavItem>
<InputGroup style={{ marginTop: '-7px' }}>
<InputGroup.Button>
<Button bsStyle="primary">
<Glyphicon glyph="search" />
</Button>
</InputGroup.Button>
<FormControl
onKeyUp={onKeyUpValue}
type="search"
placeholder="Search"
/>
</div>
</InputGroup>
</NavItem>
</Nav>
<Nav pullRight>
<NavItem
eventKey={6}
className="logbookNavItem logbookNavItem--combo"
>
<div className="comboSearchContainer">
<ComboSearch
onSearch={onSearch}
selectData={[
{ value: 'Everywhere', text: 'Everywhere' },
{ value: 'category', text: 'Category' },
{ value: 'Creation date', text: 'Creation date' },
]}
datePickerCriteria="Creation date"
/>
</div>
<NavItem>
<Well
style={{
display: 'inline-block',
padding: 4,
padding: 6,
marginBottom: 0,
marginTop: '-7px',
marginLeft: '-30px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
color: '#666',
......@@ -229,6 +232,17 @@ function EventListMenu(props) {
</Well>
</NavItem>
</Nav>
<Nav pullRight>
<NavItem className="logbookNavItem" pullRight>
<div className="hidden-xs hidden-sm hidden-md">
<LogbookPager
activePage={activePage}
eventCount={eventCountBySelectionFilter}
/>
</div>
</NavItem>
</Nav>
</Navbar.Collapse>
{isSettingsDisplayed && (
<SettingLogbookMenuPanel
......
......@@ -36,7 +36,7 @@ const AuthorAndTime = (props) => {
<small style={{ color: 'white' }}>
{' '}
<em>
@{props.event.username} -{' '}
@{props.event.fullName} -{' '}
<TimeAgo date={props.event.creationDate} />
<OverlayTrigger
trigger="click"
......
......@@ -28,10 +28,10 @@ const UI = {
},
logbook: {
/** Number of logbook events to display per page. EVENTS_PER_PAGE should be lower than EVENTS_PER_DOWNLOAD */
EVENTS_PER_PAGE: 40,
EVENTS_PER_PAGE: 100,
/** Maximum number of logbook events downloaded from the server. This enables to store a larger set of
events than those required for a single page thus reducing HTTP requests to the server. */
EVENTS_PER_DOWNLOAD: 40,
EVENTS_PER_DOWNLOAD: 100,
/* the field used to sort events. Most of the time 'creationDate' is used. Possible values: 'creationDate', '_id', 'createdAt', 'updatedAt' */
SORT_EVENTS_BY: '_id',
/* the order the events sorted by SORT_EVENTS_BY will be ordered. Possible values: 1 (for ascending order), -1 (for descending order)*/
......
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { Col, Grid, Row, Alert } from 'react-bootstrap';
import { useParams } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
......@@ -19,17 +19,32 @@ function DatasetsPage() {
const sessionId = useSelector((state) => state.user.sessionId);
const datasets = useSelector((state) => state.datasets);
const breadcrumbsList = useSelector((state) => state.breadcrumbsList);
const currentBreadcrumbsList = useRef(); // work around stale breadcrumbsList reference
currentBreadcrumbsList.current = breadcrumbsList;
const dispatch = useDispatch();
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
dispatch(fetchDatasetsByInvestigationId(sessionId, investigationId));
if (investigation) {
dispatch(setInvestigationBreadCrumbs(investigation));
dispatch(
setInvestigationBreadCrumbs(
investigation,
currentBreadcrumbsList.current
)
);
}
// `investigation` object can be duplicated in store, so we use `investigation.id` as effect dependency
}, [dispatch, investigationId, sessionId, investigation?.id]); // eslint-disable-line react-hooks/exhaustive-deps
}, [
dispatch,
investigationId,
sessionId,
investigation?.id,
currentBreadcrumbsList,
]);
if (!investigation) {
return <PageNotFound />;
......
import React, { useEffect } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { useFetcher, useResource } from 'rest-hooks';
import { Col, Grid, Row } from 'react-bootstrap';
import { Col, Grid, Row, Alert } from 'react-bootstrap';
import { useParams } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import moment from 'moment';
......@@ -25,6 +25,8 @@ const {
} = UI.logbook;
function EventsPage() {
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const { investigationId } = useParams();
const user = useSelector((state) => state.user);
......@@ -32,6 +34,7 @@ function EventsPage() {
const query = useQuery();
const edit = query.get('edit');
const search = query.get('search');
const page = query.get('page') || 1;
const { categoryTypes } = useSelector((state) => state.logbook);
......@@ -48,12 +51,17 @@ function EventsPage() {
(state) => state.logbook.isSortingLatestEventsFirst
);
const breadcrumbsList = useSelector((state) => state.breadcrumbsList);
const currentBreadcrumbsList = useRef(); // work around stale breadcrumbsList reference
currentBreadcrumbsList.current = breadcrumbsList;
const fetchingParams = {
investigationId,
skip: EVENTS_PER_PAGE * (page - 1),
limit: EVENTS_PER_DOWNLOAD,
sortBy: SORT_EVENTS_BY,
sortOrder: SORTING_ORDER,
search,
types: categoryTypes?.map((cat) =>
cat.category ? `${cat.type}-${cat.category}` : `${cat.type}`
),
......@@ -62,8 +70,10 @@ function EventsPage() {
const events = useResource(EventResource.listShape(), fetchingParams);
const refecth = useFetcher(EventResource.listShape());
const refetchEvent = () => {
refecth(fetchingParams);
const refetchEvent = async () => {
setLoading(true);
await refecth(fetchingParams);
setLoading(false);
};
const editEvent = events.find((event) => event._id === edit);
......@@ -73,14 +83,22 @@ function EventsPage() {
// Getting total number of events
let totalEventCount = events.length;
if (events && events.length > 0) {
totalEventCount = events[0].meta.page.total;
totalEventCount =
events[0].meta.search != null
? events[0].meta.search.total
: events[0].meta.page.total;
}
useEffect(() => {
if (investigation) {
dispatch(setInvestigationBreadCrumbs(investigation));
dispatch(
setInvestigationBreadCrumbs(
investigation,
currentBreadcrumbsList.current
)
);
}
}, [dispatch, investigation]);
}, [dispatch, investigation, currentBreadcrumbsList]);
const isReleased =
!!investigation.releaseDate && moment().isAfter(investigation.releaseDate);
......@@ -129,8 +147,9 @@ function EventsPage() {
automaticRefresh={automaticRefresh}
isSortingLatestEventsFirst={isSortingLatestEventsFirst}
/>
{loading && <Alert bsStyle="info">Updating new events</Alert>}
<EventList
search={search}
events={events}
isReleased={isReleased}
automaticCollapsing={automaticCollapsing}
......@@ -140,9 +159,7 @@ function EventsPage() {
</Grid>
<LogbookPager
activePage={page}
eventCount={
events ? (events.length > 0 ? events[0].meta.page.total : 0) : 0
}
eventCount={totalEventCount}
isCentered={true}
/>
</>
......
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { Col, Grid, Row } from 'react-bootstrap';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import ShippingPanel from '../components/Shipping/ShippingPanel';
import TabContainerMenu from '../components/TabContainerMenu/TabContainerMenu';
......@@ -16,12 +16,20 @@ function ShippingPage() {
id: investigationId,
});
const dispatch = useDispatch();
const breadcrumbsList = useSelector((state) => state.breadcrumbsList);
const currentBreadcrumbsList = useRef(); // work around stale breadcrumbsList reference
currentBreadcrumbsList.current = breadcrumbsList;
useEffect(() => {
if (investigation) {
dispatch(setInvestigationBreadCrumbs(investigation));
dispatch(
setInvestigationBreadCrumbs(
investigation,
currentBreadcrumbsList.current
)
);
}
}, [dispatch, investigation]);
}, [dispatch, investigation, currentBreadcrumbsList]); // eslint-disable-line react-hooks/exhaustive-deps
if (!investigation) {
return <PageNotFound />;
......
......@@ -2,7 +2,9 @@ import { INVESTIGATION_DATE_FORMAT } from '../constants';
import moment from 'moment';
import { replaceOrAppendBreadCrumb } from '../actions/breadcrumbs';
export function setInvestigationBreadCrumbs(investigation) {
export function setInvestigationBreadCrumbs(investigation, breadcrumbsList) {
const investigationBreadCrumb = 'INVESTIGATION';
function getDateFormat(date) {
return date ? moment(date).format(INVESTIGATION_DATE_FORMAT) : '';
}
......@@ -13,11 +15,32 @@ export function setInvestigationBreadCrumbs(investigation) {
name: `${getDateFormat(investigation.startDate)}-${getDateFormat(
investigation.endDate
)} ${investigation.title}`,
type: investigationBreadCrumb,
};
}
function isLastItemInvestigation(items) {
const nbItems = items.length;
return (
nbItems > 0 &&
items[nbItems - 1].type &&
items[nbItems - 1].type === investigationBreadCrumb
);
}
function getLevelInBreadCrumb(breadcrumbsList) {
const items =
breadcrumbsList && breadcrumbsList.items ? breadcrumbsList.items : [];
const level = isLastItemInvestigation(items)
? items.length - 1
: items.length;
return level;
}
const level = getLevelInBreadCrumb(breadcrumbsList);
return replaceOrAppendBreadCrumb(
getInvestigationBreadCrumb(investigation),
1
level
);
}
......@@ -22,6 +22,9 @@ export function useScrollToHash(props) {
if (hash) {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.setAttribute('style', 'border:1px solid blue');
}
if (element) {
element.scrollIntoView({ block: 'start', behavior: 'instant' });
window.scrollBy(0, offsetY);
......
......@@ -16,8 +16,16 @@ export default class EventResource extends Resource {
static listUrl(params) {
const { sessionId } = store.getState().user;
const { investigationId, skip, limit, sortOrder, sortBy, types } = params;
const {
investigationId,
skip,
limit,
sortOrder,
sortBy,
types,
search,
format,
} = params;
return getEventURL(
sessionId,
investigationId,
......@@ -25,7 +33,9 @@ export default class EventResource extends Resource {
limit,
sortOrder,
sortBy,
types
types,
format,