diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index affcdea..3b8899e 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -26,6 +26,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
+ - run: npm install
- run: npm ci
- run: npm run build --if-present
- run: npm test
diff --git a/package.json b/package.json
index 1efc722..f49ec48 100755
--- a/package.json
+++ b/package.json
@@ -16,11 +16,11 @@
"ajv": "8.10.0",
"antd": "5.7.3",
"axios": "0.26.0",
- "bootstrap": "4.3.1",
+ "bootstrap": "5.2.3",
"es6-shim": "0.35.6",
"history": "4.10.1",
"jquery": "3.6.0",
- "kpmp-common-components": "1.2.1",
+ "kpmp-common-components": "1.2.13",
"lodash": "4.17.21",
"openseadragon": "2.4.1",
"prop-types": "15.8.1",
@@ -30,7 +30,7 @@
"react-ga4": "2.1.0",
"react-redux": "7.2.2",
"react-router-dom": "5.2.0",
- "reactstrap": "8.9.0",
+ "reactstrap": "9.2.0",
"redux": "4.0.5",
"redux-thunk": "2.3.0",
"typescript": "3.7.2"
diff --git a/src/actions/Participants/participantActions.js b/src/actions/Participants/participantActions.js
index ce0bdbc..89a7b29 100644
--- a/src/actions/Participants/participantActions.js
+++ b/src/actions/Participants/participantActions.js
@@ -17,6 +17,13 @@ export const setSelectedSlide = (slide) => {
}
}
+export const setSelectedAccordion = (accordion) => {
+ return {
+ type: actionNames.SET_SELECTED_ACCORDION,
+ payload: accordion
+ }
+}
+
export const setParticipants = (participants) => {
return {
type: actionNames.SET_PARTICIPANTS,
@@ -29,12 +36,23 @@ export const getParticipantSlides = (participantId, props) => {
var config = { headers: {'Content-Type': 'application/json', 'Cache-control': 'no-cache'}};
axios.get('/api/v1/slides/' + participantId, config)
.then(result => {
- let slides = participantSelectSorter(result.data);
- dispatch(setSelectedParticipant({id: participantId, slides: slides, selectedSlide: slides[0]}));
+ let newData = {}
+ for(const [key, value] of Object.entries(result.data)){
+ let newValue = participantSelectSorter(value);
+ newData[key] = newValue
+ }
+ let sortedData = {}
+ let keys = Object.keys(newData)
+ keys.sort()
+ keys.reverse()
+ for (let key of keys) {
+ sortedData[key] = newData[key]
+ }
+ dispatch(setSelectedParticipant({id: participantId, slides: sortedData, selectedSlide:sortedData["(LM) Light Microscopy"][0], selectedAccordion: "(LM) Light Microscopy"}));
props.history.push(process.env.PUBLIC_URL + "/slides");
})
.catch(err => {
- console.log("We were unable to get a list of slides for " + participantId);
+ console.log("We were unable to get a list of slides for " + participantId);
dispatch(sendMessageToBackend(err));
});
}
diff --git a/src/actions/actionNames.js b/src/actions/actionNames.js
index b93ab78..27a64d1 100755
--- a/src/actions/actionNames.js
+++ b/src/actions/actionNames.js
@@ -1,7 +1,8 @@
const actionNames = {
SET_SELECTED_PARTICIPANT: "SET_SELECTED_PARTICIPANT",
SET_PARTICIPANTS: "SET_PARTICIPANTS",
- SET_SELECTED_SLIDE: "SET_SELECTED_SLIDE"
+ SET_SELECTED_SLIDE: "SET_SELECTED_SLIDE",
+ SET_SELECTED_ACCORDION: "SET_SELECTED_ACCORDION"
};
export default actionNames;
\ No newline at end of file
diff --git a/src/components/Slides/Menu/Header.js b/src/components/Slides/Menu/Header.js
deleted file mode 100755
index d182b1e..0000000
--- a/src/components/Slides/Menu/Header.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, { Component } from 'react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- faCaretLeft,
- faChevronRight,
- faChevronLeft,
- faDownload,
- faSquare,
- faCheckSquare,
- faCaretDown
-} from '@fortawesome/free-solid-svg-icons';
-import { Col, Row } from 'reactstrap';
-import { getNextSlide, getPreviousSlide, downloadSlide } from '../slideHelpers.js';
-import GridProperties from './GridProperties.js';
-import PropTypes from 'prop-types';
-import { handleGoogleAnalyticsEvent } from '../../../helpers/googleAnalyticsHelper.js';
-
-class Header extends Component {
- constructor(props) {
- super(props);
- this.state = { showGridProperties: false }
- this.handleShowGridProperties = this.handleShowGridProperties.bind(this)
- this.handleDownload = this.handleDownload.bind(this);
- this.textInput = React.createRef();
- this.focusTextInput = this.focusTextInput.bind(this);
- }
-
- focusTextInput() {
- // Explicitly focus the text input using the raw DOM API
- // Note: we're accessing "current" to get the DOM node
- this.textInput.current.focus();
- }
- handleNextSlide() {
- let nextSlide = getNextSlide(this.props.selectedParticipant.slides, this.props.selectedParticipant.selectedSlide);
- this.props.setSelectedSlide(nextSlide);
- this.props.toggleMenu(true);
- }
-
- handlePreviousSlide() {
- let previousSlide = getPreviousSlide(this.props.selectedParticipant.slides, this.props.selectedParticipant.selectedSlide);
- this.props.setSelectedSlide(previousSlide);
- this.props.toggleMenu(true);
- }
-
- handleDownload() {
-
- handleGoogleAnalyticsEvent('DPR', 'Download',
- this.props.selectedParticipant.id
- + this.props.selectedParticipant.selectedSlide.slideName);
-
- let downloadFileName = this.props.selectedParticipant.selectedSlide.slideName + ".jpg";
- downloadSlide(downloadFileName);
- }
-
- handleShowGridProperties() {
- if (this.state.showGridProperties) {
- this.setState({ showGridProperties: false })
- } else {
- this.setState({ showGridProperties: true })
- }
- }
-
- render() {
- return (
-
-
- CASE ID: {this.props.selectedParticipant.id}
-
-
-
-
-
-
- this.handlePreviousSlide()} size="lg" />
- this.handleNextSlide()} size="lg" />
-
-
-
-
-
- GRID
-
-
-
-
-
-
-
-
- {this.state.showGridProperties &&
-
-
- }
-
-
- );
- }
-
-}
-
-Header.propTypes = {
- selectedParticipant: PropTypes.object.isRequired,
- setSelectedSlide: PropTypes.func.isRequired,
- toggleMenu: PropTypes.func.isRequired,
- handlePreviousSlide: PropTypes.func.isRequired,
- handleNextSlide: PropTypes.func.isRequired,
- showGrid: PropTypes.bool,
- handleShowGridToggle: PropTypes.func.isRequired,
- horizontal: PropTypes.number.isRequired,
- horizontalRef: PropTypes.func.isRequired,
- vertical: PropTypes.number.isRequired,
- verticalRef: PropTypes.func.isRequired,
- handleShowLabelToggle: PropTypes.func.isRequired,
- handleSetGridPropertiesClick: PropTypes.func.isRequired,
- handleCancelGridPropertiesClick: PropTypes.func.isRequired
-
-}
-
-export default Header;
diff --git a/src/components/Slides/Menu/Menu.js b/src/components/Slides/Menu/Menu.js
index b8825c7..0862f67 100755
--- a/src/components/Slides/Menu/Menu.js
+++ b/src/components/Slides/Menu/Menu.js
@@ -36,7 +36,8 @@ class Menu extends Component {
verticalRef={this.props.verticalRef}
horizontal={this.props.horizontal}
vertical={this.props.vertical}
- toggleMenu={this.toggleMenu} />
+ toggleMenu={this.toggleMenu}
+ selectedParticipant={this.props.selectedParticipant} />
diff --git a/src/components/Slides/Menu/SlideList.js b/src/components/Slides/Menu/SlideList.js
index 2576d31..a9bd81a 100755
--- a/src/components/Slides/Menu/SlideList.js
+++ b/src/components/Slides/Menu/SlideList.js
@@ -1,62 +1,260 @@
import React, { Component } from 'react';
-import { Col, Row } from 'reactstrap';
+import { Col, Accordion, Row, AccordionItem, AccordionHeader, AccordionBody} from 'reactstrap';
import {
noSlidesFound,
- getStainImageName
} from '../slideHelpers.js';
import PropTypes from 'prop-types';
-
-
-import Header from './Header';
+import {
+ getStainImageName
+} from '../slideHelpers.js';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faCaretLeft,
+ faChevronRight,
+ faChevronLeft,
+ faDownload,
+ faSquare,
+ faCheckSquare,
+ faCaretDown
+} from '@fortawesome/free-solid-svg-icons';
+import { handleGoogleAnalyticsEvent } from '../../../helpers/googleAnalyticsHelper.js';
+import { downloadSlide } from '../slideHelpers.js';
+import GridProperties from './GridProperties.js';
class SlideList extends Component {
-
constructor(props) {
super(props);
- this.handleSelectSlide = this.handleSelectSlide.bind(this);
+ this.state = {
+ open: "",
+ openItems: [0],
+ showGridProperties: false,
+ currentSlideTypeIndex: 0,
+ slidePosition: 0,
+ slideIndex: 0,
+ };
+ this.handleShowGridProperties = this.handleShowGridProperties.bind(this)
+ this.handleDownload = this.handleDownload.bind(this);
+ this.textInput = React.createRef();
+ this.focusTextInput = this.focusTextInput.bind(this);
+ this.handleNextSlide = this.handleNextSlide.bind(this);
+ this.handlePreviousSlide = this.handlePreviousSlide.bind(this);
+ this.handleSelectSlide = this.handleSelectSlide.bind(this);
+ this.handleSelectAccordion = this.handleSelectAccordion.bind(this);
+ this.toggle = this.toggle.bind(this);
+ }
+
+ componentDidUpdate() {
+ noSlidesFound(this.props.selectedParticipant);
+ }
+
+ toggle (slideTypeIndex) {
+ let openItems = this.state.openItems;
+ const openAccordion = openItems.includes(slideTypeIndex);
+
+ if(!openAccordion){
+ this.setState( prevState => ({
+ openItems: [...prevState.openItems, slideTypeIndex]
+ }));
+ }
+ }
+
+ focusTextInput() {
+ // Explicitly focus the text input using the raw DOM API
+ // Note: we're accessing "current" to get the DOM node
+ this.textInput.current.focus();
}
+ handleNextSlide() {
+ let slidePosition = this.state.slidePosition + 1;
+ let currentSlideTypeIndex = this.state.currentSlideTypeIndex;
+ let slideTypes = Object.keys(this.props.selectedParticipant.slides);
+ slideTypes.sort();
+ slideTypes.reverse();
+
+ if (slidePosition === this.props.selectedParticipant.slides[slideTypes[currentSlideTypeIndex]].length) {
+ currentSlideTypeIndex += 1;
+ slidePosition = 0;
+ if (currentSlideTypeIndex >= slideTypes.length) {
+ currentSlideTypeIndex = 0;
+ }
+ }
+ let nextSlide = this.props.selectedParticipant.slides[slideTypes[currentSlideTypeIndex]][slidePosition];
+
+ this.setState({
+ slidePosition: slidePosition,
+ currentSlideTypeIndex: currentSlideTypeIndex,
+ });
+
+ this.props.setSelectedAccordion(...this.props.selectedParticipant.slides[slideTypes[currentSlideTypeIndex]][slidePosition].slideType)
+ this.toggle(currentSlideTypeIndex)
+ this.props.setSelectedSlide(nextSlide);
+ this.props.toggleMenu(true);
+}
+
+
- handleSelectSlide(slide) {
+handlePreviousSlide() {
+ let slidePosition = this.state.slidePosition - 1;
+ let currentSlideTypeIndex = this.state.currentSlideTypeIndex;
+ let slideTypes = Object.keys(this.props.selectedParticipant.slides);
+ slideTypes.sort();
+ slideTypes.reverse();
+
+ if (slidePosition < 0) {
+ currentSlideTypeIndex -= 1;
+ if (currentSlideTypeIndex < 0) {
+ currentSlideTypeIndex = slideTypes.length - 1;
+ }
+ slidePosition = this.props.selectedParticipant.slides[slideTypes[currentSlideTypeIndex]].length - 1;
+ }
+
+ let previousSlide = this.props.selectedParticipant.slides[slideTypes[currentSlideTypeIndex]][slidePosition];
+ this.setState({
+ slidePosition: slidePosition,
+ currentSlideTypeIndex: currentSlideTypeIndex,
+ });
+
+ this.props.setSelectedAccordion(this.props.selectedParticipant.slides[slideTypes[currentSlideTypeIndex]][slidePosition].slideType)
+ this.toggle(currentSlideTypeIndex)
+ this.props.setSelectedSlide(previousSlide);
+ this.props.toggleMenu(true);
+}
+
+
+ handleDownload() {
+
+ handleGoogleAnalyticsEvent('DPR', 'Download',
+ this.props.selectedParticipant.id
+ + this.props.selectedParticipant.selectedSlide.slideName);
+
+ let downloadFileName = this.props.selectedParticipant.selectedSlide.slideName + ".jpg";
+ downloadSlide(downloadFileName);
+ }
+
+ handleShowGridProperties() {
+ if (this.state.showGridProperties) {
+ this.setState({ showGridProperties: false })
+ } else {
+ this.setState({ showGridProperties: true })
+ }
+ }
+
+ handleSelectSlide(slide, accordion, slideIndex, accordionIndex) {
this.props.setSelectedSlide(slide);
this.props.toggleMenu(true);
+ this.setState({currentSlideTypeIndex: accordionIndex, slidePosition: slideIndex})
+ this.props.setSelectedAccordion(accordion)
}
- componentDidUpdate() {
- noSlidesFound(this.props.selectedParticipant);
- }
+ handleSelectAccordion(accordion) {
+ let openItems = this.state.openItems;
+
+ if (openItems.includes(accordion)) {
+ this.setState({ openItems: openItems.filter(item => item !== accordion) });
+ } else {
+ this.setState({ openItems: [...openItems, accordion] });
+ }
+ this.props.toggleMenu(true)
+ }
render() {
-
+ let openItems = this.state.openItems
return (
);
-
}
}
SlideList.propTypes = {
selectedParticipant: PropTypes.object.isRequired,
- setSelectedSlide: PropTypes.func.isRequired,
- toggleMenu: PropTypes.func.isRequired,
- handleSelectSlide: PropTypes.func.isRequired
}
export default SlideList;
diff --git a/src/components/Slides/Menu/SlideListContainer.js b/src/components/Slides/Menu/SlideListContainer.js
index 5dd3d03..65a02bd 100755
--- a/src/components/Slides/Menu/SlideListContainer.js
+++ b/src/components/Slides/Menu/SlideListContainer.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
import SlideList from './SlideList';
import { setSelectedSlide } from '../../../actions/Participants/participantActions';
+import { setSelectedAccordion } from '../../../actions/Participants/participantActions';
const mapStateToProps = (state, props) =>
({
@@ -12,7 +13,10 @@ const mapDispatchToProps = (dispatch, props) =>
({
setSelectedSlide(slide) {
dispatch(setSelectedSlide(slide))
- }
+ },
+ setSelectedAccordion(accordion){
+ dispatch(setSelectedAccordion(accordion))
+ }
});
export default connect(mapStateToProps, mapDispatchToProps)(SlideList);
\ No newline at end of file
diff --git a/src/components/Slides/SlideViewer.js b/src/components/Slides/SlideViewer.js
index e7a363f..3effe1b 100755
--- a/src/components/Slides/SlideViewer.js
+++ b/src/components/Slides/SlideViewer.js
@@ -24,17 +24,21 @@ class SlideViewer extends Component {
showGrid: false,
showGridLabel: false,
overlayDivs: '',
- overlayLabel: this.props.selectedParticipant.selectedSlide.metadata.overlayLabel,
+ overlayLabel: [],
renderLabels: true,
- gridOverlay: this.props.selectedParticipant.selectedSlide.metadata.overlay
+ gridOverlay: null,
+ loaded: false,
}
}
async componentDidMount() {
+ await this.props.selectedParticipant.selectedSlide.slideType
+
if (!noSlidesFound(this.props.selectedParticipant, this.props.handleError)) {
await this.renderOverlayLabels();
this.initSeaDragon();
}
+ this.setState({loaded: true})
}
async componentDidUpdate(prevProps, prevState) {
@@ -48,8 +52,14 @@ class SlideViewer extends Component {
}
async renderOverlayLabels() {
- await this.setState({ overlayLabel: this.props.selectedParticipant.selectedSlide.metadata.overlayLabel, gridOverlay: this.props.selectedParticipant.selectedSlide.metadata.overlay,
- renderLabels: false });
+ if(this.props.selectedParticipant.selectedSlide.slideType === "(LM) Light Microscopy"){
+ await this.setState({
+ overlayLabel: this.props.selectedParticipant.selectedSlide.metadata.overlayLabel,
+ gridOverlay: this.props.selectedParticipant.selectedSlide.metadata.overlay,
+ renderLabels: false,
+ }
+ )
+ }
await this.setState({renderLabels: true});
}
@@ -105,8 +115,10 @@ class SlideViewer extends Component {
}
-
-
+ selectedParticipant={this.props.selectedParticipant}/>
+ :
+ null
+ }
+
{ this.el = node; }}>
diff --git a/src/components/Summary/ParticipantSelect.js b/src/components/Summary/ParticipantSelect.js
index d959aee..2f24c35 100644
--- a/src/components/Summary/ParticipantSelect.js
+++ b/src/components/Summary/ParticipantSelect.js
@@ -21,8 +21,13 @@ class ParticipantSelect extends Component {
this.setState({buttonDisabled: false});
};
+ async handleSelectedParticipant(participantId){
+ await this.props.setSelectedParticipant(participantId)
+ }
+
handleClick = () => {
- this.props.setSelectedParticipant(this.state.participantId);
+ this.handleSelectedParticipant(this.state.participantId);
+ // this.props.setSelectedParticipant(this.state.participantId)
};
componentDidMount() {
diff --git a/src/components/Summary/participantSelectReducer.js b/src/components/Summary/participantSelectReducer.js
index 49df0df..fdc70f3 100644
--- a/src/components/Summary/participantSelectReducer.js
+++ b/src/components/Summary/participantSelectReducer.js
@@ -6,6 +6,8 @@ export const selectedParticipant = (state = {}, action) => {
return action.payload;
case actionNames.SET_SELECTED_SLIDE:
return {...state, selectedSlide: action.payload};
+ case actionNames.SET_SELECTED_ACCORDION:
+ return {...state, selectedAccordion: action.payload}
default:
return state;
}
diff --git a/src/index.scss b/src/index.scss
index 3f4bb01..da90234 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -2,6 +2,35 @@
@import "common-values.scss";
@import "grid.scss";
+@import "../node_modules/bootstrap/scss/functions";
+
+$accordion-button-active-color: black;
+$accordion-button-color: black;
+$bs-border-color: #dee2e6;
+$accordion-icon-width: .8125rem;
+$accordion-body-padding-y: 0rem;
+$accordion-body-padding-x: 0rem;
+
+@import "../node_modules/bootstrap/scss/variables";
+@import "../node_modules/bootstrap/scss/mixins";
+// import accordion component with variable overrides applied
+@import "../node_modules/bootstrap/scss/_accordion.scss";
+
+.accordion{
+ --bs-accordion-border-color: #dee2e6;
+}
+
+.accordion-item:first-of-type(.collapsed) .accordion-button {
+ --bs-accordion-color: $accordion-button-color
+ --bs-accorion-border-color: $accordion-button-color
+ box-shadow: inset 0 -1px 0 rgba(0,0,0,.125);
+}
+
+.accordion-button:not(.collapsed)::after {
+ background-color: #e7f1ff;
+ --bs-accordion-btn-color: $accordion-button-color;
+ --bs-accordion-active-color: $accordion-button-active-color;
+}
// Temporary classes
.content-warning {
diff --git a/src/initialState.json b/src/initialState.json
index a753316..9f00b6b 100755
--- a/src/initialState.json
+++ b/src/initialState.json
@@ -1,6 +1,6 @@
{
"participants": [],
- "selectedParticipant": {"id": "", "slides": [], "selectedSlide": ""}
+ "selectedParticipant": {"id": "", "slides": [], "selectedSlide": "", "selectedAccordion": ""}
}
\ No newline at end of file
diff --git a/src/menu.scss b/src/menu.scss
index 1d6292d..38da976 100755
--- a/src/menu.scss
+++ b/src/menu.scss
@@ -217,12 +217,13 @@ $menu-header-height: 80px;
#slides-col {
padding-left: 0px;
padding-right: 0px;
+ max-height: calc(100vh - (#{$header-height} + #{$menu-header-height}));
+ min-height: calc(100vh - (#{$header-height} + #{$menu-header-height}));
+ overflow: auto;
}
#menu-slide-list-slides {
- max-height: calc(100vh - (#{$header-height} + #{$menu-header-height}));
- min-height: calc(100vh - (#{$header-height} + #{$menu-header-height}));
- overflow: auto;
+
.slide-highlighted {
background-color: rgba(230, 241, 255, 1);