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);