From 84dde66a933d09dc9a055c843a14569c593932e2 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 17 Feb 2021 10:44:26 +0100 Subject: [PATCH] fix: control bar refactor (#1529) Fixes: * in small screen, make dashboard non-interactive when control bar is expanded (DHIS2-10436) * in small screen control bar horizontal scrollbar only shows half height (DHIS2-10417) * control bar now resizes instantly when moving between portrait and landscape (DHIS2-10418) * control bar fixed while dashboard and header bar scroll in phone landscape orientation (DHIS2-10423) Refactor: * remove most height calculations for dashboard and control bar. This includes for the PrintDashboards. Use flexbox and margins instead. * separate control bar drag functionality into a DragHandle component ** Most code in DragHandle hasn't changed from its original form in the deleted ControlBar file * delete ControlBar and move functionality (related to userRows) to DashboardsBar together. Most height calcs removed, and instead css to calculate controlbar height. * Filter component is kind of complex now, as it renders both a Filter for sm (collapsed and expanded) and lg screens (controlled by css display). --- .../responsive_dashboard.js | 10 +- cypress/integration/ui/view_dashboard.feature | 6 + .../ui/view_dashboard/control_bar.js | 31 + .../integration/ui/view_dashboard/index.js | 7 +- cypress/selectors/viewDashboard.js | 5 + i18n/en.pot | 10 +- src/components/App.css | 18 +- src/components/ControlBar/ControlBar.js | 116 -- src/components/ControlBar/DashboardsBar.js | 208 -- .../{ => ViewControlBar}/ClearButton.js | 2 +- .../ControlBar/ViewControlBar/Content.js | 105 + .../{ => ViewControlBar}/DashboardItemChip.js | 4 +- .../ViewControlBar/DashboardsBar.js | 117 ++ .../ControlBar/ViewControlBar/DragHandle.js | 49 + .../ControlBar/{ => ViewControlBar}/Filter.js | 83 +- .../{ => ViewControlBar}/ShowMoreButton.js | 14 +- .../__tests__/ClearButton.spec.js | 0 .../__tests__/DashboardItemChip.spec.js | 0 .../__tests__/DashboardsBar.spec.js | 139 +- .../__tests__/Filter.spec.js | 2 +- .../__tests__/ShowMoreButton.spec.js | 0 .../__snapshots__/DashboardsBar.spec.js.snap | 1796 +++++++++++++++++ .../__snapshots__/Filter.spec.js.snap | 166 ++ .../__snapshots__/ShowMoreButton.spec.js.snap | 4 +- .../__tests__/controlBarDimensions.spec.js | 10 + .../{ => ViewControlBar}/assets/icons.js | 0 .../ViewControlBar/controlBarDimensions.js | 9 + .../styles/ClearButton.module.css | 0 .../ViewControlBar/styles/Content.module.css | 71 + .../styles/DashboardItemChip.module.css | 0 .../styles/DashboardsBar.module.css | 83 + .../styles/DragHandle.module.css | 15 + .../styles/Filter.module.css | 63 +- .../styles/ShowMoreButton.module.css | 7 + .../ControlBar/__tests__/ControlBar.spec.js | 119 -- .../__snapshots__/DashboardsBar.spec.js.snap | 1001 --------- .../__snapshots__/Filter.spec.js.snap | 96 - .../__tests__/controlBarDimensions.spec.js | 16 - .../ControlBar/controlBarDimensions.js | 58 - .../ControlBar/styles/ControlBar.module.css | 53 - .../styles/DashboardsBar.module.css | 81 - .../ControlBar/styles/EditBar.module.css | 1 + src/components/Dashboard/Dashboard.js | 2 +- src/components/Dashboard/EditDashboard.js | 43 +- src/components/Dashboard/NewDashboard.js | 51 +- src/components/Dashboard/PrintActionsBar.js | 20 +- src/components/Dashboard/PrintDashboard.js | 23 +- .../Dashboard/PrintLayoutDashboard.js | 43 +- src/components/Dashboard/ViewDashboard.js | 53 +- .../Dashboard/__tests__/Dashboard.spec.js | 2 +- .../Dashboard/__tests__/EditDashboard.spec.js | 13 +- .../Dashboard/__tests__/NewDashboard.spec.js | 9 +- .../Dashboard/__tests__/ViewDashboard.spec.js | 22 +- .../__snapshots__/EditDashboard.spec.js.snap | 199 +- .../__snapshots__/NewDashboard.spec.js.snap | 143 +- .../__snapshots__/ViewDashboard.spec.js.snap | 31 +- .../Dashboard/styles/EditDashboard.module.css | 19 + .../Dashboard/styles/NewDashboard.module.css | 19 + .../styles/PrintActionsBar.module.css | 2 +- .../styles/PrintDashboard.module.css | 10 +- .../styles/PrintLayoutDashboard.module.css | 15 +- .../Dashboard/styles/ViewDashboard.module.css | 23 + src/components/FilterBar/FilterBar.js | 2 +- .../styles/ItemSelector.module.css | 6 + src/components/WindowDimensionsProvider.js | 2 +- src/modules/getFilteredDashboards.js | 16 + src/reducers/controlBar.js | 2 +- 67 files changed, 3160 insertions(+), 2185 deletions(-) create mode 100644 cypress/integration/ui/view_dashboard/control_bar.js delete mode 100644 src/components/ControlBar/ControlBar.js delete mode 100644 src/components/ControlBar/DashboardsBar.js rename src/components/ControlBar/{ => ViewControlBar}/ClearButton.js (90%) create mode 100644 src/components/ControlBar/ViewControlBar/Content.js rename src/components/ControlBar/{ => ViewControlBar}/DashboardItemChip.js (92%) create mode 100644 src/components/ControlBar/ViewControlBar/DashboardsBar.js create mode 100644 src/components/ControlBar/ViewControlBar/DragHandle.js rename src/components/ControlBar/{ => ViewControlBar}/Filter.js (54%) rename src/components/ControlBar/{ => ViewControlBar}/ShowMoreButton.js (83%) rename src/components/ControlBar/{ => ViewControlBar}/__tests__/ClearButton.spec.js (100%) rename src/components/ControlBar/{ => ViewControlBar}/__tests__/DashboardItemChip.spec.js (100%) rename src/components/ControlBar/{ => ViewControlBar}/__tests__/DashboardsBar.spec.js (56%) rename src/components/ControlBar/{ => ViewControlBar}/__tests__/Filter.spec.js (98%) rename src/components/ControlBar/{ => ViewControlBar}/__tests__/ShowMoreButton.spec.js (100%) create mode 100644 src/components/ControlBar/ViewControlBar/__tests__/__snapshots__/DashboardsBar.spec.js.snap create mode 100644 src/components/ControlBar/ViewControlBar/__tests__/__snapshots__/Filter.spec.js.snap rename src/components/ControlBar/{ => ViewControlBar}/__tests__/__snapshots__/ShowMoreButton.spec.js.snap (71%) create mode 100644 src/components/ControlBar/ViewControlBar/__tests__/controlBarDimensions.spec.js rename src/components/ControlBar/{ => ViewControlBar}/assets/icons.js (100%) create mode 100644 src/components/ControlBar/ViewControlBar/controlBarDimensions.js rename src/components/ControlBar/{ => ViewControlBar}/styles/ClearButton.module.css (100%) create mode 100644 src/components/ControlBar/ViewControlBar/styles/Content.module.css rename src/components/ControlBar/{ => ViewControlBar}/styles/DashboardItemChip.module.css (100%) create mode 100644 src/components/ControlBar/ViewControlBar/styles/DashboardsBar.module.css create mode 100644 src/components/ControlBar/ViewControlBar/styles/DragHandle.module.css rename src/components/ControlBar/{ => ViewControlBar}/styles/Filter.module.css (63%) rename src/components/ControlBar/{ => ViewControlBar}/styles/ShowMoreButton.module.css (66%) delete mode 100644 src/components/ControlBar/__tests__/ControlBar.spec.js delete mode 100644 src/components/ControlBar/__tests__/__snapshots__/DashboardsBar.spec.js.snap delete mode 100644 src/components/ControlBar/__tests__/__snapshots__/Filter.spec.js.snap delete mode 100644 src/components/ControlBar/__tests__/controlBarDimensions.spec.js delete mode 100644 src/components/ControlBar/controlBarDimensions.js delete mode 100644 src/components/ControlBar/styles/ControlBar.module.css delete mode 100644 src/components/ControlBar/styles/DashboardsBar.module.css create mode 100644 src/components/Dashboard/styles/EditDashboard.module.css create mode 100644 src/components/Dashboard/styles/NewDashboard.module.css create mode 100644 src/components/Dashboard/styles/ViewDashboard.module.css create mode 100644 src/modules/getFilteredDashboards.js diff --git a/cypress/integration/ui/responsive_dashboard/responsive_dashboard.js b/cypress/integration/ui/responsive_dashboard/responsive_dashboard.js index 097c71355..1e025a0fb 100644 --- a/cypress/integration/ui/responsive_dashboard/responsive_dashboard.js +++ b/cypress/integration/ui/responsive_dashboard/responsive_dashboard.js @@ -47,16 +47,16 @@ Then('the wide screen view is shown', () => { Then('the small screen edit view is shown', () => { //no controlbar - cy.contains('Save changes').should('not.exist') - cy.contains('Exit without saving').should('not.exist') + cy.contains('Save changes').should('not.be.visible') + cy.contains('Exit without saving').should('not.be.visible') //notice box and no dashboard cy.contains('dashboards on small screens is not supported').should( 'be.visible' ) // no title or item grid - cy.get('[data-test="dashboard-title-input"]').should('not.exist') - cy.get('.react-grid-layout').should('not.exist') + cy.get('[data-test="dashboard-title-input"]').should('not.be.visible') + cy.get('.react-grid-layout').should('not.be.visible') }) Then('the wide screen edit view is shown', () => { @@ -93,7 +93,7 @@ Then('the {string} dashboard displays in default view mode', title => { }) cy.get(dashboardTitleSel).should('be.visible').and('contain', title) - cy.get(chartSel, EXTENDED_TIMEOUT).should('exist') + cy.get(chartSel, EXTENDED_TIMEOUT).should('be.visible') }) // Scenario: I change the url to 'edit' while in small screen diff --git a/cypress/integration/ui/view_dashboard.feature b/cypress/integration/ui/view_dashboard.feature index c86a4de96..dbbdc585b 100644 --- a/cypress/integration/ui/view_dashboard.feature +++ b/cypress/integration/ui/view_dashboard.feature @@ -38,3 +38,9 @@ Feature: Viewing dashboards Then the print one-item-per-page displays for "Delivery" dashboard When I click to exit print preview Then the "Delivery" dashboard displays in view mode + + @mutating + Scenario: I change the height of the control bar + Given I open the "Delivery" dashboard + When I drag to increase the height of the control bar + Then the control bar height should be updated diff --git a/cypress/integration/ui/view_dashboard/control_bar.js b/cypress/integration/ui/view_dashboard/control_bar.js new file mode 100644 index 000000000..b9d9d416a --- /dev/null +++ b/cypress/integration/ui/view_dashboard/control_bar.js @@ -0,0 +1,31 @@ +import { When, Then } from 'cypress-cucumber-preprocessor/steps' +import { EXTENDED_TIMEOUT } from '../../../support/utils' +import { + dragHandleSel, + dashboardsBarSel, +} from '../../../selectors/viewDashboard' + +// Scenario: I change the height of the control bar +When('I drag to increase the height of the control bar', () => { + cy.intercept('PUT', '/userDataStore/dashboard/controlBarRows').as('putRows') + cy.get(dragHandleSel, EXTENDED_TIMEOUT) + .trigger('mousedown') + .trigger('mousemove', { clientY: 300 }) + .trigger('mouseup') + + cy.wait('@putRows').its('response.statusCode').should('eq', 201) +}) + +Then('the control bar height should be updated', () => { + cy.visit('/') + cy.get(dashboardsBarSel, EXTENDED_TIMEOUT) + .invoke('height') + .should('eq', 231) + + // restore the original height + cy.get(dragHandleSel) + .trigger('mousedown') + .trigger('mousemove', { clientY: 71 }) + .trigger('mouseup') + cy.wait('@putRows').its('response.statusCode').should('eq', 201) +}) diff --git a/cypress/integration/ui/view_dashboard/index.js b/cypress/integration/ui/view_dashboard/index.js index 78c2b655e..d0bf23b6f 100644 --- a/cypress/integration/ui/view_dashboard/index.js +++ b/cypress/integration/ui/view_dashboard/index.js @@ -3,6 +3,7 @@ import { dashboards } from '../../../assets/backends/sierraLeone_236' import { dashboardTitleSel, dashboardChipSel, + dashboardSearchInputSel, } from '../../../selectors/viewDashboard' When('I select the Immunization dashboard', () => { @@ -10,7 +11,7 @@ When('I select the Immunization dashboard', () => { }) When('I search for dashboards containing Immunization', () => { - cy.get('[data-test="search-dashboard-input"]').type('Immun') + cy.get(dashboardSearchInputSel).type('Immun') }) Then('Immunization and Immunization data dashboards are choices', () => { @@ -18,11 +19,11 @@ Then('Immunization and Immunization data dashboards are choices', () => { }) When('I press enter in the search dashboard field', () => { - cy.get('[data-test="search-dashboard-input"]').type('{enter}') + cy.get(dashboardSearchInputSel).type('{enter}') }) When('I search for dashboards containing Noexist', () => { - cy.get('[data-test="search-dashboard-input"]').type('Noexist') + cy.get(dashboardSearchInputSel).type('Noexist') }) Then('no dashboards are choices', () => { cy.get(dashboardChipSel).should('not.exist') diff --git a/cypress/selectors/viewDashboard.js b/cypress/selectors/viewDashboard.js index fcdb7baa2..e73af09b1 100644 --- a/cypress/selectors/viewDashboard.js +++ b/cypress/selectors/viewDashboard.js @@ -2,6 +2,8 @@ export const dashboardChipSel = '[data-test="dashboard-chip"]' export const newDashboardLinkSel = '[data-test="link-new-dashboard"]' export const chipStarSel = '[data-test="dhis2-uicore-chip-icon"]' +export const dashboardSearchInputSel = + 'input:visible[placeholder="Search for a dashboard"]' // Active dashboard export const dashboardTitleSel = '[data-test="view-dashboard-title"]' @@ -9,3 +11,6 @@ export const dashboardDescriptionSel = '[data-test="dashboard-description"]' export const starSel = '[data-test="button-star-dashboard"]' export const dashboardStarredSel = '[data-test="dashboard-starred"]' export const dashboardUnstarredSel = '[data-test="dashboard-unstarred"]' + +export const dragHandleSel = '[data-test="controlbar-drag-handle"]' +export const dashboardsBarSel = '[data-test="dashboards-bar"]' diff --git a/i18n/en.pot b/i18n/en.pot index c995c3676..a92533a76 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2021-02-05T15:43:33.906Z\n" -"PO-Revision-Date: 2021-02-05T15:43:33.906Z\n" +"POT-Creation-Date: 2021-02-15T14:41:50.992Z\n" +"PO-Revision-Date: 2021-02-15T14:41:50.992Z\n" msgid "Untitled dashboard" msgstr "" @@ -237,13 +237,13 @@ msgstr "" msgid "Failed to star the dashboard" msgstr "" -msgid "Edit" +msgid "More" msgstr "" -msgid "Share" +msgid "Edit" msgstr "" -msgid "More" +msgid "Share" msgstr "" msgid "Dashboard layout" diff --git a/src/components/App.css b/src/components/App.css index 15b5f2235..190139bd0 100644 --- a/src/components/App.css +++ b/src/components/App.css @@ -1,9 +1,17 @@ -/* control bar variables: height */ +/* control bar variables */ :root { - --controlbar-showmore-height: 24px; - --headerbar-height: 48px; - --searchbar-height: 40px; - --drag-handle-height: 7px; + --user-rows-count: 1; + --controlbar-padding: 31px; + --row-height: 40px; + --min-rows-height: calc( + var(--controlbar-padding) + (1 * var(--row-height)) + ); + --max-rows-height: calc( + var(--controlbar-padding) + (10 * var(--row-height)) + ); + --user-rows-height: calc( + var(--controlbar-padding) + (var(--user-rows-count) * var(--row-height)) + ); } body { diff --git a/src/components/ControlBar/ControlBar.js b/src/components/ControlBar/ControlBar.js deleted file mode 100644 index 329ff7c92..000000000 --- a/src/components/ControlBar/ControlBar.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import cx from 'classnames' -import classes from './styles/ControlBar.module.css' - -//Matches the height of .dragHandle in ControlBar.module.css -export const DRAG_HANDLE_HEIGHT = 7 - -class ControlBar extends React.Component { - constructor(props) { - super(props) - - this.state = { - dragging: false, - } - } - - onStartDrag = () => { - this.setState({ dragging: true }) - window.addEventListener('mousemove', this.onDrag) - window.addEventListener('mouseup', this.onEndDrag) - } - - onDrag = event => { - event.preventDefault() - event.stopPropagation() - - const newHeight = event.clientY - - if ( - this.props.onChangeHeight && - newHeight !== this.props.height && - newHeight > 0 - ) { - requestAnimationFrame(() => { - this.props.onChangeHeight(newHeight) - }) - } - } - - onEndDrag = () => { - this.setState({ dragging: false }) - window.removeEventListener('mousemove', this.onDrag) - window.removeEventListener('mouseup', this.onEndDrag) - - if (this.props.onEndDrag) { - this.props.onEndDrag() - } - } - - renderDragHandle = () => - typeof this.props.onChangeHeight === 'function' && ( -
- ) - - render() { - const height = Math.max(this.props.height, 0) + DRAG_HANDLE_HEIGHT - - const rootClass = cx( - classes.root, - this.state.dragging && classes.dragging, - this.props.isMaxHeight && classes.expanded - ) - - return ( -
-
{this.props.children}
- {this.renderDragHandle()} -
- ) - } -} - -ControlBar.propTypes = { - /** - * The height of the control bar in number of lines. Must be a positive integer. - */ - children: PropTypes.node.isRequired, - - /** - * Callback function that is called when the control bar is resized. - * The callback receives one argument: The new height in pixels. - * - * If no callback is specified the control bar will not have a drag handle. - */ - height: PropTypes.number.isRequired, - - /** - * Control bar is expanded or is in its max height. - * */ - isMaxHeight: PropTypes.bool, - - /** - * Callback function that is called when the control bar is dropped after being dragged. - * The callback receives one argument: The new height in pixels. - * - * Ignored if no "onChangeHeight" function is provided. - */ - onChangeHeight: PropTypes.func, - - /** - * The contents of the control bar. - */ - onEndDrag: PropTypes.func, -} - -ControlBar.defaultProps = { - onChangeHeight: null, - onEndDrag: null, -} - -export default ControlBar diff --git a/src/components/ControlBar/DashboardsBar.js b/src/components/ControlBar/DashboardsBar.js deleted file mode 100644 index b85f08743..000000000 --- a/src/components/ControlBar/DashboardsBar.js +++ /dev/null @@ -1,208 +0,0 @@ -import React, { useState, useEffect, createRef } from 'react' -import { connect } from 'react-redux' -import { Link, withRouter } from 'react-router-dom' -import cx from 'classnames' -import arraySort from 'd2-utilizr/lib/arraySort' -import PropTypes from 'prop-types' - -import ControlBar, { DRAG_HANDLE_HEIGHT } from './ControlBar' -import Chip from './DashboardItemChip' -import AddCircleIcon from '../../icons/AddCircle' -import Filter from './Filter' -import ShowMoreButton from './ShowMoreButton' -import { - FIRST_ROW_PADDING_HEIGHT, - MIN_ROW_COUNT, - getRowsHeight, - getControlBarHeight, - getNumRowsFromHeight, -} from './controlBarDimensions' -import { useWindowDimensions } from '../WindowDimensionsProvider' -import { sGetDashboardsFilter } from '../../reducers/dashboardsFilter' -import { sGetControlBarUserRows } from '../../reducers/controlBar' -import { sGetAllDashboards } from '../../reducers/dashboards' -import { sGetSelectedId } from '../../reducers/selected' -import { acSetControlBarUserRows } from '../../actions/controlBar' -import { apiPostControlBarRows } from '../../api/controlBar' - -import { isSmallScreen } from '../../modules/smallScreen' - -import classes from './styles/DashboardsBar.module.css' - -export const MAX_ROW_COUNT = 10 -export const isDashboardBarMaxHeight = rows => rows === MAX_ROW_COUNT - -const DashboardsBar = ({ - userRows, - onChangeHeight, - history, - dashboards, - selectedId, - filterText, -}) => { - const [rows, setRows] = useState(userRows) - const { width } = useWindowDimensions() - const ref = createRef() - - useEffect(() => { - setRows(userRows) - }, [userRows]) - - const isMaxHeight = () => isDashboardBarMaxHeight(rows) - - const adjustHeight = newHeight => { - const newRows = Math.max( - MIN_ROW_COUNT, - getNumRowsFromHeight(newHeight - 52) // don't rush the transition to a bigger row count - ) - - if (newRows !== rows) { - onChangeHeight(Math.min(newRows, MAX_ROW_COUNT)) - } - } - - const onEndDrag = () => apiPostControlBarRows(rows) - - const scrollToTop = () => { - if (isMaxHeight()) { - ref.current.scroll(0, 0) - } - } - - const toggleMaxHeight = () => { - const newRows = isMaxHeight() ? userRows : MAX_ROW_COUNT - scrollToTop() - setRows(newRows) - } - - const cancelMaxHeight = () => { - scrollToTop() - setRows(userRows) - } - - const onSelectDashboard = () => { - const id = getFilteredDashboards()[0]?.id - if (id) { - history.push(id) - } - } - - const getFilteredDashboards = () => { - const filteredDashboards = arraySort( - Object.values(dashboards).filter(d => - d.displayName.toLowerCase().includes(filterText.toLowerCase()) - ), - 'ASC', - 'displayName' - ) - - return [ - ...filteredDashboards.filter(d => d.starred), - ...filteredDashboards.filter(d => !d.starred), - ] - } - - const viewableRows = - isSmallScreen(width) && !isMaxHeight() ? MIN_ROW_COUNT : rows - - const rowHeightProp = { - height: getRowsHeight(viewableRows) + FIRST_ROW_PADDING_HEIGHT, - } - - const getDashboardChips = () => { - const chips = getFilteredDashboards().map(dashboard => ( - - )) - if (isSmallScreen(width)) { - const chipContainerClasses = cx( - classes.chipContainer, - isMaxHeight() ? classes.expanded : classes.collapsed - ) - return ( -
- {chips} -
- ) - } else { - return chips - } - } - - const containerClass = cx( - classes.container, - isMaxHeight() ? classes.expanded : classes.collapsed - ) - - return ( - <> - -
-
- - - - -
- {getDashboardChips()} -
- -
-
- - ) -} - -DashboardsBar.propTypes = { - dashboards: PropTypes.object, - filterText: PropTypes.string, - history: PropTypes.object, - selectedId: PropTypes.string, - userRows: PropTypes.number, - onChangeHeight: PropTypes.func, -} - -const mapStateToProps = state => ({ - dashboards: sGetAllDashboards(state), - filterText: sGetDashboardsFilter(state), - selectedId: sGetSelectedId(state), - userRows: sGetControlBarUserRows(state), -}) - -const mapDispatchToProps = { - onChangeHeight: acSetControlBarUserRows, -} - -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(DashboardsBar) -) diff --git a/src/components/ControlBar/ClearButton.js b/src/components/ControlBar/ViewControlBar/ClearButton.js similarity index 90% rename from src/components/ControlBar/ClearButton.js rename to src/components/ControlBar/ViewControlBar/ClearButton.js index 3e91e7853..4a0b69c86 100644 --- a/src/components/ControlBar/ClearButton.js +++ b/src/components/ControlBar/ViewControlBar/ClearButton.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import ClearIcon from '../../icons/Clear' +import ClearIcon from '../../../icons/Clear' import classes from './styles/ClearButton.module.css' diff --git a/src/components/ControlBar/ViewControlBar/Content.js b/src/components/ControlBar/ViewControlBar/Content.js new file mode 100644 index 000000000..e5466962a --- /dev/null +++ b/src/components/ControlBar/ViewControlBar/Content.js @@ -0,0 +1,105 @@ +import React from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import cx from 'classnames' +import { Link, withRouter } from 'react-router-dom' + +import Chip from './DashboardItemChip' +import AddCircleIcon from '../../../icons/AddCircle' +import Filter from './Filter' + +import { sGetAllDashboards } from '../../../reducers/dashboards' +import { sGetDashboardsFilter } from '../../../reducers/dashboardsFilter' +import { sGetSelectedId } from '../../../reducers/selected' +import { getFilteredDashboards } from '../../../modules/getFilteredDashboards' + +import classes from './styles/Content.module.css' + +const Content = ({ + dashboards, + expanded, + filterText, + history, + selectedId, + onChipClicked, + onSearchClicked, +}) => { + const onSelectDashboard = () => { + const id = getFilteredDashboards(dashboards, filterText)[0]?.id + if (id) { + history.push(id) + } + } + + const getChips = () => + getFilteredDashboards(dashboards, filterText).map(dashboard => ( + + )) + + const getControlsSmall = () => ( +
+ +
+ ) + + const getControlsLarge = () => ( +
+ + + + +
+ ) + + return ( +
+ {getControlsSmall()} +
+ {getControlsLarge()} + {getChips()} +
+
+ ) +} + +Content.propTypes = { + dashboards: PropTypes.object, + expanded: PropTypes.bool, + filterText: PropTypes.string, + history: PropTypes.object, + selectedId: PropTypes.string, + onChipClicked: PropTypes.func, + onSearchClicked: PropTypes.func, +} + +const mapStateToProps = state => ({ + dashboards: sGetAllDashboards(state), + selectedId: sGetSelectedId(state), + filterText: sGetDashboardsFilter(state), +}) + +export default withRouter(connect(mapStateToProps)(Content)) diff --git a/src/components/ControlBar/DashboardItemChip.js b/src/components/ControlBar/ViewControlBar/DashboardItemChip.js similarity index 92% rename from src/components/ControlBar/DashboardItemChip.js rename to src/components/ControlBar/ViewControlBar/DashboardItemChip.js index cc0f63a70..60f433888 100644 --- a/src/components/ControlBar/DashboardItemChip.js +++ b/src/components/ControlBar/ViewControlBar/DashboardItemChip.js @@ -4,8 +4,8 @@ import { Chip } from '@dhis2/ui' import { Link } from 'react-router-dom' import debounce from 'lodash/debounce' -import StarIcon from '../../icons/Star' -import { apiPostDataStatistics } from '../../api/dataStatistics' +import StarIcon from '../../../icons/Star' +import { apiPostDataStatistics } from '../../../api/dataStatistics' import classes from './styles/DashboardItemChip.module.css' diff --git a/src/components/ControlBar/ViewControlBar/DashboardsBar.js b/src/components/ControlBar/ViewControlBar/DashboardsBar.js new file mode 100644 index 000000000..9c30d8a58 --- /dev/null +++ b/src/components/ControlBar/ViewControlBar/DashboardsBar.js @@ -0,0 +1,117 @@ +import React, { useState, useRef, useEffect, createRef } from 'react' +import { connect } from 'react-redux' +import cx from 'classnames' +import PropTypes from 'prop-types' + +import Content from './Content' +import ShowMoreButton from './ShowMoreButton' +import DragHandle from './DragHandle' +import { getRowsFromHeight } from './controlBarDimensions' +import { sGetControlBarUserRows } from '../../../reducers/controlBar' +import { acSetControlBarUserRows } from '../../../actions/controlBar' +import { apiPostControlBarRows } from '../../../api/controlBar' + +import classes from './styles/DashboardsBar.module.css' + +export const MIN_ROW_COUNT = 1 +export const MAX_ROW_COUNT = 10 + +const DashboardsBar = ({ + userRows, + updateUserRows, + expanded, + onExpandedChanged, +}) => { + const [dragging, setDragging] = useState(false) + const userRowsChanged = useRef(false) + const ref = createRef() + + const rootElement = document.documentElement + + const adjustRows = newHeight => { + const newRows = Math.max( + MIN_ROW_COUNT, + getRowsFromHeight(newHeight - 52) // don't rush the transition to a bigger row count + ) + + if (newRows !== userRows) { + updateUserRows(Math.min(newRows, MAX_ROW_COUNT)) + userRowsChanged.current = true + } + } + + useEffect(() => { + rootElement.style.setProperty('--user-rows-count', userRows) + }, [userRows]) + + useEffect(() => { + if (!dragging && userRowsChanged.current) { + apiPostControlBarRows(userRows) + userRowsChanged.current = false + } + }, [dragging, userRowsChanged.current]) + + const scrollToTop = () => { + if (expanded) { + ref.current.scroll(0, 0) + } + } + + const toggleExpanded = () => { + if (expanded) { + cancelExpanded() + } else { + scrollToTop() + onExpandedChanged(!expanded) + } + } + + const cancelExpanded = () => { + scrollToTop() + onExpandedChanged(false) + } + + return ( +
+
+
+ +
+ + +
+
+
+ ) +} + +DashboardsBar.propTypes = { + expanded: PropTypes.bool, + updateUserRows: PropTypes.func, + userRows: PropTypes.number, + onExpandedChanged: PropTypes.func, +} + +const mapStateToProps = state => ({ + userRows: sGetControlBarUserRows(state), +}) + +const mapDispatchToProps = { + updateUserRows: acSetControlBarUserRows, +} + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardsBar) diff --git a/src/components/ControlBar/ViewControlBar/DragHandle.js b/src/components/ControlBar/ViewControlBar/DragHandle.js new file mode 100644 index 000000000..4a08cd5b1 --- /dev/null +++ b/src/components/ControlBar/ViewControlBar/DragHandle.js @@ -0,0 +1,49 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' + +import classes from './styles/DragHandle.module.css' + +const DragHandle = ({ onHeightChanged, setDragging }) => { + const [startingY, setStartingY] = useState(0) + + const onStartDrag = e => { + setStartingY(e.clientY) + setDragging(true) + window.addEventListener('mousemove', onDrag) + window.addEventListener('mouseup', onEndDrag) + } + + const onDrag = e => { + e.preventDefault() + e.stopPropagation() + + const currentY = e.clientY + + if (currentY !== startingY && currentY > 0) { + requestAnimationFrame(() => { + onHeightChanged(currentY) + }) + } + } + + const onEndDrag = () => { + setDragging(false) + window.removeEventListener('mousemove', onDrag) + window.removeEventListener('mouseup', onEndDrag) + } + + return ( +
+ ) +} + +DragHandle.propTypes = { + setDragging: PropTypes.func, + onHeightChanged: PropTypes.func, +} + +export default DragHandle diff --git a/src/components/ControlBar/Filter.js b/src/components/ControlBar/ViewControlBar/Filter.js similarity index 54% rename from src/components/ControlBar/Filter.js rename to src/components/ControlBar/ViewControlBar/Filter.js index f42db7f82..a4b54e010 100644 --- a/src/components/ControlBar/Filter.js +++ b/src/components/ControlBar/ViewControlBar/Filter.js @@ -3,16 +3,16 @@ import { connect } from 'react-redux' import PropTypes from 'prop-types' import i18n from '@dhis2/d2-i18n' import cx from 'classnames' -import SearchIcon from '../../icons/Search' +import SearchIcon from '../../../icons/Search' import ClearButton from './ClearButton' -import { useWindowDimensions } from '../WindowDimensionsProvider' +import { useWindowDimensions } from '../../WindowDimensionsProvider' import { acSetDashboardsFilter, acClearDashboardsFilter, -} from '../../actions/dashboardsFilter' -import { sGetDashboardsFilter } from '../../reducers/dashboardsFilter' -import { isSmallScreen } from '../../modules/smallScreen' +} from '../../../actions/dashboardsFilter' +import { sGetDashboardsFilter } from '../../../reducers/dashboardsFilter' +import { isSmallScreen } from '../../../modules/smallScreen' import classes from './styles/Filter.module.css' @@ -21,11 +21,11 @@ export const KEYCODE_ESCAPE = 27 export const FilterUnconnected = ({ clearDashboardsFilter, + expanded, filterText, - isMaxHeight, setDashboardsFilter, onKeypressEnter, - onToggleMaxHeight, + onSearchClicked, }) => { const [focusedClassName, setFocusedClassName] = useState('') const [inputFocused, setInputFocus] = useState(false) @@ -66,53 +66,60 @@ export const FilterUnconnected = ({ } } - const toggleMaxHeight = () => { - onToggleMaxHeight() + const activateSearchInput = () => { + onSearchClicked() setInputFocus(true) } - return isSmallScreen(width) && !isMaxHeight ? ( - - ) : ( + return (
-
- + + +
+
+ + +
+ + {filterText && ( +
+ +
+ )}
- - {filterText && ( -
- -
- )}
) } FilterUnconnected.propTypes = { clearDashboardsFilter: PropTypes.func, + expanded: PropTypes.bool, filterText: PropTypes.string, - isMaxHeight: PropTypes.bool, setDashboardsFilter: PropTypes.func, onKeypressEnter: PropTypes.func, - onToggleMaxHeight: PropTypes.func, + onSearchClicked: PropTypes.func, } const mapStateToProps = state => ({ diff --git a/src/components/ControlBar/ShowMoreButton.js b/src/components/ControlBar/ViewControlBar/ShowMoreButton.js similarity index 83% rename from src/components/ControlBar/ShowMoreButton.js rename to src/components/ControlBar/ViewControlBar/ShowMoreButton.js index 0164f63f4..d57798480 100644 --- a/src/components/ControlBar/ShowMoreButton.js +++ b/src/components/ControlBar/ViewControlBar/ShowMoreButton.js @@ -6,11 +6,9 @@ import { ChevronDown, ChevronUp } from './assets/icons' import classes from './styles/ShowMoreButton.module.css' -export const SHOWMORE_BAR_HEIGHT = 16 - -const ShowMoreButton = ({ onClick, isMaxHeight, disabled }) => { +const ShowMoreButton = ({ onClick, dashboardBarIsExpanded, disabled }) => { const containerRef = useRef(null) - const buttonLabel = isMaxHeight + const buttonLabel = dashboardBarIsExpanded ? i18n.t('Show fewer dashboards') : i18n.t('Show more dashboards') @@ -48,7 +46,11 @@ const ShowMoreButton = ({ onClick, isMaxHeight, disabled }) => { onMouseOver={onMouseOver} onMouseOut={onMouseOut} > - {isMaxHeight ? : } + {dashboardBarIsExpanded ? ( + + ) : ( + + )} )} @@ -58,8 +60,8 @@ const ShowMoreButton = ({ onClick, isMaxHeight, disabled }) => { } ShowMoreButton.propTypes = { + dashboardBarIsExpanded: PropTypes.bool, disabled: PropTypes.bool, - isMaxHeight: PropTypes.bool, onClick: PropTypes.func, } diff --git a/src/components/ControlBar/__tests__/ClearButton.spec.js b/src/components/ControlBar/ViewControlBar/__tests__/ClearButton.spec.js similarity index 100% rename from src/components/ControlBar/__tests__/ClearButton.spec.js rename to src/components/ControlBar/ViewControlBar/__tests__/ClearButton.spec.js diff --git a/src/components/ControlBar/__tests__/DashboardItemChip.spec.js b/src/components/ControlBar/ViewControlBar/__tests__/DashboardItemChip.spec.js similarity index 100% rename from src/components/ControlBar/__tests__/DashboardItemChip.spec.js rename to src/components/ControlBar/ViewControlBar/__tests__/DashboardItemChip.spec.js diff --git a/src/components/ControlBar/__tests__/DashboardsBar.spec.js b/src/components/ControlBar/ViewControlBar/__tests__/DashboardsBar.spec.js similarity index 56% rename from src/components/ControlBar/__tests__/DashboardsBar.spec.js rename to src/components/ControlBar/ViewControlBar/__tests__/DashboardsBar.spec.js index 6c057c779..ce27e16d1 100644 --- a/src/components/ControlBar/__tests__/DashboardsBar.spec.js +++ b/src/components/ControlBar/ViewControlBar/__tests__/DashboardsBar.spec.js @@ -4,11 +4,9 @@ import { fireEvent } from '@testing-library/dom' import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' import { Router } from 'react-router-dom' -import WindowDimensionsProvider from '../../WindowDimensionsProvider' +import WindowDimensionsProvider from '../../../WindowDimensionsProvider' import { createMemoryHistory } from 'history' -import DashboardsBar, { MAX_ROW_COUNT } from '../DashboardsBar' -import { MIN_ROW_COUNT } from '../controlBarDimensions' -import * as api from '../../../api/controlBar' +import DashboardsBar, { MIN_ROW_COUNT, MAX_ROW_COUNT } from '../DashboardsBar' // TODO this spy is an implementation detail jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb()) @@ -33,7 +31,7 @@ test('renders a DashboardsBar with minimum height', () => { const store = { dashboards, dashboardsFilter: '', - controlBar: { userRows: MIN_ROW_COUNT }, + controlBar: { userRows: parseInt(MIN_ROW_COUNT) }, selected: { id: 'rainbow123' }, } const { container } = render( @@ -81,11 +79,15 @@ test('small screen: clicking "Show more" maximizes dashboards bar height', () => controlBar: { userRows: 3 }, selected: { id: 'fluttershy123' }, } + const mockExpandedChanged = jest.fn() const { getByLabelText, asFragment } = render( - + @@ -93,6 +95,7 @@ test('small screen: clicking "Show more" maximizes dashboards bar height', () => fireEvent.click(getByLabelText('Show more dashboards')) expect(asFragment()).toMatchSnapshot() + expect(mockExpandedChanged).toBeCalledWith(true) global.innerWidth = 800 global.innerHeight = 600 }) @@ -101,14 +104,17 @@ test('renders a DashboardsBar with maximum height', () => { const store = { dashboards, dashboardsFilter: '', - controlBar: { userRows: MAX_ROW_COUNT }, + controlBar: { userRows: parseInt(MAX_ROW_COUNT) }, selected: { id: 'rainbow123' }, } const { container } = render( - + @@ -120,7 +126,7 @@ test('renders a DashboardsBar with selected item', () => { const store = { dashboards, dashboardsFilter: '', - controlBar: { userRows: MIN_ROW_COUNT }, + controlBar: { userRows: parseInt(MIN_ROW_COUNT) }, selected: { id: 'fluttershy123' }, } @@ -128,7 +134,10 @@ test('renders a DashboardsBar with selected item', () => { - + @@ -140,7 +149,7 @@ test('renders a DashboardsBar with no items', () => { const store = { dashboards: { byId: {} }, dashboardsFilter: '', - controlBar: { userRows: MIN_ROW_COUNT }, + controlBar: { userRows: parseInt(MIN_ROW_COUNT) }, selected: { id: 'rainbow123' }, } @@ -148,7 +157,10 @@ test('renders a DashboardsBar with no items', () => { - + @@ -160,113 +172,24 @@ test('clicking "Show more" maximizes dashboards bar height', () => { const store = { dashboards, dashboardsFilter: '', - controlBar: { userRows: MIN_ROW_COUNT }, + controlBar: { userRows: parseInt(MIN_ROW_COUNT) }, selected: { id: 'fluttershy123' }, } + const mockOnExpandedChanged = jest.fn() const { getByLabelText, asFragment } = render( - + ) fireEvent.click(getByLabelText('Show more dashboards')) + expect(mockOnExpandedChanged).toBeCalledWith(true) expect(asFragment()).toMatchSnapshot() }) - -test('triggers onChangeHeight when controlbar height is changed', () => { - const store = mockStore({ - dashboards, - dashboardsFilter: '', - controlBar: { userRows: MIN_ROW_COUNT }, - selected: { id: 'fluttershy123' }, - }) - const { getByTestId } = render( - - - - - - - - ) - - const spy = jest.spyOn(api, 'apiPostControlBarRows') - - // TODO - these are implementation details! Refactor the component so this - // isn't necessary to run the test - fireEvent.mouseDown(getByTestId('controlbar-drag-handle')) - fireEvent.mouseMove(window, { clientY: 777 }) - fireEvent.mouseUp(window) - - const actions = store.getActions() - - expect(actions.length).toEqual(1) - expect(actions[0].type).toEqual('SET_CONTROLBAR_USER_ROWS') - expect(actions[0].value).toEqual(10) - - spy.mockRestore() -}) - -test('does not trigger onChangeHeight when controlbar height is changed to similar value', () => { - const store = mockStore({ - dashboards, - dashboardsFilter: '', - controlBar: { userRows: MIN_ROW_COUNT }, - selected: { id: 'fluttershy123' }, - }) - const { getByTestId } = render( - - - - - - - - ) - - const spy = jest.spyOn(api, 'apiPostControlBarRows') - - // TODO - these are implementation details! Refactor the component so this - // isn't necessary to run the test - fireEvent.mouseDown(getByTestId('controlbar-drag-handle')) - fireEvent.mouseMove(window, { clientY: 80 }) - fireEvent.mouseUp(window) - - const actions = store.getActions() - - expect(actions.length).toEqual(0) - spy.mockRestore() -}) - -test('calls the api to post user rows when drag ends', () => { - const store = { - dashboards, - dashboardsFilter: '', - controlBar: { userRows: MIN_ROW_COUNT }, - selected: { id: 'rainbow123' }, - } - const { getByTestId } = render( - - - - - - - - ) - - const spy = jest.spyOn(api, 'apiPostControlBarRows') - - // TODO - these are implementation details! Refactor the component so this - // isn't necessary to run the test - fireEvent.mouseDown(getByTestId('controlbar-drag-handle')) - fireEvent.mouseMove(window, { clientY: 333 }) - fireEvent.mouseUp(window) - - expect(spy).toHaveBeenCalledTimes(1) - spy.mockRestore() -}) diff --git a/src/components/ControlBar/__tests__/Filter.spec.js b/src/components/ControlBar/ViewControlBar/__tests__/Filter.spec.js similarity index 98% rename from src/components/ControlBar/__tests__/Filter.spec.js rename to src/components/ControlBar/ViewControlBar/__tests__/Filter.spec.js index 1c56d34fb..6f474a129 100644 --- a/src/components/ControlBar/__tests__/Filter.spec.js +++ b/src/components/ControlBar/ViewControlBar/__tests__/Filter.spec.js @@ -8,7 +8,7 @@ import Filter, { KEYCODE_ENTER, KEYCODE_ESCAPE, } from '../Filter' -import WindowDimensionsProvider from '../../WindowDimensionsProvider' +import WindowDimensionsProvider from '../../../WindowDimensionsProvider' const mockStore = configureMockStore() diff --git a/src/components/ControlBar/__tests__/ShowMoreButton.spec.js b/src/components/ControlBar/ViewControlBar/__tests__/ShowMoreButton.spec.js similarity index 100% rename from src/components/ControlBar/__tests__/ShowMoreButton.spec.js rename to src/components/ControlBar/ViewControlBar/__tests__/ShowMoreButton.spec.js diff --git a/src/components/ControlBar/ViewControlBar/__tests__/__snapshots__/DashboardsBar.spec.js.snap b/src/components/ControlBar/ViewControlBar/__tests__/__snapshots__/DashboardsBar.spec.js.snap new file mode 100644 index 000000000..1ade39f0d --- /dev/null +++ b/src/components/ControlBar/ViewControlBar/__tests__/__snapshots__/DashboardsBar.spec.js.snap @@ -0,0 +1,1796 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`clicking "Show more" maximizes dashboards bar height 1`] = ` + +