diff --git a/.eslintrc.js b/.eslintrc.js index 74ea743b2..26a5001e9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,4 +2,15 @@ const { config } = require('@dhis2/cli-style') module.exports = { extends: [config.eslintReact, 'plugin:cypress/recommended'], + overrides: [ + { + files: ['src/**/*.spec.js'], + rules: { + 'react/prop-types': 'off', + 'react/display-name': 'off', + 'react/no-unknown-property': 'off', + 'no-unused-vars': ['error', { ignoreRestSiblings: true }], + }, + }, + ], } diff --git a/.github/workflows/generate-and-upload-bom.yml b/.github/workflows/generate-and-upload-bom.yml new file mode 100644 index 000000000..5c4dda9eb --- /dev/null +++ b/.github/workflows/generate-and-upload-bom.yml @@ -0,0 +1,49 @@ +name: 'This workflow creates bill of material and uploads it to Dependency-Track each night' + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow}}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + create-bom: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.x + + - name: Install + run: yarn install --frozen-lockfile + + - name: Install CycloneDX CLI + run: | + curl -s https://api.github.com/repos/CycloneDX/cyclonedx-cli/releases/latest | grep "browser_download_url.*linux.x64" | cut -d '"' -f 4 | wget -i - + sudo mv cyclonedx-linux-x64 /usr/local/bin/ + sudo chmod +x /usr/local/bin/cyclonedx-linux-x64 + + - name: Generate BOMs + run: | + npm install -g @cyclonedx/cdxgen + cdxgen -o sbom.json + + - name: Upload SBOM to DependencyTrack + env: + DEPENDENCY_TRACK_API: 'https://dt.security.dhis2.org/api/v1/bom' + run: | + curl -X POST "$DEPENDENCY_TRACK_API" \ + --fail-with-body \ + -H "Content-Type: multipart/form-data" \ + -H "X-Api-Key: ${{ secrets.DEPENDENCYTRACK_APIKEY }}" \ + -F "project=c0bd0f2d-d512-460a-81f9-e256e4fb1054" \ + -F "bom=@sbom.json" diff --git a/cypress/e2e/common/add_a_FILTERTYPE_filter.js b/cypress/e2e/common/add_a_FILTERTYPE_filter.js index 5e3e15bef..89ff1b6e0 100644 --- a/cypress/e2e/common/add_a_FILTERTYPE_filter.js +++ b/cypress/e2e/common/add_a_FILTERTYPE_filter.js @@ -11,7 +11,7 @@ const OU_ID = 'ImspTQPwCqd' //Sierra Leone const FACILITY_TYPE = 'Clinic' When('I add a {string} filter', (dimensionType) => { - cy.contains('Add filter').click() + cy.containsExact('Filter').click() // select an item in the modal switch (dimensionType) { diff --git a/cypress/e2e/common/click_on_the_FILTERTYPE_filter_badge.js b/cypress/e2e/common/click_on_the_FILTERTYPE_filter_badge.js index 8b59fa991..52a929068 100644 --- a/cypress/e2e/common/click_on_the_FILTERTYPE_filter_badge.js +++ b/cypress/e2e/common/click_on_the_FILTERTYPE_filter_badge.js @@ -2,5 +2,8 @@ import { When } from '@badeball/cypress-cucumber-preprocessor' import { filterBadgeSel } from '../../elements/dashboardFilter.js' When('I click on the {string} filter badge', (filterName) => { - cy.get(filterBadgeSel).find('span:visible').contains(filterName).click() + cy.get(filterBadgeSel) + .find('button') + .contains(filterName) + .click({ force: true }) }) diff --git a/cypress/e2e/common/open_print_layout.js b/cypress/e2e/common/open_print_layout.js index ac013f180..b06116a37 100644 --- a/cypress/e2e/common/open_print_layout.js +++ b/cypress/e2e/common/open_print_layout.js @@ -1,8 +1,7 @@ import { When } from '@badeball/cypress-cucumber-preprocessor' -import { clickViewActionButton } from '../../elements/viewDashboard.js' When('I click to preview the print layout', () => { - clickViewActionButton('More') + cy.get('[data-test="more-actions-button"]').click() cy.get('[data-test="print-menu-item"]').click() cy.get('[data-test="print-layout-menu-item"]').click() }) diff --git a/cypress/e2e/common/open_the_SL_dashboard.js b/cypress/e2e/common/open_the_SL_dashboard.js index 7c7ce15c0..ad7e30137 100644 --- a/cypress/e2e/common/open_the_SL_dashboard.js +++ b/cypress/e2e/common/open_the_SL_dashboard.js @@ -1,14 +1,11 @@ import { Given } from '@badeball/cypress-cucumber-preprocessor' import { dashboards } from '../../assets/backends/index.js' // import { gridItemSel, chartSel } from '../../elements/dashboardItem.js' -import { - dashboardTitleSel, - dashboardChipSel, -} from '../../elements/viewDashboard.js' -import { EXTENDED_TIMEOUT } from '../../support/utils.js' +import { getNavigationMenuItem } from '../../elements/navigationMenu.js' +import { dashboardTitleSel } from '../../elements/viewDashboard.js' Given('I open the {string} dashboard', (title) => { - cy.get(dashboardChipSel, EXTENDED_TIMEOUT).contains(title).click() + getNavigationMenuItem(title).click() cy.location().should((loc) => { expect(loc.hash).to.equal(dashboards[title].route) diff --git a/cypress/e2e/dashboard_filter/create_dashboard.js b/cypress/e2e/dashboard_filter/create_dashboard.js index c4102b05c..c283a0262 100644 --- a/cypress/e2e/dashboard_filter/create_dashboard.js +++ b/cypress/e2e/dashboard_filter/create_dashboard.js @@ -1,9 +1,7 @@ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor' import { gridItemSel } from '../../elements/dashboardItem.js' -import { - dashboardChipSel, - dashboardTitleSel, -} from '../../elements/viewDashboard.js' +import { getNavigationMenuItem } from '../../elements/navigationMenu.js' +import { dashboardTitleSel } from '../../elements/viewDashboard.js' import { EXTENDED_TIMEOUT, createDashboardTitle, @@ -79,9 +77,7 @@ When('I add items and save', () => { }) Given('I open an existing dashboard', () => { - cy.get(dashboardChipSel, EXTENDED_TIMEOUT) - .contains(TEST_DASHBOARD_TITLE) - .click() + getNavigationMenuItem(TEST_DASHBOARD_TITLE).click() }) // Some map visualization load very slowly: diff --git a/cypress/e2e/dashboard_filter/dashboard_filter.js b/cypress/e2e/dashboard_filter/dashboard_filter.js index 693f99d28..2521e9593 100644 --- a/cypress/e2e/dashboard_filter/dashboard_filter.js +++ b/cypress/e2e/dashboard_filter/dashboard_filter.js @@ -2,6 +2,7 @@ import { Then, When } from '@badeball/cypress-cucumber-preprocessor' import { filterBadgeSel, dimensionsModalSel, + filterBadgeDeleteBtnSel, } from '../../elements/dashboardFilter.js' // import { // gridItemSel, @@ -128,7 +129,7 @@ Then('the filter modal is opened', () => { }) When('I remove the {string} filter', () => { - cy.get(filterBadgeSel).find('button').contains('Remove').click() + cy.get(filterBadgeDeleteBtnSel).click() }) Then('the filter is removed from the dashboard', () => { diff --git a/cypress/e2e/edit_dashboard/edit_dashboard.js b/cypress/e2e/edit_dashboard/edit_dashboard.js index abf8b758f..3eeb428cb 100644 --- a/cypress/e2e/edit_dashboard/edit_dashboard.js +++ b/cypress/e2e/edit_dashboard/edit_dashboard.js @@ -9,9 +9,10 @@ import { titleInputSel, clickEditActionButton, } from '../../elements/editDashboard.js' +import { getNavigationMenuItem } from '../../elements/navigationMenu.js' import { - dashboardChipSel, dashboardTitleSel, + dashboardsNavMenuButtonSel, } from '../../elements/viewDashboard.js' import { EXTENDED_TIMEOUT, createDashboardTitle } from '../../support/utils.js' @@ -79,9 +80,8 @@ Then('different valid dashboard displays in view mode', () => { }) Given('I open existing dashboard', () => { - cy.get(dashboardChipSel, EXTENDED_TIMEOUT) - .contains(TEST_DASHBOARD_TITLE) - .click() + cy.get(dashboardsNavMenuButtonSel, EXTENDED_TIMEOUT).click() + cy.get('[role="menu"]').find('li').contains(TEST_DASHBOARD_TITLE).click() cy.location().should((loc) => { const currentRoute = getRouteFromHash(loc.hash) @@ -124,8 +124,7 @@ Scenario: I delete a dashboard */ Then('the dashboard is deleted and first starred dashboard displayed', () => { - cy.get(dashboardChipSel).contains(TEST_DASHBOARD_TITLE).should('not.exist') - + getNavigationMenuItem(TEST_DASHBOARD_TITLE).should('not.exist') cy.get(dashboardTitleSel).should('exist').should('not.be.empty') }) diff --git a/cypress/e2e/edit_dashboard/star_dashboard.js b/cypress/e2e/edit_dashboard/star_dashboard.js index 99abed3e6..a4f6cd6bf 100644 --- a/cypress/e2e/edit_dashboard/star_dashboard.js +++ b/cypress/e2e/edit_dashboard/star_dashboard.js @@ -1,10 +1,12 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor' import { - starSel, + getNavigationMenuItem, + closeNavigationMenu, +} from '../../elements/navigationMenu.js' +import { dashboardStarredSel, dashboardUnstarredSel, - dashboardChipSel, - chipStarSel, + navMenuItemStarIconSel, } from '../../elements/viewDashboard.js' import { TEST_DASHBOARD_TITLE } from './edit_dashboard.js' @@ -12,14 +14,14 @@ import { TEST_DASHBOARD_TITLE } from './edit_dashboard.js' When('I click to star the dashboard', () => { cy.intercept('POST', '**/favorite').as('starDashboard') - cy.get(starSel).click() + cy.get(dashboardUnstarredSel).click() cy.wait('@starDashboard').its('response.statusCode').should('eq', 200) }) When('I click to unstar the dashboard', () => { cy.intercept('DELETE', '**/favorite').as('unstarDashboard') - cy.get(starSel).click() + cy.get(dashboardStarredSel).click() cy.wait('@unstarDashboard').its('response.statusCode').should('eq', 200) }) @@ -28,12 +30,11 @@ Then('the dashboard is starred', () => { cy.get(dashboardStarredSel).should('be.visible') cy.get(dashboardUnstarredSel).should('not.exist') - cy.get(dashboardChipSel) - .contains(TEST_DASHBOARD_TITLE) - .parent() - .siblings(chipStarSel) - .first() + getNavigationMenuItem(TEST_DASHBOARD_TITLE) + .find(navMenuItemStarIconSel) .should('be.visible') + + closeNavigationMenu() }) Then('the dashboard is not starred', () => { @@ -41,9 +42,9 @@ Then('the dashboard is not starred', () => { cy.get(dashboardUnstarredSel).should('be.visible') cy.get(dashboardStarredSel).should('not.exist') - cy.get(dashboardChipSel) - .contains(TEST_DASHBOARD_TITLE) - .parent() - .siblings() + getNavigationMenuItem(TEST_DASHBOARD_TITLE) + .find(navMenuItemStarIconSel) .should('not.exist') + + closeNavigationMenu() }) diff --git a/cypress/e2e/filter_restrict/filter_restrict.js b/cypress/e2e/filter_restrict/filter_restrict.js index a262ba146..2dd028a5a 100644 --- a/cypress/e2e/filter_restrict/filter_restrict.js +++ b/cypress/e2e/filter_restrict/filter_restrict.js @@ -146,7 +146,7 @@ When('I save the dashboard', () => { }) When('I click Add Filter', () => { - clickViewActionButton('Add filter') + clickViewActionButton('Filter') }) Then('I see Facility Ownership and no other dimensions', () => { @@ -168,7 +168,7 @@ Scenario: I restrict filters to no dimensions and do not see Add Filter in dashb */ Then('Add Filter button is not visible', () => { - cy.contains('Add filter').should('not.exist') + cy.containsExact('Filter').should('not.exist') }) When('I delete the dashboard', () => { diff --git a/cypress/e2e/offline/offline.js b/cypress/e2e/offline/offline.js index 767bcefc0..93cd00bb2 100644 --- a/cypress/e2e/offline/offline.js +++ b/cypress/e2e/offline/offline.js @@ -212,7 +212,7 @@ Then( // edit, sharing, starring, filtering, all options under more getViewActionButton('Edit').should('be.disabled') getViewActionButton('Share').should('be.disabled') - getViewActionButton('Add filter').should('be.disabled') + getViewActionButton('Filter').should('be.disabled') getViewActionButton('More').should('be.enabled') checkCorrectMoreOptionsEnabledState(false, cacheState) diff --git a/cypress/e2e/responsive_dashboard/responsive_dashboard.js b/cypress/e2e/responsive_dashboard/responsive_dashboard.js index 7b8bf5b15..c63af7378 100644 --- a/cypress/e2e/responsive_dashboard/responsive_dashboard.js +++ b/cypress/e2e/responsive_dashboard/responsive_dashboard.js @@ -27,10 +27,7 @@ Then('the small screen view is shown', () => { //titlebar - only the More button and the title cy.get('button').contains('Edit').should('not.be.visible') cy.get('button').contains('Share').should('not.be.visible') - cy.get('button').contains('Add filter').should('not.be.visible') - - cy.get('button.small').contains('More').should('be.visible') - cy.get('button').not('.small').contains('More').should('not.be.visible') + cy.get('button').contains('Filter').should('not.be.visible') }) When('I restore the wide screen', () => { @@ -44,10 +41,7 @@ Then('the wide screen view is shown', () => { cy.get('button').contains('Edit').should('be.visible') cy.get('button').contains('Share').should('be.visible') - cy.get('button').contains('Add filter').should('be.visible') - - cy.get('button').not('.small').contains('More').should('be.visible') - cy.get('button.small').contains('More').should('not.be.visible') + cy.get('button').contains('Filter').should('be.visible') }) Then('the small screen edit view is shown', () => { diff --git a/cypress/e2e/slideshow.feature b/cypress/e2e/slideshow.feature new file mode 100644 index 000000000..ff2c4c2ec --- /dev/null +++ b/cypress/e2e/slideshow.feature @@ -0,0 +1,31 @@ +Feature: Slideshow + + Scenario: I view a dashboard in slideshow + Given I open the "Delivery" dashboard + When I click the slideshow button + Then item 1 is shown in fullscreen + When I click the next slide button + Then item 2 is shown in fullscreen + When I click the previous slide button + Then item 1 is shown in fullscreen + When I click the exit slideshow button + Then the normal view is shown + + + Scenario: I view fullscreen on the second item of the dashboard + Given I open the "Delivery" dashboard + When I click the fullscreen button on the second item + Then item 2 is shown in fullscreen + When I click the exit slideshow button + Then the normal view is shown + + Scenario: I view fullscreen on the third item of the dashboard and navigate backwards + Given I open the "Delivery" dashboard + When I click the fullscreen button on the third item + Then item 3 is shown in fullscreen + When I click the previous slide button + Then item 2 is shown in fullscreen + When I click the previous slide button + Then item 1 is shown in fullscreen + When I click the exit slideshow button + Then the normal view is shown diff --git a/cypress/e2e/slideshow/index.js b/cypress/e2e/slideshow/index.js new file mode 100644 index 000000000..6a8a6a12a --- /dev/null +++ b/cypress/e2e/slideshow/index.js @@ -0,0 +1 @@ +'../common/index.js' diff --git a/cypress/e2e/slideshow/slideshow.js b/cypress/e2e/slideshow/slideshow.js new file mode 100644 index 000000000..254dd3976 --- /dev/null +++ b/cypress/e2e/slideshow/slideshow.js @@ -0,0 +1,103 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor' +import { + getDashboardItem, + clickMenuButton, +} from '../../elements/dashboardItem.js' + +const sortedDashboardItemIds = ['GaVhJpqABYX', 'qXsjttMYuoZ', 'Rwb3oXJ3bZ9'] + +const assertItemIsVisible = (slideshowItemIndex) => { + getDashboardItem(sortedDashboardItemIds[slideshowItemIndex]).should( + 'have.css', + 'opacity', + '1' + ) +} + +const assertItemIsNotVisible = (slideshowItemIndex) => { + getDashboardItem(sortedDashboardItemIds[slideshowItemIndex]).should( + 'have.css', + 'opacity', + '0' + ) +} + +const getSlideshowExitButton = () => + cy.getByDataTest('slideshow-exit-button', { timeout: 15000 }) + +When('I click the slideshow button', () => { + cy.get('button').contains('Slideshow').realClick() +}) + +Then('item 1 is shown in fullscreen', () => { + getSlideshowExitButton().should('be.visible') + + // check that only the first item is shown + assertItemIsVisible(0) + assertItemIsNotVisible(1) + assertItemIsNotVisible(2) + + cy.getByDataTest('slideshow-page-counter').should('have.text', '1 / 11') + + // visible item does not have context menu button + getDashboardItem(sortedDashboardItemIds[0]) + .findByDataTest('dashboarditem-menu-button') + .should('not.exist') +}) + +When('I click the exit slideshow button', () => { + getSlideshowExitButton().realClick() +}) + +Then('the normal view is shown', () => { + getSlideshowExitButton().should('not.exist') + + // check that multiple items are shown + assertItemIsVisible(0) + assertItemIsVisible(1) + assertItemIsVisible(2) + + // items have context menu button + getDashboardItem(sortedDashboardItemIds[0]) + .findByDataTest('dashboarditem-menu-button') + .should('be.visible') + + getDashboardItem(sortedDashboardItemIds[1]) + .findByDataTest('dashboarditem-menu-button') + .should('be.visible') +}) + +// When I click the next slide button +When('I click the next slide button', () => { + cy.getByDataTest('slideshow-next-button').realClick() +}) + +Then('item 2 is shown in fullscreen', () => { + assertItemIsNotVisible(0) + assertItemIsVisible(1) + assertItemIsNotVisible(2) + + cy.getByDataTest('slideshow-page-counter').should('have.text', '2 / 11') +}) + +When('I click the previous slide button', () => { + cy.getByDataTest('slideshow-prev-button').realClick() +}) + +When('I click the fullscreen button on the second item', () => { + clickMenuButton(sortedDashboardItemIds[1]) + cy.contains('View fullscreen').realClick() +}) + +When('I click the fullscreen button on the third item', () => { + clickMenuButton(sortedDashboardItemIds[2]) + cy.contains('View fullscreen').realClick() +}) + +Then('item 3 is shown in fullscreen', () => { + assertItemIsNotVisible(0) + assertItemIsNotVisible(1) + assertItemIsVisible(2) + + cy.getByDataTest('slideshow-page-counter').should('have.text', '3 / 11') +}) diff --git a/cypress/e2e/view_dashboard.feature b/cypress/e2e/view_dashboard.feature index 4ecf38f00..6057b23e6 100644 --- a/cypress/e2e/view_dashboard.feature +++ b/cypress/e2e/view_dashboard.feature @@ -11,7 +11,7 @@ Feature: Viewing dashboards Given I open the "Antenatal Care" dashboard When I search for dashboards containing "Immun" Then Immunization and Immunization data dashboards are choices - When I press enter in the search dashboard field + When I press tab in the search dashboard field and then enter Then the "Immunization" dashboard displays in view mode @nonmutating @@ -19,8 +19,6 @@ Feature: Viewing dashboards Given I open the "Antenatal Care" dashboard When I search for dashboards containing "Noexist" Then no dashboards are choices - When I press enter in the search dashboard field - Then dashboards list restored and dashboard is still "Antenatal Care" @nonmutating Scenario: I view the print layout preview and then print one-item-per-page preview @@ -39,21 +37,6 @@ Feature: Viewing dashboards Given I open the "Delivery" dashboard with shapes removed Then the "Delivery" dashboard displays in view mode - @nonmutating - Scenario: I expand the control bar - Given I open the "Delivery" dashboard - Then the control bar should be at collapsed height - When I toggle show more dashboards - Then the control bar should be expanded to full height - - @nonmutating - Scenario: I expand the control bar when dashboard not found - Given I type an invalid dashboard id in the browser url - Then a message displays informing that the dashboard is not found - And the control bar should be at collapsed height - When I toggle show more dashboards - Then the control bar should be expanded to full height - # @nonmutating # FIXME # Scenario: Maps with tracked entities show layer names in legend diff --git a/cypress/e2e/view_dashboard/dashboard_items_without_shape.js b/cypress/e2e/view_dashboard/dashboard_items_without_shape.js index 128ed7825..284b8c37a 100644 --- a/cypress/e2e/view_dashboard/dashboard_items_without_shape.js +++ b/cypress/e2e/view_dashboard/dashboard_items_without_shape.js @@ -1,7 +1,6 @@ import { Given } from '@badeball/cypress-cucumber-preprocessor' import { dashboards } from '../../assets/backends/index.js' -import { dashboardChipSel } from '../../elements/viewDashboard.js' -import { EXTENDED_TIMEOUT } from '../../support/utils.js' +import { getNavigationMenuItem } from '../../elements/navigationMenu.js' Given('I open the {string} dashboard with shapes removed', (title) => { const regex = new RegExp(`dashboards/${dashboards[title].id}`, 'g') @@ -17,5 +16,5 @@ Given('I open the {string} dashboard with shapes removed', (title) => { res.send({ body: res.body }) }) }) - cy.get(dashboardChipSel, EXTENDED_TIMEOUT).contains(title).click() + getNavigationMenuItem(title).click() }) diff --git a/cypress/e2e/view_dashboard/open_dashboard_app.js b/cypress/e2e/view_dashboard/open_dashboard_app.js index 531eb937b..7786dc3d5 100644 --- a/cypress/e2e/view_dashboard/open_dashboard_app.js +++ b/cypress/e2e/view_dashboard/open_dashboard_app.js @@ -1,8 +1,5 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor' -import { - dashboardTitleSel, - dashboardChipSel, -} from '../../elements/viewDashboard.js' +import { dashboardTitleSel } from '../../elements/viewDashboard.js' import { EXTENDED_TIMEOUT } from '../../support/utils.js' When('I open the dashboard app with the root url', () => { @@ -13,7 +10,6 @@ When('I open the dashboard app with the root url', () => { }) cy.get(dashboardTitleSel).should('be.visible') - cy.get(dashboardChipSel, EXTENDED_TIMEOUT).should('be.visible') }) Then('the {string} dashboard displays', (title) => { diff --git a/cypress/e2e/view_dashboard/print.js b/cypress/e2e/view_dashboard/print.js index ddd1a9329..eae166b20 100644 --- a/cypress/e2e/view_dashboard/print.js +++ b/cypress/e2e/view_dashboard/print.js @@ -1,9 +1,8 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor' import { dashboards } from '../../assets/backends/sierraLeone_236.js' -import { clickViewActionButton } from '../../elements/viewDashboard.js' When('I click to preview the print one-item-per-page', () => { - clickViewActionButton('More') + cy.get('[data-test="more-actions-button"]').click() cy.get('[data-test="print-menu-item"]').click() cy.get('[data-test="print-oipp-menu-item"]').click() }) diff --git a/cypress/e2e/view_dashboard/resize_dashboards_bar.js b/cypress/e2e/view_dashboard/resize_dashboards_bar.js deleted file mode 100644 index 6906c746d..000000000 --- a/cypress/e2e/view_dashboard/resize_dashboards_bar.js +++ /dev/null @@ -1,54 +0,0 @@ -import { When, Then } from '@badeball/cypress-cucumber-preprocessor' -import { - dragHandleSel, - dashboardsBarSel, -} from '../../elements/viewDashboard.js' -import { EXTENDED_TIMEOUT } from '../../support/utils.js' - -const RESP_CODE_200 = 200 -const RESP_CODE_201 = 201 - -// 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).as('dragHandleSel') - - cy.get('@dragHandleSel').trigger('mousedown') - cy.get('@dragHandleSel').trigger('mousemove', { clientY: 300 }) - cy.get('@dragHandleSel').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 - // eslint-disable-next-line cypress/unsafe-to-chain-command - cy.get(dragHandleSel) - .trigger('mousedown') - .trigger('mousemove', { clientY: 71 }) - .trigger('mouseup') - cy.wait('@putRows') - .its('response.statusCode') - .should('be.oneOf', [RESP_CODE_200, RESP_CODE_201]) -}) - -When('I drag to decrease the height of the control bar', () => { - cy.intercept('PUT', '**/userDataStore/dashboard/controlBarRows').as( - 'putRows' - ) - cy.get(dragHandleSel, EXTENDED_TIMEOUT).as('dragHandleSel') - - cy.get('@dragHandleSel').trigger('mousedown') - cy.get('@dragHandleSel').trigger('mousemove', { clientY: 300 }) - cy.get('@dragHandleSel').trigger('mouseup') - cy.wait('@putRows') - .its('response.statusCode') - .should('be.oneOf', [RESP_CODE_200, RESP_CODE_201]) -}) diff --git a/cypress/e2e/view_dashboard/search_for_dashboard.js b/cypress/e2e/view_dashboard/search_for_dashboard.js index 3d958a4a9..c532817b2 100644 --- a/cypress/e2e/view_dashboard/search_for_dashboard.js +++ b/cypress/e2e/view_dashboard/search_for_dashboard.js @@ -1,40 +1,31 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor' -import { dashboards } from '../../assets/backends/sierraLeone_236.js' -// import { gridItemSel } from '../../elements/dashboardItem.js' import { - dashboardTitleSel, - dashboardChipSel, - dashboardSearchInputSel, -} from '../../elements/viewDashboard.js' + getNavigationMenuFilter, + getNavigationMenu, +} from '../../elements/navigationMenu.js' When('I search for dashboards containing {string}', (title) => { - cy.get(dashboardSearchInputSel).type(title) + getNavigationMenuFilter().type(title) }) Then('Immunization and Immunization data dashboards are choices', () => { - cy.get(dashboardChipSel).should('be.visible').and('have.length', 2) + getNavigationMenu(true) + .find('li') + .should('be.visible') + .and('have.length', 2) +}) +When('I press tab in the search dashboard field and then enter', () => { + cy.realPress('Tab') + cy.realPress('Enter') }) When('I press enter in the search dashboard field', () => { - cy.get(dashboardSearchInputSel).type('{enter}') + getNavigationMenuFilter(true).type('{enter}') }) Then('no dashboards are choices', () => { - cy.get(dashboardChipSel).should('not.exist') -}) - -Then('dashboards list restored and dashboard is still {string}', (title) => { - cy.get(dashboardChipSel).should('be.visible').and('have.lengthOf.above', 0) - - cy.location().should((loc) => { - expect(loc.hash).to.equal(dashboards[title].route) - }) - - cy.get(dashboardTitleSel).should('be.visible').and('contain', title) - // FIXME - // cy.get(`${gridItemSel}.VISUALIZATION`) - // .first() - // .getIframeBody() - // .find('.highcharts-background') - // .should('exist') + getNavigationMenu(true) + .find('li') + .contains('No dashboards found') + .should('be.visible') }) diff --git a/cypress/e2e/view_dashboard/toggle_show_more_dashboards.js b/cypress/e2e/view_dashboard/toggle_show_more_dashboards.js index c9c8f5edc..9a48903e2 100644 --- a/cypress/e2e/view_dashboard/toggle_show_more_dashboards.js +++ b/cypress/e2e/view_dashboard/toggle_show_more_dashboards.js @@ -1,12 +1,4 @@ -import { When, Then } from '@badeball/cypress-cucumber-preprocessor' -import { - dashboardsBarContainerSel, - showMoreLessSel, -} from '../../elements/viewDashboard.js' -import { getApiBaseUrl, EXTENDED_TIMEOUT } from '../../support/utils.js' - -const MIN_DASHBOARDS_BAR_HEIGHT = 71 -const MAX_DASHBOARDS_BAR_HEIGHT = 431 +import { getApiBaseUrl } from '../../support/utils.js' const RESP_CODE_200 = 200 const RESP_CODE_201 = 201 @@ -23,19 +15,3 @@ beforeEach(() => { expect(response.status).to.be.oneOf([RESP_CODE_201, RESP_CODE_200]) ) }) - -When('I toggle show more dashboards', () => { - cy.get(showMoreLessSel).click() -}) - -Then('the control bar should be at collapsed height', () => { - cy.get(dashboardsBarContainerSel, EXTENDED_TIMEOUT) - .invoke('height') - .should('eq', MIN_DASHBOARDS_BAR_HEIGHT) -}) - -Then('the control bar should be expanded to full height', () => { - cy.get(dashboardsBarContainerSel, EXTENDED_TIMEOUT) - .invoke('height') - .should('eq', MAX_DASHBOARDS_BAR_HEIGHT) -}) diff --git a/cypress/e2e/view_errors.feature b/cypress/e2e/view_errors.feature index cb518deb5..43d602cb1 100644 --- a/cypress/e2e/view_errors.feature +++ b/cypress/e2e/view_errors.feature @@ -19,6 +19,11 @@ Feature: Errors while in view mode When I open the "Delivery" dashboard Then the "Delivery" dashboard displays in view mode + @nonmutating + Scenario: I navigate to a dashboard that fails to load + Given I type a dashboard id in the browser url that fails to load + Then a warning message is displayed stating that the dashboard could not be loaded + # @nonmutating # Scenario: I navigate to print dashboard that doesn't exist # Given I type an invalid print dashboard id in the browser url diff --git a/cypress/e2e/view_errors/dashboard_item_missing_type.js b/cypress/e2e/view_errors/dashboard_item_missing_type.js index 83247188d..ab62883da 100644 --- a/cypress/e2e/view_errors/dashboard_item_missing_type.js +++ b/cypress/e2e/view_errors/dashboard_item_missing_type.js @@ -3,11 +3,8 @@ import { getDashboardItem, clickItemDeleteButton, } from '../../elements/dashboardItem.js' -import { - dashboardChipSel, - dashboardTitleSel, -} from '../../elements/viewDashboard.js' -import { EXTENDED_TIMEOUT } from '../../support/utils.js' +import { getNavigationMenuItem } from '../../elements/navigationMenu.js' +import { dashboardTitleSel } from '../../elements/viewDashboard.js' const ITEM_1_UID = 'GaVhJpqABYX' const ITEM_2_UID = 'qXsjttMYuoZ' @@ -38,7 +35,7 @@ const interceptDashboardRequest = () => { Given('I open the Delivery dashboard with items missing a type', () => { interceptDashboardRequest() - cy.get(dashboardChipSel, EXTENDED_TIMEOUT).contains('Delivery').click() + getNavigationMenuItem('Delivery').click() cy.get(dashboardTitleSel).should('be.visible').and('contain', 'Delivery') }) diff --git a/cypress/e2e/view_errors/error_while_fetching_dashboard_details.js b/cypress/e2e/view_errors/error_while_fetching_dashboard_details.js new file mode 100644 index 000000000..9569c589e --- /dev/null +++ b/cypress/e2e/view_errors/error_while_fetching_dashboard_details.js @@ -0,0 +1,21 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor' + +When('I type a dashboard id in the browser url that fails to load', () => { + cy.intercept('**/dashboards/iMnYyBfSxmM**', { + statusCode: 500, + body: 'Oopsie!', + }).as('failure') + + cy.visit('#/iMnYyBfSxmM') + cy.wait('@failure') +}) + +Then( + 'a warning message is displayed stating that the dashboard could not be loaded', + () => { + cy.contains('Load dashboard failed').should('exist') + cy.contains( + 'This dashboard could not be loaded. Please try again later.' + ).should('exist') + } +) diff --git a/cypress/e2e/view_errors/error_while_starring_dashboard.js b/cypress/e2e/view_errors/error_while_starring_dashboard.js index 5e836daae..50b13f268 100644 --- a/cypress/e2e/view_errors/error_while_starring_dashboard.js +++ b/cypress/e2e/view_errors/error_while_starring_dashboard.js @@ -1,10 +1,8 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor' import { dashboards } from '../../assets/backends/index.js' import { - starSel, dashboardUnstarredSel, dashboardStarredSel, - dashboardChipSel, } from '../../elements/viewDashboard.js' When('clicking to star {string} dashboard fails', (title) => { @@ -13,7 +11,7 @@ When('clicking to star {string} dashboard fails', (title) => { statusCode: 409, }).as('starDashboardFail') - cy.get(starSel).click() + cy.get(dashboardUnstarredSel).click() cy.wait('@starDashboardFail').its('response.statusCode').should('eq', 409) }) @@ -28,10 +26,8 @@ Then( } ) -Then('the {string} dashboard is not starred', (title) => { +Then('the {string} dashboard is not starred', () => { // check for the unfilled star next to the title cy.get(dashboardUnstarredSel).should('be.visible') cy.get(dashboardStarredSel).should('not.exist') - - cy.get(dashboardChipSel).contains(title).siblings().should('not.exist') }) diff --git a/cypress/e2e/view_errors/item_chart_fails_to_render.js b/cypress/e2e/view_errors/item_chart_fails_to_render.js index 9bf1241b4..d3139d5f2 100644 --- a/cypress/e2e/view_errors/item_chart_fails_to_render.js +++ b/cypress/e2e/view_errors/item_chart_fails_to_render.js @@ -41,7 +41,7 @@ Given('I open a dashboard with a chart that will fail', () => { When( 'I apply a {string} filter of type {string}', (dimensionType, filterName) => { - cy.contains('Add filter').click() + cy.containsExact('Filter').click() cy.get(filterDimensionsPanelSel).contains(dimensionType).click() cy.get(dimensionsModalSel, EXTENDED_TIMEOUT).should('be.visible') diff --git a/cypress/elements/dashboardFilter.js b/cypress/elements/dashboardFilter.js index e614774e5..13dcdcbab 100644 --- a/cypress/elements/dashboardFilter.js +++ b/cypress/elements/dashboardFilter.js @@ -1,5 +1,7 @@ export const filterBadgeSel = '[data-test="dashboard-filter-badge"]' +export const filterBadgeDeleteBtnSel = '[data-test="filter-badge-clear-button"]' + export const filterDimensionsPanelSel = '[data-test="dashboard-filter-popover"]' export const unselectedItemsSel = diff --git a/cypress/elements/navigationMenu.js b/cypress/elements/navigationMenu.js new file mode 100644 index 000000000..3459458be --- /dev/null +++ b/cypress/elements/navigationMenu.js @@ -0,0 +1,24 @@ +export const getNavigationMenuDropdown = () => + cy.get('[data-test="dashboards-nav-menu-button"]') + +export const getNavigationMenu = (isOpen = false) => { + if (!isOpen) { + getNavigationMenuDropdown().click() + } + return cy.get('[role="menu"]') +} + +export const getNavigationMenuItem = (dashboardDisplayName, isOpen) => + getNavigationMenu(isOpen).find('li').contains(dashboardDisplayName) + +export const closeNavigationMenu = () => { + cy.get('.backdrop').click() + cy.get('.backdrop').should('not.exist') +} + +export const getNavigationMenuFilter = (isOpen) => { + if (!isOpen) { + getNavigationMenuDropdown().click() + } + return cy.get('input:visible[placeholder="Search for a dashboard"]') +} diff --git a/cypress/elements/viewDashboard.js b/cypress/elements/viewDashboard.js index 6c23b6487..ed7ef6e0a 100644 --- a/cypress/elements/viewDashboard.js +++ b/cypress/elements/viewDashboard.js @@ -4,19 +4,15 @@ import { EXTENDED_TIMEOUT } from '../support/utils.js' // Dashboards bar export const dashboardChipSel = '[data-test="dashboard-chip"]' +export const dashboardsNavMenuButtonSel = + '[data-test="dashboards-nav-menu-button"]' export const newButtonSel = '[data-test="new-button"]' -export const chipStarSel = '[data-test="dhis2-uicore-chip-icon"]' -export const dashboardSearchInputSel = - 'input:visible[placeholder="Search for a dashboard"]' -export const showMoreLessSel = '[data-test="showmore-button"]' -export const dragHandleSel = '[data-test="controlbar-drag-handle"]' +export const navMenuItemStarIconSel = '[data-test="starred-dashboard"]' export const dashboardsBarSel = '[data-test="dashboards-bar"]' // Active dashboard export const dashboardTitleSel = '[data-test="view-dashboard-title"]' -export const dashboardsBarContainerSel = '[data-test="dashboardsbar-container"]' 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 titleBarSel = '[data-test="title-bar"]' diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 7045ae0f0..7a4034227 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,5 +1,6 @@ // import '@dhis2/cypress-commands' import { enableAutoLogin } from '@dhis2/cypress-commands' +import 'cypress-real-events' import './commands.js' enableAutoLogin() @@ -51,9 +52,14 @@ before(() => { beforeEach(() => { const baseUrl = Cypress.env('dhis2BaseUrl') const instanceVersion = Cypress.env('dhis2InstanceVersion') + const hideRequestsFromLog = Cypress.env('hideRequestsFromLog') const envVariableName = computeEnvVariableName(instanceVersion) const { name, value, ...options } = JSON.parse(Cypress.env(envVariableName)) + if (hideRequestsFromLog) { + // disable Cypress's default behavior of logging all XMLHttpRequests and fetches + cy.intercept({ resourceType: /xhr|fetch/ }, { log: false }) + } localStorage.setItem(LOCAL_STORAGE_KEY, baseUrl) cy.setCookie(name, value, options) diff --git a/docs/dashboards.md b/docs/dashboards.md index b19b4a9e2..b5902fb64 100644 --- a/docs/dashboards.md +++ b/docs/dashboards.md @@ -2,11 +2,12 @@ ## About the Dashboards app -The Dashboards app makes it possible to present a high level overview of your data, including displaying analytical objects such as maps, charts, reports and tables, as well as displaying text-based information, resource links, and app widgets. +The Dashboards app makes it possible to present a high level overview of your data, including displaying analytical objects such as maps, charts, reports, tables, and line lists, as well as displaying text-based information, resource links, and app widgets. Features of the Dashboards app include: - View and print dashboards +- Slideshow - Create and edit dashboards - Share dashboards with users and user groups - Apply temporary filters while viewing dashboards @@ -15,60 +16,55 @@ Features of the Dashboards app include: ## Dashboards app layout { #dashboards_setup } -Dashboards have a title, description, and any number of dashboard items. Above the dashboard is the dashboards bar, which shows all your available dashboards, a dashboard search field, and a **+** button for creating a new dashboard. +Dashboards have a title, description, dashboard items. The title and actions bar is at the top of the page under the header bar, and shows the title, a dropdown for searching and selecting dashboards, and actions that can be taken with the current dashboard. There is also a **+** button for creating a new dashboard. The Dashboards app has two modes: _view_ and _edit/create_. When you first log in to DHIS2, your most recently used dashboard will be displayed in view mode, if you are on the same computer as you were previously. If you are using a different computer or browser, then the first starred dashboard will be displayed. If there are no starred dashboards, then the first dashboard (alphabetically) will be displayed. Starred dashboards always show first in the dashboard list. -Below is an example of a dashboard named "Antenatal Care", which has been populated with charts and maps: +Below is a dashboard named "Antenatal Care", which has been populated with charts and maps: ![](resources/images/dashboard-view-mode.png) ### Personalization -The Dashboards app can be personalized in the following ways: +To adjust the Dashboards app to suit your needs, you can: -- [Set the height of the dashboards bar](#dashboards_personalize_bar) - [Star dashboards for quick access to your favorite dashboards](#dashboard-star-dashboard) - [Show or hide dashboard description](#dashboard-show-description) ### Responsive view on small screens -When viewing dashboards on small screens (for instance, portrait orientation on a mobile phone ), the dashboard will adapt to the screen and show all items in a single column. Some options, including editing, filtering and sharing, will not be available. +When viewing dashboards on small screens like mobile phones, the dashboard will adapt to the screen and show all items in a single column. Some options, including editing, filtering and sharing, will not be available. -![](resources/images/dashboard-small-screen.png) +Dashboard small screen ### Searching for a dashboard -You can search for a specific dashboard using the search field in the upper left of the dashboards bar entitled “Search for a dashboard”. The search is case insensitive, and as you type, the list of dashboards will be narrowed down to those that match your search text. +You can search for a specific dashboard using the search field available from the Dashboards dropdown selector in the title bar. The search is case insensitive, and as you type, the list of dashboards will be narrowed down to those that match your search text. -![](resources/images/dashboard-search-for-dashboard.png) - -### Personalizing the height of the dashboards bar { #dashboards_personalize_bar } - -You can set a specific height for the dashboards bar by -down-clicking and dragging the bottom edge of the bar. When you finish dragging, the new height will be set. Clicking on the down arrow at the bottom of the dashboards bar will expand the bar to its maximum height (10 "rows"). Clicking on the up arrow will reset the height to your personalized height. +![](resources/images/dashboard-list-and-filter.png) ## Creating and editing a dashboard -To create a new dashboard, click the **+** button in the left corner of the dashboards bar to enter create/edit mode: +To create a new dashboard, click the **+** button in the upper corner of the title bar to enter create/edit mode: ![](resources/images/dashboard-new-button.png) -To edit an existing dashboard, click the **Edit** button next to the dashboard title (you must have edit access to see this button): +To edit an existing dashboard, click the **Edit** button (you must have edit access to see this button): -![](resources/images/dashboard-title-bar.png) +![](resources/images/dashboard-edit-button.png) -In create/edit mode, you can add or change the dashboard title, description and dashboard items. If you do not add a title, the dashboard will automatically be titled "Untitled dashboard". +In create/edit mode, you can add or change the dashboard title, description, dashboard code and dashboard items. If you do not add a title, the dashboard will automatically be titled "Untitled dashboard". ![](resources/images/dashboard-create-mode.png) ### Adding items to the dashboard -Add items to the dashboard by searching for items using the **Search for items to add to this dashboard** drop down selector. Item types are: +Add items to the dashboard by searching for items using the **Search for items to add to this dashboard** dropdown selector. Item types are: -- Visualizations (charts and tables) +- Visualizations (charts and pivot tables) - Maps +- Line lists - Event reports - Event charts - Reports @@ -80,14 +76,14 @@ Add items to the dashboard by searching for items using the **Search for items t ![](resources/images/dashboard-item-selector.png) -The list of items in the drop-down initially displays 10 visualizations (charts and tables), and 5 from each of the other categories, based on the search text you enter. Messages (Email), text boxes and spacer items are also found in the list. To view more items, click on **Show more**, and the list for that type will be extended to 25 items. If you still do not find the item you want, try typing a more specific search text. +The list of items in the dropdown initially displays 10 visualizations (charts and tables), and 5 from each of the other categories, based on the search text you enter. To view more items, click on **Show more**, and the list for that type will be extended to 25 items. If you still do not find the item you want, try typing a more specific search text. Messages (Email), text boxes and spacer items can also be chosen from the list. #### Dashboard layout and placement of new items When adding items to the dashboard you can choose an overall layout by clicking on **Change layout** button. You can change this layout setting at any time. - With _Freeflow_ layout, the added items can be moved using the mouse by down-clicking on the item and dragging it to the desired position. Items can also be resized with the mouse by down-clicking on the drag handle in the lower right corner of the item and dragging to the desired size. -- With _Fixed columns_ layout, you can choose the number of columns to have on the dashboard, and the dashboard will automatically be layed out for you. Items cannot be moved or resized in _Fixed columns_ layout. +- With _Fixed columns_ layout, you can choose the number of columns to have on the dashboard, and the dashboard will automatically be layed out for you. Items cannot be moved or resized in _Fixed columns_ layout. If you want to make custom adjustments to a _Fixed columns_ layout, return to a _Freeflow_ layout. ![](resources/images/dashboard-layout-modal.png) @@ -109,13 +105,13 @@ Spacer in **view mode**: #### Removing items -Remove items by clicking on the red trash can at the upper right of the item. Be aware that when you remove an item while in _Freeflow_ layout, the items that are positioned below the removed item will "rise" upwards until they bump into an item above. +Remove items by clicking on the red trash can at the upper right of the item. Be aware that when you remove an item while in _Freeflow_ layout, the items that are positioned below the removed item will "rise" upwards until they bump into an item above. in _Fixed columns_ layout, the items will be adjusted to fill every column in the layout so there are no empty column slots. ### Actions in create/edit mode In create/edit mode you will see the following buttons in the actions bar at the top of the page: **Save changes**, **Print preview**, **Filter settings**, **Translate**, **Delete**, and **Exit without saving**. The **Translate** and **Delete** buttons are only shown if you are editing an existing dashboard. -![](resources/images/dashboard-edit-mode-actions.png) +![](resources/images/dashboard-edit-anc.png) ### Saving the dashboard @@ -137,7 +133,7 @@ By default, users will be able to filter dashboard items by any dimension define To restrict available filters, you can click **Only allow filtering by selected dimensions** and select the filters you wish to allow on the dashboard. Period and Organisation Unit are selected by default but can be removed if desired. When the dashboard is viewed, users will only be able to choose from among the filters selected. -![](resources/images/dashboard-filter-settings.png) +Dashboard filter settings In order to save updates to filter settings, you need to first click **Confirm** to close the Filter settings dialog and then click **Save changes** to save the dashboard changes. @@ -145,41 +141,50 @@ In order to save updates to filter settings, you need to first click **Confirm** ### Translating dashboard title and description -If you are editing an existing dashboard, then there will be a **Translate** button. Click on this button to open the Translation dialog, which provides a list of languages to translate to, and shows the original dashboard title underneath the name input field. First choose the language you want to translate for, then fill in the dashboard name and description translation. +If you are editing an existing dashboard, then there will be a **Translate** button. Click on this button to open the Translation dialog, which provides a list of languages to translate to, and shows the original dashboard title and description. First choose the language you want to translate for, then fill in the dashboard name and description translation. -![](resources/images/dashboard-translation-dialog.png) +Dashboard translation dialog ### Deleting a dashboard If you have access to delete the dashboard, then there will be a **Delete** button. When you click the **Delete** button, a confirmation dialog will first be displayed to confirm that you want to delete the dashboard. -![](resources/images/dashboard-confirm-delete.png) +Dashboard confirm delete ## Viewing a dashboard -From view mode, you can toggle showing the description, star a dashboard, apply filters, print the dashboard, make the dashboard available offline, and share the dashboard with other users and user groups. +The following actions are available on the dashboard in view mode: + +- Set the show/hide description setting +- Star the dashboard so it appears first in the dashboard list +- Filter the dashboard +- Print the dashboard +- Display the dashboard in a slideshow +- Make the dashboard available offline +- Share the dashboard with other users and user groups +- Close the dashboard ![](resources/images/dashboard-more-menu.png) ### Show description { #dashboard-show-description } -To toggle the description, open the **...More** menu and choose **Show description** (or **Hide description**). This setting will be remembered for all dashboards that you open. This setting applies to you, not other users. +To toggle the description, open the **...** menu and choose **Show description** (or **Hide description**). This setting will be remembered for all dashboards that you open. This setting applies to you, not other users. ### Star dashboards { #dashboard-star-dashboard } -Your starred dashboards are listed first in the list of dashboards for quick access. To star a dashboard, click on the star button to the right of the title. You can also toggle the star from the **...More** menu. When the star is “filled”, that means the dashboard is starred. Starring a dashboard only applies to you, not other users. +Your starred dashboards are listed first in the list of dashboards for quick access. To star a dashboard, click on the star button to the right of the title. You can also toggle the star from the **...** menu. When the star is “filled”, that means the dashboard is starred. Starring a dashboard only applies to you, not other users. -### Filtering a dashboard +### Filter a dashboard Applying filters to a dashboard change the data displayed in dashboard items containing visualizations. The filters are applied to each dashboard item in the same way: each added filter overrides the original value for that dimension in the original chart, table or map. It is possible to filter on Organisation Units and Periods, as well as dynamic dimensions, depending on the DHIS2 instance. You can apply multiple filters to the dashboard. -To add a filter, click on the **Add Filter** button and choose a dimension: +To add a filter, click on the **Filter** button and choose a dimension: ![Adding a filter](resources/images/dashboard-filters.png) A dialog opens where the filter selection can be made. -![Org Unit filter selection](resources/images/dashboard-orgunit-filter-dialog.png) +![Org Unit filter selection](resources/images/dashboard-period-filter-dialog.png) Click on **Confirm** in the dialog to apply the filter to the current dashboard. @@ -192,23 +197,31 @@ You can edit a filter by clicking on the filter badge to open the filter selecti By default, users are able to filter dashboard items by any dimension defined in the DHIS2 instance. To limit available filters, see [Restricting dashboard filters](#restricting-dashboard-filters). -### Making dashboards available offline +### Display the dashboard in a slideshow { #dashboard-slideshow } + +The dashboard can be displayed in a slideshow by clicking on the **Slideshow** button. + +![Slideshow button](resources/images/dashboard-slideshow-button.png) -To make a dashboard available offline, choose the **Make dashboard available offline** option in the **...More** menu. This will cause a reload of the dashboard where requests to the server are recorded and saved in browser storage. Note that offline dashboards are only available on the computer and browser where you set it to offline. If you currently have a filter applied when requesting the dashboard be made available offline, a dialog will appear to confirm the removal of the filters. +When you enter the slideshow, you'll find navigation buttons and an exit button in a navigation bar at the bottom of the page. You can also navigate with the forward and back arrow keys on the keyboard, and exit the slideshow with the **esc** key. Any filters that are applied will be displayed in the navigation bar. Note that messages and spacer items are not displayed in the slideshow. + +![Slideshow navigation bar](resources/images/dashboard-slideshow-navbar.png) + +### Make dashboards available offline + +To make a dashboard available offline, choose the **Make available offline** option in the **...** menu. This will cause a reload of the dashboard where requests to the server are recorded and saved in browser storage. Note that offline dashboards are only available on the computer and browser where you set it to offline. If you currently have a filter applied when requesting the dashboard be made available offline, a dialog will appear to confirm the removal of the filters. ![](resources/images/dashboard-clear-filters-to-sync.png) -Dashboards that have been saved for offline have an indicator on the dashboard chip in the dashboards bar, as well as a tag showing the time it was saved. +Dashboards that have been saved for offline display a tag next to the dashboard title showing the time it was saved. In the dashboard selector, an icon is displayed if the dashboard is available offline. ![](resources/images/dashboard-offline-dashboard.png) -If the dashboard has been changed since you made it available offline, either by you or someone else, you'll need to choose **Sync offline data now** from the **...More** menu to save the latest version of the dashboard. - -![](resources/images/dashboard-sync-offline.png) +If the dashboard has been changed since you made it available offline, either by you or someone else, you'll need to choose **Sync offline data now** from the **...** menu to save the latest version of the dashboard. -You can remove a dashboard from offline storaged by choosing **Remove from offline storage** in the **...More** menu. +You can remove a dashboard from offline storaged by choosing **Remove from offline storage** in the **...** menu. -![](resources/images/dashboard-remove-offline.png) +![](resources/images/dashboard-sync-remove-offline.png) #### Other notes about Dashboards app when you are offline: @@ -218,7 +231,7 @@ If you are offline, any buttons or actions that require a connection to complete ### Printing a dashboard -From the **...More** menu you can print the current dashboard. There are two styles of dashboard print: _Dashboard layout_ and _One item per page_. For both styles, a title page is added that shows the dashboard title, description (if the _Show description_ setting is enabled), and any applied dashboard filters. +From the **...** menu you can print the current dashboard. There are two styles of dashboard print: _Dashboard layout_ and _One item per page_. For both styles, a title page is added that shows the dashboard title, description (if the _Show description_ setting is enabled), and any applied dashboard filters. ![](resources/images/dashboard-print-menu.png) @@ -244,19 +257,19 @@ Click on the **Print** button in the upper right to trigger the browser print fu ![](resources/images/dashboard-print-oipp.png) -## Dashboard items with charts, pivot tables or maps +## Dashboard items with charts, pivot tables, maps and line lists -Dashboard items with charts, pivot table or maps may have an item menu button in the upper right corner of the item with additional viewing options, depending on the system settings that have been configured for the DHIS2 instance. If all the relevant system settings have been disabled by the DHIS2 instance, then there will not be an item menu button. Here are the possible item menu options: +Dashboard items with charts, pivot table, maps, line lists, event reports and event charts may have an item menu button in the upper right corner of the item with additional viewing options, depending on the system settings that have been configured for the DHIS2 instance. If all the relevant system settings have been disabled by the DHIS2 instance, then there will not be an item menu button. Here are the possible item menu options: ### Switching between visualizations -It is possible to toggle the visualization view for items containing charts, pivot tables and maps. Click on the item menu button and choose the desired view (e.g., **View as Table**, **View as Map**, **View as Chart**): +It is possible to toggle the visualization view of charts, pivot tables and maps, and between event charts and reports. Click on the item menu button and choose the desired view (e.g., **View as Table**, **View as Map**, **View as Chart**): ![](resources/images/dashboard-item-menu.png) ### View item in fullscreen -To view the chart, table or map in fullscreen, click on the **View fullscreen** option. To exit fullscreen, you can either press **esc** key or click the exit button in the upper right corner of the fullscreen view. +To view the chart, table, map or line list in fullscreen, click on the **View fullscreen** option. To exit fullscreen, you can either press **esc** key or click the exit button in the upper right corner of the fullscreen view. Note that you actually enter the slideshow, and can then use the navigation bar as described in the [Slideshow section](#dashboard-slideshow) to navigate to other dashboard items in fullscreen. ### Open in app @@ -264,23 +277,23 @@ To open the visualization in its corresponding app (e.g., Data Visualizer, Maps) ### Show interpretations and details -You can write interpretations for the chart, pivot table, map, event report, and event chart items by clicking on **Show interpretations and details**: +You can write interpretations for charts, pivot tables, maps, line lists, event reports, and event charts by clicking on **Show interpretations and details**. The item will be expanded vertically underneath to show the description, interpretations and replies: ![](resources/images/dashboard-item-menu-interpretations.png) -The item will be expanded vertically underneath to show the description, interpretations and replies. You can like an interpretation, reply to an interpretation, and add your own interpretation. You can edit, share or delete your own interpretations and replies, and if you have moderator access, you can delete others’ interpretations. +You can like an interpretation, reply to an interpretation, and add your own interpretations. You can edit, share or delete your own interpretations and replies, and if you have moderator access, you can delete others’ interpretations. -It is possible to format the description field, and interpretations with **bold**, _italic_ by using the Markdown style markers \* and \_ for **bold** and _italic_ respectively. The text field for writing new interpretations has a toolbar for adding rich text. Keyboard shortcuts are also available: Ctrl/Cmd + B and Ctrl/Cmd + I. A limited set of smilies is supported and can be used by typing one of the following character combinations: :) :-) :( :-( :+1 :-1. URLs are automatically detected and converted into a clickable link. +It is possible to format interpretation text with **bold**, _italic_ by using the Markdown style markers \* and \_ for **bold** and _italic_ respectively. The text field for writing new interpretations has a toolbar for adding rich text. Keyboard shortcuts are also available: Ctrl/Cmd + B and Ctrl/Cmd + I. A limited set of smilies is supported and can be used by typing one of the following character combinations: :) :-) :( :-( :+1 :-1. URLs are automatically detected and converted into a clickable link. Interpretations are sorted in descending order by date, with the most recent shown on top. Interpretation replies are sorted in ascending order by date, with the oldest shown on top. -![](resources/images/dashboard-interpretations.png) +Dashboard interpretations -## Sharing a dashboard { #dashboard_sharing } +## Share the dashboard { #dashboard_sharing } -In order to share a dashboard with users and user groups, click on the **Share** button to the right of the dashboard title to display the _Sharing and access_ dialog. +In order to share the dashboard with users and user groups, click on the **Share** button to the right of the dashboard title to display the _Sharing and access_ dialog. -![](resources/images/dashboard-sharing-dialog.png) +Dashboard sharing dialog There are three levels of sharing permissions available for a dashboard: @@ -302,7 +315,7 @@ All dashboards have the _All users_ group set to **No access** by default. The _ To share a dashboard with specific users and user groups, type the name in the input field, choose the desired access level and click on **Give access**. -![](resources/images/dashboard-sharing-add-user.png) +Dashboard sharing add user You can provide users with the url of the dashboard, allowing them to navigate directly to the dashboard. To get the dashboard url, just open the dashboard in view mode, and copy the browser url. For example, the url to the Antenatal Care dashboard in play.dhis2.org/dev is: @@ -312,4 +325,4 @@ https://play.dhis2.org/dev/dhis-web-dashboard/#/nghVC4wtyzi To ensure that all charts, maps and tables on the dashboard are shared with the chosen users and user groups, click on the **Apply sharing to dashboard items** button. -![](resources/images/dashboard-sharing-cascade-sharing.png) +Dashboard cascade sharing diff --git a/docs/resources/images/dashboard-clear-filters-to-sync.png b/docs/resources/images/dashboard-clear-filters-to-sync.png index 3583c53a6..41e7b536c 100644 Binary files a/docs/resources/images/dashboard-clear-filters-to-sync.png and b/docs/resources/images/dashboard-clear-filters-to-sync.png differ diff --git a/docs/resources/images/dashboard-confirm-delete.png b/docs/resources/images/dashboard-confirm-delete.png index b1b592957..4e02264f9 100644 Binary files a/docs/resources/images/dashboard-confirm-delete.png and b/docs/resources/images/dashboard-confirm-delete.png differ diff --git a/docs/resources/images/dashboard-create-mode.png b/docs/resources/images/dashboard-create-mode.png index c8ec30901..73e4b1c74 100644 Binary files a/docs/resources/images/dashboard-create-mode.png and b/docs/resources/images/dashboard-create-mode.png differ diff --git a/docs/resources/images/dashboard-edit-anc.png b/docs/resources/images/dashboard-edit-anc.png new file mode 100644 index 000000000..f49566b24 Binary files /dev/null and b/docs/resources/images/dashboard-edit-anc.png differ diff --git a/docs/resources/images/dashboard-edit-button.png b/docs/resources/images/dashboard-edit-button.png new file mode 100644 index 000000000..db22d1a47 Binary files /dev/null and b/docs/resources/images/dashboard-edit-button.png differ diff --git a/docs/resources/images/dashboard-edit-print-preview.png b/docs/resources/images/dashboard-edit-print-preview.png index bdd3bcda0..991a4bb12 100644 Binary files a/docs/resources/images/dashboard-edit-print-preview.png and b/docs/resources/images/dashboard-edit-print-preview.png differ diff --git a/docs/resources/images/dashboard-filter-badges.png b/docs/resources/images/dashboard-filter-badges.png index 5266adafd..e2dbe5757 100644 Binary files a/docs/resources/images/dashboard-filter-badges.png and b/docs/resources/images/dashboard-filter-badges.png differ diff --git a/docs/resources/images/dashboard-filter-settings.png b/docs/resources/images/dashboard-filter-settings.png index 8e81706e4..09da39edd 100644 Binary files a/docs/resources/images/dashboard-filter-settings.png and b/docs/resources/images/dashboard-filter-settings.png differ diff --git a/docs/resources/images/dashboard-filters.png b/docs/resources/images/dashboard-filters.png index 4681e4cc4..e9b640f9d 100644 Binary files a/docs/resources/images/dashboard-filters.png and b/docs/resources/images/dashboard-filters.png differ diff --git a/docs/resources/images/dashboard-generic.png b/docs/resources/images/dashboard-generic.png new file mode 100644 index 000000000..5c2b60d8b Binary files /dev/null and b/docs/resources/images/dashboard-generic.png differ diff --git a/docs/resources/images/dashboard-interpretations.png b/docs/resources/images/dashboard-interpretations.png new file mode 100644 index 000000000..570917c22 Binary files /dev/null and b/docs/resources/images/dashboard-interpretations.png differ diff --git a/docs/resources/images/dashboard-item-menu-interpretations.png b/docs/resources/images/dashboard-item-menu-interpretations.png index d50233b8d..d9edcde1c 100644 Binary files a/docs/resources/images/dashboard-item-menu-interpretations.png and b/docs/resources/images/dashboard-item-menu-interpretations.png differ diff --git a/docs/resources/images/dashboard-item-menu.png b/docs/resources/images/dashboard-item-menu.png index 986687f37..0d117c02d 100644 Binary files a/docs/resources/images/dashboard-item-menu.png and b/docs/resources/images/dashboard-item-menu.png differ diff --git a/docs/resources/images/dashboard-item-selector.png b/docs/resources/images/dashboard-item-selector.png index a8c05edb5..f7469b0e5 100644 Binary files a/docs/resources/images/dashboard-item-selector.png and b/docs/resources/images/dashboard-item-selector.png differ diff --git a/docs/resources/images/dashboard-layout-modal.png b/docs/resources/images/dashboard-layout-modal.png index 594f5769e..27747bfd4 100644 Binary files a/docs/resources/images/dashboard-layout-modal.png and b/docs/resources/images/dashboard-layout-modal.png differ diff --git a/docs/resources/images/dashboard-list-and-filter.png b/docs/resources/images/dashboard-list-and-filter.png new file mode 100644 index 000000000..f4c0bea00 Binary files /dev/null and b/docs/resources/images/dashboard-list-and-filter.png differ diff --git a/docs/resources/images/dashboard-more-menu.png b/docs/resources/images/dashboard-more-menu.png index 7f9150f18..b194581d3 100644 Binary files a/docs/resources/images/dashboard-more-menu.png and b/docs/resources/images/dashboard-more-menu.png differ diff --git a/docs/resources/images/dashboard-new-button.png b/docs/resources/images/dashboard-new-button.png index 25781b134..803ced2b2 100644 Binary files a/docs/resources/images/dashboard-new-button.png and b/docs/resources/images/dashboard-new-button.png differ diff --git a/docs/resources/images/dashboard-offline-dashboard.png b/docs/resources/images/dashboard-offline-dashboard.png index ec2efc734..283b03606 100644 Binary files a/docs/resources/images/dashboard-offline-dashboard.png and b/docs/resources/images/dashboard-offline-dashboard.png differ diff --git a/docs/resources/images/dashboard-orgunit-filter-dialog.png b/docs/resources/images/dashboard-orgunit-filter-dialog.png deleted file mode 100644 index e158a36dd..000000000 Binary files a/docs/resources/images/dashboard-orgunit-filter-dialog.png and /dev/null differ diff --git a/docs/resources/images/dashboard-period-filter-dialog.png b/docs/resources/images/dashboard-period-filter-dialog.png new file mode 100644 index 000000000..7b8e20d3e Binary files /dev/null and b/docs/resources/images/dashboard-period-filter-dialog.png differ diff --git a/docs/resources/images/dashboard-place-items.png b/docs/resources/images/dashboard-place-items.png new file mode 100644 index 000000000..73ef06aeb Binary files /dev/null and b/docs/resources/images/dashboard-place-items.png differ diff --git a/docs/resources/images/dashboard-print-menu.png b/docs/resources/images/dashboard-print-menu.png index 53473be44..73b274344 100644 Binary files a/docs/resources/images/dashboard-print-menu.png and b/docs/resources/images/dashboard-print-menu.png differ diff --git a/docs/resources/images/dashboard-search-for-dashboard.png b/docs/resources/images/dashboard-search-for-dashboard.png deleted file mode 100644 index 63ac3cef3..000000000 Binary files a/docs/resources/images/dashboard-search-for-dashboard.png and /dev/null differ diff --git a/docs/resources/images/dashboard-sharing-add-user.png b/docs/resources/images/dashboard-sharing-add-user.png index b78187413..9eadd989c 100644 Binary files a/docs/resources/images/dashboard-sharing-add-user.png and b/docs/resources/images/dashboard-sharing-add-user.png differ diff --git a/docs/resources/images/dashboard-sharing-cascade-sharing.png b/docs/resources/images/dashboard-sharing-cascade-sharing.png index 46af33666..52b59e94b 100644 Binary files a/docs/resources/images/dashboard-sharing-cascade-sharing.png and b/docs/resources/images/dashboard-sharing-cascade-sharing.png differ diff --git a/docs/resources/images/dashboard-sharing-dialog.png b/docs/resources/images/dashboard-sharing-dialog.png index 6853cff2a..1e641f116 100644 Binary files a/docs/resources/images/dashboard-sharing-dialog.png and b/docs/resources/images/dashboard-sharing-dialog.png differ diff --git a/docs/resources/images/dashboard-slideshow-button.png b/docs/resources/images/dashboard-slideshow-button.png new file mode 100644 index 000000000..81609dced Binary files /dev/null and b/docs/resources/images/dashboard-slideshow-button.png differ diff --git a/docs/resources/images/dashboard-slideshow-navbar.png b/docs/resources/images/dashboard-slideshow-navbar.png new file mode 100644 index 000000000..57c455798 Binary files /dev/null and b/docs/resources/images/dashboard-slideshow-navbar.png differ diff --git a/docs/resources/images/dashboard-small-screen.png b/docs/resources/images/dashboard-small-screen.png index 8099aa5b8..b26e6d0f6 100644 Binary files a/docs/resources/images/dashboard-small-screen.png and b/docs/resources/images/dashboard-small-screen.png differ diff --git a/docs/resources/images/dashboard-spacer-edit-mode.png b/docs/resources/images/dashboard-spacer-edit-mode.png index c03588b0d..285376f64 100644 Binary files a/docs/resources/images/dashboard-spacer-edit-mode.png and b/docs/resources/images/dashboard-spacer-edit-mode.png differ diff --git a/docs/resources/images/dashboard-spacer-view-mode.png b/docs/resources/images/dashboard-spacer-view-mode.png index 22a3a99d9..2ad12d8f9 100644 Binary files a/docs/resources/images/dashboard-spacer-view-mode.png and b/docs/resources/images/dashboard-spacer-view-mode.png differ diff --git a/docs/resources/images/dashboard-sync-remove-offline.png b/docs/resources/images/dashboard-sync-remove-offline.png new file mode 100644 index 000000000..466810721 Binary files /dev/null and b/docs/resources/images/dashboard-sync-remove-offline.png differ diff --git a/docs/resources/images/dashboard-translation-dialog.png b/docs/resources/images/dashboard-translation-dialog.png index 118375078..9321fce9f 100644 Binary files a/docs/resources/images/dashboard-translation-dialog.png and b/docs/resources/images/dashboard-translation-dialog.png differ diff --git a/docs/resources/images/dashboard-view-mode.png b/docs/resources/images/dashboard-view-mode.png index 6572b9297..ef998ad96 100644 Binary files a/docs/resources/images/dashboard-view-mode.png and b/docs/resources/images/dashboard-view-mode.png differ diff --git a/i18n/en.pot b/i18n/en.pot index 76eae083b..fa8cab2e1 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,26 +5,123 @@ 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: 2024-08-27T07:26:05.058Z\n" -"PO-Revision-Date: 2024-08-27T07:26:05.060Z\n" +"POT-Creation-Date: 2024-12-19T11:30:27.893Z\n" +"PO-Revision-Date: 2024-12-19T11:30:27.893Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" -msgid "Cannot create a dashboard while offline" -msgstr "Cannot create a dashboard while offline" +msgid "Dashboards" +msgstr "Dashboards" -msgid "Create new dashboard" -msgstr "Create new dashboard" +msgid "The dashboard couldn't be made available offline. Try again." +msgstr "The dashboard couldn't be made available offline. Try again." + +msgid "Remove from offline storage" +msgstr "Remove from offline storage" + +msgid "Make available offline" +msgstr "Make available offline" + +msgid "Sync offline data now" +msgstr "Sync offline data now" + +msgid "Unstar dashboard" +msgstr "Unstar dashboard" + +msgid "Star dashboard" +msgstr "Star dashboard" + +msgid "Hide description" +msgstr "Hide description" + +msgid "Show description" +msgstr "Show description" + +msgid "Print" +msgstr "Print" + +msgid "Dashboard layout" +msgstr "Dashboard layout" + +msgid "One item per page" +msgstr "One item per page" + +msgid "Close dashboard" +msgstr "Close dashboard" + +msgid "No dashboard items to show in slideshow" +msgstr "No dashboard items to show in slideshow" + +msgid "Not available offline" +msgstr "Not available offline" + +msgid "Edit" +msgstr "Edit" + +msgid "Share" +msgstr "Share" + +msgid "Slideshow" +msgstr "Slideshow" + +msgid "Clear dashboard filters?" +msgstr "Clear dashboard filters?" + +msgid "" +"A dashboard's filters can’t be saved offline. Do you want to remove the " +"filters and make this dashboard available offline?" +msgstr "" +"A dashboard's filters can’t be saved offline. Do you want to remove the " +"filters and make this dashboard available offline?" + +msgid "No, cancel" +msgstr "No, cancel" + +msgid "Yes, clear filters and sync" +msgstr "Yes, clear filters and sync" + +msgid "Cancel" +msgstr "Cancel" + +msgid "Confirm" +msgstr "Confirm" + +msgid "Filter" +msgstr "Filter" + +msgid "Failed to unstar the dashboard" +msgstr "Failed to unstar the dashboard" + +msgid "Failed to star the dashboard" +msgstr "Failed to star the dashboard" + +msgid "Offline data last updated {{timeAgo}}" +msgstr "Offline data last updated {{timeAgo}}" + +msgid "Synced {{timeAgo}}" +msgstr "Synced {{timeAgo}}" + +msgid "Cannot unstar this dashboard while offline" +msgstr "Cannot unstar this dashboard while offline" + +msgid "Cannot star this dashboard while offline" +msgstr "Cannot star this dashboard while offline" + +msgid "No dashboards available." +msgstr "No dashboards available." + +msgid "Create a new dashboard using the + button." +msgstr "Create a new dashboard using the + button." msgid "Search for a dashboard" msgstr "Search for a dashboard" -msgid "Show fewer dashboards" -msgstr "Show fewer dashboards" +msgid "No dashboards found" +msgstr "No dashboards found" -msgid "Show more dashboards" -msgstr "Show more dashboards" +msgid "{{appKey}} app not found" +msgstr "{{appKey}} app not found" msgid "Remove this item" msgstr "Remove this item" @@ -86,6 +183,9 @@ msgstr "Hide details and interpretations" msgid "Show details and interpretations" msgstr "Show details and interpretations" +msgid "Open menu" +msgstr "Open menu" + msgid "Open in {{appName}} app" msgstr "Open in {{appName}} app" @@ -149,8 +249,11 @@ msgstr "There was an error loading data for this item" msgid "Open this item in {{appName}}" msgstr "Open this item in {{appName}}" -msgid "Not available offline" -msgstr "Not available offline" +msgid "Resources" +msgstr "Resources" + +msgid "Reports" +msgstr "Reports" msgid "Visualizations" msgstr "Visualizations" @@ -176,12 +279,6 @@ msgstr "Line lists" msgid "Apps" msgstr "Apps" -msgid "Reports" -msgstr "Reports" - -msgid "Resources" -msgstr "Resources" - msgid "Users" msgstr "Users" @@ -248,9 +345,6 @@ msgstr "" "This action cannot be undone. Are you sure you want to permanently delete " "this dashboard?" -msgid "Cancel" -msgstr "Cancel" - msgid "Discard changes" msgstr "Discard changes" @@ -310,9 +404,6 @@ msgstr "Available Filters" msgid "Selected Filters" msgstr "Selected Filters" -msgid "Confirm" -msgstr "Confirm" - msgid "There are no items on this dashboard" msgstr "There are no items on this dashboard" @@ -340,9 +431,6 @@ msgstr "Cannot search for dashboard items while offline" msgid "Additional items" msgstr "Additional items" -msgid "Dashboard layout" -msgstr "Dashboard layout" - msgid "Freeflow" msgstr "Freeflow" @@ -410,9 +498,6 @@ msgstr "End of dashboard" msgid "Start of dashboard" msgstr "Start of dashboard" -msgid "Print" -msgstr "Print" - msgid "dashboard layout" msgstr "dashboard layout" @@ -463,13 +548,19 @@ msgstr "No dashboards found. Use the + button to create a new dashboard." msgid "Requested dashboard not found" msgstr "Requested dashboard not found" +msgid "No description" +msgstr "No description" + msgid "{{count}} selected" msgid_plural "{{count}} selected" msgstr[0] "{{count}} selected" msgstr[1] "{{count}} selected" -msgid "Cannot remove filters while offline" -msgstr "Cannot remove filters while offline" +msgid "Cannot edit filters while offline" +msgstr "Cannot edit filters while offline" + +msgid "Cannot edit filters on a small screen" +msgstr "Cannot edit filters on a small screen" msgid "Removing filters while offline" msgstr "Removing filters while offline" @@ -481,84 +572,25 @@ msgstr "" "Removing this filter while offline will remove all other filters. Do you " "want to remove all filters on this dashboard?" -msgid "No, cancel" -msgstr "No, cancel" - msgid "Yes, remove filters" msgstr "Yes, remove filters" -msgid "The dashboard couldn't be made available offline. Try again." -msgstr "The dashboard couldn't be made available offline. Try again." - -msgid "Failed to unstar the dashboard" -msgstr "Failed to unstar the dashboard" - -msgid "Failed to star the dashboard" -msgstr "Failed to star the dashboard" - -msgid "Remove from offline storage" -msgstr "Remove from offline storage" - -msgid "Make available offline" -msgstr "Make available offline" - -msgid "Sync offline data now" -msgstr "Sync offline data now" - -msgid "Unstar dashboard" -msgstr "Unstar dashboard" - -msgid "Star dashboard" -msgstr "Star dashboard" - -msgid "Hide description" -msgstr "Hide description" - -msgid "Show description" -msgstr "Show description" - -msgid "One item per page" -msgstr "One item per page" - -msgid "Close dashboard" -msgstr "Close dashboard" - -msgid "More" -msgstr "More" - -msgid "Edit" -msgstr "Edit" - -msgid "Share" -msgstr "Share" - -msgid "Clear dashboard filters?" -msgstr "Clear dashboard filters?" - -msgid "" -"A dashboard's filters can’t be saved offline. Do you want to remove the " -"filters and make this dashboard available offline?" -msgstr "" -"A dashboard's filters can’t be saved offline. Do you want to remove the " -"filters and make this dashboard available offline?" - -msgid "Yes, clear filters and sync" -msgstr "Yes, clear filters and sync" - -msgid "No description" -msgstr "No description" +msgid "Exit slideshow" +msgstr "Exit slideshow" -msgid "Add filter" -msgstr "Add filter" +msgid "Previous item" +msgstr "Previous item" -msgid "Offline data last updated {{time}}" -msgstr "Offline data last updated {{time}}" +msgid "Next item" +msgstr "Next item" -msgid "Cannot unstar this dashboard while offline" -msgstr "Cannot unstar this dashboard while offline" +msgid "{{name}}: {{filter}}" +msgstr "{{name}}: {{filter}}" -msgid "Cannot star this dashboard while offline" -msgstr "Cannot star this dashboard while offline" +msgid "{{count}} filters active" +msgid_plural "{{count}} filters active" +msgstr[0] "{{count}} filter active" +msgstr[1] "{{count}} filters active" msgid "Loading dashboard – {{name}}" msgstr "Loading dashboard – {{name}}" @@ -566,6 +598,12 @@ msgstr "Loading dashboard – {{name}}" msgid "Loading dashboard" msgstr "Loading dashboard" +msgid "Load dashboard failed" +msgstr "Load dashboard failed" + +msgid "This dashboard could not be loaded. Please try again later." +msgstr "This dashboard could not be loaded. Please try again later." + msgid "Offline" msgstr "Offline" @@ -574,9 +612,3 @@ msgstr "This dashboard cannot be loaded while offline." msgid "Go to start page" msgstr "Go to start page" - -msgid "Load dashboard failed" -msgstr "Load dashboard failed" - -msgid "This dashboard could not be loaded. Please try again later." -msgstr "This dashboard could not be loaded. Please try again later." diff --git a/package.json b/package.json index 131892cf7..b62d147e0 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "private": true, "license": "BSD-3-Clause", "dependencies": { - "@dhis2/analytics": "^26.8.2", + "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#e398d08e696356908725c8f51f32c30e7cb002ec", "@dhis2/app-runtime": "^3.10.6", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/d2-i18n": "^1.1.3", - "@dhis2/ui": "^9.11.3", + "@dhis2/ui": "^10.1.4", "@krakenjs/post-robot": "^11.0.0", "classnames": "^2.3.2", "d2": "^31.10.0", @@ -51,9 +51,10 @@ "@semantic-release/changelog": "^6", "@semantic-release/exec": "^6", "@semantic-release/git": "^10", - "@testing-library/jest-dom": "^6.1.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^12", "cypress": "^13.13.1", + "cypress-real-events": "^1.13.0", "d2-manifest": "^1.0.0", "eslint-plugin-cypress": "^3.3.0", "immutability-helper": "^3.1.1", @@ -68,6 +69,12 @@ "jest": { "moduleNameMapper": { "^.+\\.(css|sass|scss)$": "identity-obj-proxy" - } + }, + "setupFilesAfterEnv": [ + "/config/testSetup.js" + ] + }, + "resolutions": { + "@dhis2/ui": "^10.1.4" } } diff --git a/src/actions/controlBar.js b/src/actions/controlBar.js deleted file mode 100644 index e7f6eae90..000000000 --- a/src/actions/controlBar.js +++ /dev/null @@ -1,29 +0,0 @@ -import { apiGetControlBarRows } from '../api/controlBar.js' -import { SET_CONTROLBAR_USER_ROWS } from '../reducers/controlBar.js' - -// actions - -export const acSetControlBarUserRows = (rows) => ({ - type: SET_CONTROLBAR_USER_ROWS, - value: rows, -}) - -// thunks - -export const tSetControlBarRows = () => async (dispatch) => { - const onSuccess = (rows) => { - dispatch(acSetControlBarUserRows(rows)) - } - - const onError = (error) => { - console.log('Error (apiGetControlBarRows): ', error) - return error - } - - try { - const controlBarRows = await apiGetControlBarRows() - return onSuccess(controlBarRows) - } catch (err) { - return onError(err) - } -} diff --git a/src/actions/slideshow.js b/src/actions/slideshow.js new file mode 100644 index 000000000..38a841ff6 --- /dev/null +++ b/src/actions/slideshow.js @@ -0,0 +1,6 @@ +import { SET_SLIDESHOW } from '../reducers/slideshow.js' + +export const acSetSlideshow = (isSlideshow) => ({ + type: SET_SLIDESHOW, + value: isSlideshow, +}) diff --git a/src/api/controlBar.js b/src/api/controlBar.js deleted file mode 100644 index ce3627c5b..000000000 --- a/src/api/controlBar.js +++ /dev/null @@ -1,16 +0,0 @@ -import { DEFAULT_STATE_CONTROLBAR_ROWS } from '../reducers/controlBar.js' -import { - apiGetUserDataStoreValue, - apiPostUserDataStoreValue, -} from './userDataStore.js' - -const KEY_CONTROLBAR_ROWS = 'controlBarRows' - -export const apiGetControlBarRows = async () => - await apiGetUserDataStoreValue( - KEY_CONTROLBAR_ROWS, - DEFAULT_STATE_CONTROLBAR_ROWS - ) - -export const apiPostControlBarRows = async (value) => - await apiPostUserDataStoreValue(KEY_CONTROLBAR_ROWS, value) diff --git a/src/components/App.js b/src/components/App.js index 28820c851..66e12a726 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -5,7 +5,6 @@ import React, { useEffect } from 'react' import { connect } from 'react-redux' import { Redirect, HashRouter as Router, Route, Switch } from 'react-router-dom' import { acClearActiveModalDimension } from '../actions/activeModalDimension.js' -import { tSetControlBarRows } from '../actions/controlBar.js' import { tFetchDashboards } from '../actions/dashboards.js' import { acClearDashboardsFilter } from '../actions/dashboardsFilter.js' import { acClearEditDashboard } from '../actions/editDashboard.js' @@ -31,7 +30,6 @@ const App = (props) => { useEffect(() => { props.fetchDashboards() - props.setControlBarRows() props.setShowDescription() // store the headerbar height for controlbar height calculations @@ -48,7 +46,7 @@ const App = (props) => { return ( systemSettings && ( <> - + { App.propTypes = { fetchDashboards: PropTypes.func, resetState: PropTypes.func, - setControlBarRows: PropTypes.func, setShowDescription: PropTypes.func, } const mapDispatchToProps = { fetchDashboards: tFetchDashboards, - setControlBarRows: tSetControlBarRows, setShowDescription: tSetShowDescription, resetState: () => (dispatch) => { dispatch(acSetSelected({})) diff --git a/src/components/DashboardsBar/Chip.js b/src/components/DashboardsBar/Chip.js deleted file mode 100644 index 0e9cbd010..000000000 --- a/src/components/DashboardsBar/Chip.js +++ /dev/null @@ -1,74 +0,0 @@ -import { useDhis2ConnectionStatus, useDataEngine } from '@dhis2/app-runtime' -import { Chip as UiChip, colors, IconStarFilled24 } from '@dhis2/ui' -import cx from 'classnames' -import debounce from 'lodash/debounce.js' -import PropTypes from 'prop-types' -import React from 'react' -import { Link } from 'react-router-dom' -import { apiPostDataStatistics } from '../../api/dataStatistics.js' -import { useCacheableSection } from '../../modules/useCacheableSection.js' -import { OfflineSaved } from './assets/icons.js' -import classes from './styles/Chip.module.css' - -const Chip = ({ starred, selected, label, dashboardId, onClick }) => { - const { lastUpdated } = useCacheableSection(dashboardId) - const { isConnected: online } = useDhis2ConnectionStatus() - const engine = useDataEngine() - const chipProps = { - selected, - } - - if (starred) { - chipProps.icon = ( - - ) - } - const debouncedPostStatistics = debounce( - () => apiPostDataStatistics('DASHBOARD_VIEW', dashboardId, engine), - 500 - ) - - const handleClick = () => { - online && debouncedPostStatistics() - onClick() - } - - return ( - - - - {label} - - {lastUpdated && ( - - )} - - - ) -} - -Chip.propTypes = { - dashboardId: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - selected: PropTypes.bool.isRequired, - starred: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -} - -export default Chip diff --git a/src/components/DashboardsBar/ClearButton.js b/src/components/DashboardsBar/ClearButton.js deleted file mode 100644 index 05214033e..000000000 --- a/src/components/DashboardsBar/ClearButton.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import ClearIcon from './assets/Clear.js' -import classes from './styles/ClearButton.module.css' - -const ClearButton = ({ onClear }) => ( - -) - -ClearButton.propTypes = { - onClear: PropTypes.func.isRequired, -} - -export default ClearButton diff --git a/src/components/DashboardsBar/Content.js b/src/components/DashboardsBar/Content.js deleted file mode 100644 index e95f1a44a..000000000 --- a/src/components/DashboardsBar/Content.js +++ /dev/null @@ -1,135 +0,0 @@ -import { useDhis2ConnectionStatus } from '@dhis2/app-runtime' -import i18n from '@dhis2/d2-i18n' -import { Button, ComponentCover, Tooltip, IconAdd24 } from '@dhis2/ui' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { useState } from 'react' -import { connect } from 'react-redux' -import { Redirect, withRouter } from 'react-router-dom' -import { sGetAllDashboards } from '../../reducers/dashboards.js' -import { sGetDashboardsFilter } from '../../reducers/dashboardsFilter.js' -import { sGetSelectedId } from '../../reducers/selected.js' -import Chip from './Chip.js' -import Filter from './Filter.js' -import { getFilteredDashboards } from './getFilteredDashboards.js' -import classes from './styles/Content.module.css' - -const Content = ({ - dashboards, - expanded, - filterText, - history, - selectedId, - onChipClicked, - onSearchClicked, -}) => { - const [redirectUrl, setRedirectUrl] = useState(null) - const { isDisconnected: offline } = useDhis2ConnectionStatus() - - const onSelectDashboard = () => { - const id = getFilteredDashboards(dashboards, filterText)[0]?.id - if (id) { - history.push(id) - } - } - - const enterNewMode = () => { - if (!offline) { - setRedirectUrl('/new') - } - } - - const getChips = () => - getFilteredDashboards(dashboards, filterText).map((dashboard) => ( - - )) - - const getControlsSmall = () => ( -
- -
- ) - - const getControlsLarge = () => ( -
-
-
- -
-
- -
- ) - - if (redirectUrl) { - return - } - - 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/DashboardsBar/DashboardsBar.js b/src/components/DashboardsBar/DashboardsBar.js index e42cbc753..81abe8bb4 100644 --- a/src/components/DashboardsBar/DashboardsBar.js +++ b/src/components/DashboardsBar/DashboardsBar.js @@ -1,147 +1,45 @@ -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { - useState, - useRef, - useEffect, - useCallback, - createRef, -} from 'react' -import { connect } from 'react-redux' -import { acSetControlBarUserRows } from '../../actions/controlBar.js' -import { apiPostControlBarRows } from '../../api/controlBar.js' -import { useWindowDimensions } from '../../components/WindowDimensionsProvider.js' -import { sGetControlBarUserRows } from '../../reducers/controlBar.js' -import Content from './Content.js' -import DragHandle from './DragHandle.js' -import { getRowsFromHeight } from './getRowsFromHeight.js' -import ShowMoreButton from './ShowMoreButton.js' -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 [mouseYPos, setMouseYPos] = useState(0) - const userRowsChanged = useRef(false) - const ref = createRef() - const { height } = useWindowDimensions() - - const rootElement = document.documentElement - - useEffect(() => { - if (mouseYPos === 0) { - return - } - - const newRows = Math.max( - MIN_ROW_COUNT, - getRowsFromHeight(mouseYPos - 52) // don't rush the transition to a bigger row count - ) - - if (newRows < MAX_ROW_COUNT) { - onExpandedChanged(false) - } - - if (newRows !== userRows) { - updateUserRows(Math.min(newRows, MAX_ROW_COUNT)) - userRowsChanged.current = true - } - }, [mouseYPos]) - - useEffect(() => { - rootElement.style.setProperty('--user-rows-count', userRows) - }, [userRows]) - - useEffect(() => { - const vh = height * 0.01 - rootElement.style.setProperty('--vh', `${vh}px`) - }, [height]) - - useEffect(() => { - if (!dragging && userRowsChanged.current) { - apiPostControlBarRows(userRows) - userRowsChanged.current = false - } - }, [dragging, userRowsChanged.current]) - - const scrollToTop = () => { - if (expanded) { - ref.current.scroll(0, 0) - } - } - - const memoizedToggleExpanded = useCallback(() => { - if (expanded) { - memoizedCancelExpanded() - } else { - scrollToTop() - onExpandedChanged(!expanded) - } - }, [expanded]) - - const memoizedCancelExpanded = useCallback(() => { - scrollToTop() - onExpandedChanged(false) - }, []) +import i18n from '@dhis2/d2-i18n' +import { Button, IconAdd16, DropdownButton } from '@dhis2/ui' +import React, { useState } from 'react' +import { useHistory } from 'react-router-dom' +import InformationBlock from './InformationBlock/InformationBlock.js' +import { IconNavigation, NavigationMenu } from './NavigationMenu/index.js' +import styles from './styles/DashboardsBar.module.css' + +export const DashboardsBar = () => { + const history = useHistory() + const [navigationMenuOpen, setNavigationMenuOpen] = useState(false) return ( -
-
-
- -
- - +
+
-
+
) } - -DashboardsBar.propTypes = { - expanded: PropTypes.bool, - updateUserRows: PropTypes.func, - userRows: PropTypes.number, - onExpandedChanged: PropTypes.func, -} - -DashboardsBar.defaultProps = { - onExpandedChanged: Function.prototype, -} - -const mapStateToProps = (state) => ({ - userRows: sGetControlBarUserRows(state), -}) - -const mapDispatchToProps = { - updateUserRows: acSetControlBarUserRows, -} - -export default connect(mapStateToProps, mapDispatchToProps)(DashboardsBar) diff --git a/src/components/DashboardsBar/DragHandle.js b/src/components/DashboardsBar/DragHandle.js deleted file mode 100644 index f4543525e..000000000 --- a/src/components/DashboardsBar/DragHandle.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useState } from 'react' -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 React.memo(DragHandle, () => true) diff --git a/src/components/DashboardsBar/Filter.js b/src/components/DashboardsBar/Filter.js deleted file mode 100644 index 0cf2ce7c7..000000000 --- a/src/components/DashboardsBar/Filter.js +++ /dev/null @@ -1,131 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { colors, IconSearch16, IconSearch24 } from '@dhis2/ui' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { useState } from 'react' -import { connect } from 'react-redux' -import { - acSetDashboardsFilter, - acClearDashboardsFilter, -} from '../../actions/dashboardsFilter.js' -import { isSmallScreen } from '../../modules/smallScreen.js' -import { sGetDashboardsFilter } from '../../reducers/dashboardsFilter.js' -import { useWindowDimensions } from '../WindowDimensionsProvider.js' -import ClearButton from './ClearButton.js' -import classes from './styles/Filter.module.css' - -export const KEYCODE_ENTER = 13 -export const KEYCODE_ESCAPE = 27 - -const Filter = ({ - clearDashboardsFilter, - expanded, - filterText, - setDashboardsFilter, - onKeypressEnter, - onSearchClicked, -}) => { - const [focusedClassName, setFocusedClassName] = useState('') - const [inputFocused, setInputFocus] = useState(false) - const { width } = useWindowDimensions() - - const setFilterValue = (event) => { - event.preventDefault() - setDashboardsFilter(event.target.value) - } - - const onKeyUp = (event) => { - switch (event.keyCode) { - case KEYCODE_ENTER: - onKeypressEnter() - clearDashboardsFilter() - break - case KEYCODE_ESCAPE: - clearDashboardsFilter() - break - default: - break - } - } - - const onFocus = (event) => { - event.preventDefault() - setFocusedClassName(classes.focused) - } - - const onBlur = (event) => { - event.preventDefault() - setFocusedClassName('') - } - - const onFocusInput = (input) => { - if (input && inputFocused && isSmallScreen(width)) { - return input.focus() - } - } - - const activateSearchInput = () => { - onSearchClicked() - setInputFocus(true) - } - - return ( - - -
-
- -
- - {filterText && ( -
- -
- )} -
-
- ) -} - -Filter.propTypes = { - clearDashboardsFilter: PropTypes.func, - expanded: PropTypes.bool, - filterText: PropTypes.string, - setDashboardsFilter: PropTypes.func, - onKeypressEnter: PropTypes.func, - onSearchClicked: PropTypes.func, -} - -const mapStateToProps = (state) => ({ - filterText: sGetDashboardsFilter(state), -}) - -const mapDispatchToProps = { - setDashboardsFilter: acSetDashboardsFilter, - clearDashboardsFilter: acClearDashboardsFilter, -} - -export default connect(mapStateToProps, mapDispatchToProps)(Filter) diff --git a/src/pages/view/TitleBar/ActionsBar.js b/src/components/DashboardsBar/InformationBlock/ActionsBar.js similarity index 67% rename from src/pages/view/TitleBar/ActionsBar.js rename to src/components/DashboardsBar/InformationBlock/ActionsBar.js index 24da7ec8b..8add1296b 100644 --- a/src/pages/view/TitleBar/ActionsBar.js +++ b/src/components/DashboardsBar/InformationBlock/ActionsBar.js @@ -1,138 +1,109 @@ import { OfflineTooltip } from '@dhis2/analytics' -import { - useDataEngine, - useAlert, - useDhis2ConnectionStatus, -} from '@dhis2/app-runtime' +import { useDhis2ConnectionStatus } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import { Button, FlyoutMenu, colors, - IconMore24, + IconMore16, SharingDialog, } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useCallback, useState } from 'react' import { connect } from 'react-redux' -import { Link, Redirect } from 'react-router-dom' -import { acSetDashboardStarred } from '../../../actions/dashboards.js' +import { useHistory, Redirect } from 'react-router-dom' import { acClearItemFilters } from '../../../actions/itemFilters.js' import { acSetShowDescription } from '../../../actions/showDescription.js' +import { acSetSlideshow } from '../../../actions/slideshow.js' import { apiPostShowDescription } from '../../../api/description.js' import ConfirmActionDialog from '../../../components/ConfirmActionDialog.js' import DropdownButton from '../../../components/DropdownButton/DropdownButton.js' import MenuItem from '../../../components/MenuItemWithTooltip.js' +import { useSystemSettings } from '../../../components/SystemSettingsProvider.js' +import { itemTypeSupportsFullscreen } from '../../../modules/itemTypes.js' import { useCacheableSection } from '../../../modules/useCacheableSection.js' import { orObject } from '../../../modules/util.js' -import { sGetDashboardStarred } from '../../../reducers/dashboards.js' +import { ROUTE_START_PATH } from '../../../pages/start/index.js' import { sGetNamedItemFilters } from '../../../reducers/itemFilters.js' import { sGetSelected } from '../../../reducers/selected.js' import { sGetShowDescription } from '../../../reducers/showDescription.js' -import { ROUTE_START_PATH } from '../../start/index.js' -import { apiStarDashboard } from './apiStarDashboard.js' import FilterSelector from './FilterSelector.js' -import StarDashboardButton from './StarDashboardButton.js' import classes from './styles/ActionsBar.module.css' -const ViewActions = ({ +const ActionsBar = ({ id, access, showDescription, starred, - setDashboardStarred, + setSlideshow, + toggleDashboardStarred, + showAlert, updateShowDescription, removeAllFilters, restrictFilters, allowedFilters, filtersLength, + dashboardItems, }) => { - const [moreOptionsSmallIsOpen, setMoreOptionsSmallIsOpen] = useState(false) + const history = useHistory() const [moreOptionsIsOpen, setMoreOptionsIsOpen] = useState(false) const [sharingDialogIsOpen, setSharingDialogIsOpen] = useState(false) const [confirmCacheDialogIsOpen, setConfirmCacheDialogIsOpen] = useState(false) const [redirectUrl, setRedirectUrl] = useState(null) - const dataEngine = useDataEngine() const { isDisconnected: offline } = useDhis2ConnectionStatus() const { lastUpdated, isCached, startRecording, remove } = useCacheableSection(id) + const { allowVisFullscreen } = useSystemSettings().systemSettings - const { show } = useAlert( - ({ msg }) => msg, - ({ isCritical }) => - isCritical ? { critical: true } : { warning: true } - ) - - const toggleMoreOptions = (small) => - small - ? setMoreOptionsSmallIsOpen(!moreOptionsSmallIsOpen) - : setMoreOptionsIsOpen(!moreOptionsIsOpen) - - const closeMoreOptions = () => { - setMoreOptionsSmallIsOpen(false) - setMoreOptionsIsOpen(false) - } - - if (redirectUrl) { - return - } - - const onRecordError = () => { - show({ + const onRecordError = useCallback(() => { + showAlert({ msg: i18n.t( "The dashboard couldn't be made available offline. Try again." ), isCritical: true, }) - } + }, [showAlert]) - const onCacheDashboardConfirmed = () => { + const onCacheDashboardConfirmed = useCallback(() => { setConfirmCacheDialogIsOpen(false) removeAllFilters() startRecording({ onError: onRecordError, }) - } + }, [onRecordError, removeAllFilters, startRecording]) - const onRemoveFromOffline = () => { - closeMoreOptions() + const onRemoveFromOffline = useCallback(() => { + setMoreOptionsIsOpen(false) lastUpdated && remove() - } + }, [lastUpdated, remove]) - const onAddToOffline = () => { - closeMoreOptions() + const onAddToOffline = useCallback(() => { + setMoreOptionsIsOpen(false) return filtersLength ? setConfirmCacheDialogIsOpen(true) : startRecording({ onError: onRecordError, }) - } + }, [filtersLength, onRecordError, startRecording]) - const onToggleShowDescription = () => { + const onToggleShowDescription = useCallback(() => { updateShowDescription(!showDescription) - closeMoreOptions() + setMoreOptionsIsOpen(false) !offline && apiPostShowDescription(!showDescription) - } - - const onToggleStarredDashboard = () => - apiStarDashboard(dataEngine, id, !starred) - .then(() => { - setDashboardStarred(id, !starred) - closeMoreOptions() - }) - .catch(() => { - const msg = starred - ? i18n.t('Failed to unstar the dashboard') - : i18n.t('Failed to star the dashboard') - show({ msg, isCritical: false }) - }) + }, [offline, showDescription, updateShowDescription]) - const onToggleSharingDialog = () => - setSharingDialogIsOpen(!sharingDialogIsOpen) + const onToggleSharingDialog = useCallback( + () => setSharingDialogIsOpen(!sharingDialogIsOpen), + [sharingDialogIsOpen] + ) const userAccess = orObject(access) + const hasSlideshowItems = dashboardItems?.some( + (i) => itemTypeSupportsFullscreen(i.type) || false + ) + const getMoreMenu = () => ( {lastUpdated ? ( @@ -166,7 +137,7 @@ const ViewActions = ({ ? i18n.t('Unstar dashboard') : i18n.t('Star dashboard') } - onClick={onToggleStarredDashboard} + onClick={toggleDashboardStarred} /> - - - + history.push(ROUTE_START_PATH)} + /> ) - const getMoreButton = (className, useSmall) => ( - toggleMoreOptions(useSmall)} - icon={} - component={getMoreMenu()} - > - {i18n.t('More')} - - ) + if (redirectUrl) { + return + } + + const slideshowTooltipContent = !hasSlideshowItems + ? i18n.t('No dashboard items to show in slideshow') + : offline && !isCached + ? i18n.t('Not available offline') + : null return ( <>
- -
+
{userAccess.update ? ( ) : null} + {allowVisFullscreen ? ( + + + + ) : null} - {getMoreButton(classes.moreButton, false)} - {getMoreButton(classes.moreButtonSmall, true)}
+ setMoreOptionsIsOpen(!moreOptionsIsOpen)} + icon={} + component={getMoreMenu()} + > + +
{id && sharingDialogIsOpen && ( { return { ...dashboard, filtersLength: sGetNamedItemFilters(state).length, - starred: dashboard.id - ? sGetDashboardStarred(state, dashboard.id) - : false, showDescription: sGetShowDescription(state), } } export default connect(mapStateToProps, { - setDashboardStarred: acSetDashboardStarred, + setSlideshow: acSetSlideshow, removeAllFilters: acClearItemFilters, updateShowDescription: acSetShowDescription, -})(ViewActions) +})(ActionsBar) diff --git a/src/pages/view/TitleBar/FilterDialog.js b/src/components/DashboardsBar/InformationBlock/FilterDialog.js similarity index 96% rename from src/pages/view/TitleBar/FilterDialog.js rename to src/components/DashboardsBar/InformationBlock/FilterDialog.js index f1586e612..78d3e0443 100644 --- a/src/pages/view/TitleBar/FilterDialog.js +++ b/src/components/DashboardsBar/InformationBlock/FilterDialog.js @@ -31,9 +31,9 @@ import { acAddItemFilter, acRemoveItemFilter, } from '../../../actions/itemFilters.js' -import { useSystemSettings } from '../../../components/SystemSettingsProvider.js' -import { useUserSettings } from '../../../components/UserSettingsProvider.js' import { sGetItemFiltersRoot } from '../../../reducers/itemFilters.js' +import { useSystemSettings } from '../../SystemSettingsProvider.js' +import { useUserSettings } from '../../UserSettingsProvider.js' const FilterDialog = ({ dimension, diff --git a/src/pages/view/TitleBar/FilterSelector.js b/src/components/DashboardsBar/InformationBlock/FilterSelector.js similarity index 82% rename from src/pages/view/TitleBar/FilterSelector.js rename to src/components/DashboardsBar/InformationBlock/FilterSelector.js index cf32d25aa..69221edf9 100644 --- a/src/pages/view/TitleBar/FilterSelector.js +++ b/src/components/DashboardsBar/InformationBlock/FilterSelector.js @@ -10,12 +10,11 @@ import { acClearActiveModalDimension, acSetActiveModalDimension, } from '../../../actions/activeModalDimension.js' -import DropdownButton from '../../../components/DropdownButton/DropdownButton.js' import useDimensions from '../../../modules/useDimensions.js' import { sGetActiveModalDimension } from '../../../reducers/activeModalDimension.js' import { sGetItemFiltersRoot } from '../../../reducers/itemFilters.js' +import DropdownButton from '../../DropdownButton/DropdownButton.js' import FilterDialog from './FilterDialog.js' -import classes from './styles/FilterSelector.module.css' const FilterSelector = (props) => { const [filterDialogIsOpen, setFilterDialogIsOpen] = useState(false) @@ -60,17 +59,17 @@ const FilterSelector = (props) => { return props.restrictFilters && !props.allowedFilters?.length ? null : ( <> - - } - component={getFilterSelector()} - > - {i18n.t('Add filter')} - - + } + component={getFilterSelector()} + > + {i18n.t('Filter')} + {!isEmpty(props.dimension) ? ( { + const dataEngine = useDataEngine() + const { show: showAlert } = useAlert( + ({ msg }) => msg, + ({ isCritical }) => + isCritical ? { critical: true } : { warning: true } + ) + const toggleDashboardStarred = useCallback( + () => + apiStarDashboard(dataEngine, id, !starred) + .then(() => { + setDashboardStarred(id, !starred) + }) + .catch(() => { + const msg = starred + ? i18n.t('Failed to unstar the dashboard') + : i18n.t('Failed to star the dashboard') + showAlert({ msg, isCritical: false }) + }), + [dataEngine, id, setDashboardStarred, showAlert, starred] + ) + + if (!id) { + return null + } + + return ( +
+
+

+ {displayName} +

+ + +
+ +
+ ) +} + +InformationBlock.propTypes = { + displayName: PropTypes.string, + id: PropTypes.string, + setDashboardStarred: PropTypes.func, + starred: PropTypes.bool, +} + +const mapStateToProps = (state) => { + const dashboard = sGetSelected(state) + + return { + displayName: dashboard.displayName, + id: dashboard.id, + starred: dashboard.id + ? sGetDashboardStarred(state, dashboard.id) + : false, + } +} + +export default connect(mapStateToProps, { + setDashboardStarred: acSetDashboardStarred, +})(InformationBlock) diff --git a/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js b/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js new file mode 100644 index 000000000..081ddcbd9 --- /dev/null +++ b/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js @@ -0,0 +1,44 @@ +import i18n from '@dhis2/d2-i18n' +import { Tag, Tooltip } from '@dhis2/ui' +import moment from 'moment' +import PropTypes from 'prop-types' +import React from 'react' +import { useWindowDimensions } from '../../../components/WindowDimensionsProvider.js' +import { useCacheableSection } from '../../../modules/useCacheableSection.js' + +const LastUpdatedTag = ({ id }) => { + const { lastUpdated } = useCacheableSection(id) + const { width } = useWindowDimensions() + + if (!lastUpdated) { + return null + } + + const timeAgo = moment(lastUpdated).fromNow() + const message = + width > 480 + ? i18n.t('Offline data last updated {{timeAgo}}', { + timeAgo, + }) + : i18n.t('Synced {{timeAgo}}', { timeAgo }) + + return ( + + {(props) => ( +
+ {message} +
+ )} +
+ ) +} + +LastUpdatedTag.propTypes = { + id: PropTypes.string, +} + +export default LastUpdatedTag diff --git a/src/pages/view/TitleBar/StarDashboardButton.js b/src/components/DashboardsBar/InformationBlock/StarDashboardButton.js similarity index 66% rename from src/pages/view/TitleBar/StarDashboardButton.js rename to src/components/DashboardsBar/InformationBlock/StarDashboardButton.js index 6e8a3f7f2..2b54520ec 100644 --- a/src/pages/view/TitleBar/StarDashboardButton.js +++ b/src/components/DashboardsBar/InformationBlock/StarDashboardButton.js @@ -1,6 +1,6 @@ import { useDhis2ConnectionStatus } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { Tooltip, IconStar24, IconStarFilled24, colors } from '@dhis2/ui' +import { Tooltip, IconStar16, IconStarFilled16, colors } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import classes from './styles/StarDashboardButton.module.css' @@ -8,7 +8,7 @@ import classes from './styles/StarDashboardButton.module.css' const StarDashboardButton = ({ starred, onClick }) => { const { isConnected: online } = useDhis2ConnectionStatus() - const StarIcon = starred ? IconStarFilled24 : IconStar24 + const StarIcon = starred ? IconStarFilled16 : IconStar16 const handleOnClick = () => { online && onClick() @@ -32,22 +32,25 @@ const StarDashboardButton = ({ starred, onClick }) => { } return ( - + + )} + ) } diff --git a/src/pages/view/TitleBar/__tests__/FilterSelector.spec.js b/src/components/DashboardsBar/InformationBlock/__tests__/FilterSelector.spec.js similarity index 59% rename from src/pages/view/TitleBar/__tests__/FilterSelector.spec.js rename to src/components/DashboardsBar/InformationBlock/__tests__/FilterSelector.spec.js index ded375771..108da2d4e 100644 --- a/src/pages/view/TitleBar/__tests__/FilterSelector.spec.js +++ b/src/components/DashboardsBar/InformationBlock/__tests__/FilterSelector.spec.js @@ -2,81 +2,65 @@ import { useDhis2ConnectionStatus } from '@dhis2/app-runtime' import { render, screen } from '@testing-library/react' import React from 'react' import { Provider } from 'react-redux' -import configureMockStore from 'redux-mock-store' +import { createStore } from 'redux' import useDimensions from '../../../../modules/useDimensions.js' import FilterSelector from '../FilterSelector.js' -const mockStore = configureMockStore() - jest.mock('@dhis2/app-runtime', () => ({ useDhis2ConnectionStatus: jest.fn(() => ({ isDisconnected: false })), })) -/* eslint-disable react/prop-types */ -jest.mock( - '../../../../components/DropdownButton/DropdownButton.js', - () => - function Mock({ children, ...props }) { - return ( - - ) - } -) -/* eslint-enable react/prop-types */ - jest.mock('../../../../modules/useDimensions', () => jest.fn()) useDimensions.mockImplementation(() => ['Moomin', 'Snorkmaiden']) +const baseState = { activeModalDimension: {}, itemFilters: {} } +const createMockStore = (state) => + createStore(() => Object.assign({}, baseState, state)) + test('is disabled when offline', () => { useDhis2ConnectionStatus.mockImplementationOnce( jest.fn(() => ({ isDisconnected: true })) ) - const store = { activeModalDimension: {}, itemFilters: {} } - const props = { allowedFilters: [], restrictFilters: false, } - const { container } = render( - + const { getByTestId } = render( + ) - expect(container).toMatchSnapshot() + expect(getByTestId('dhis2-uicore-button')).toBeDisabled() }) test('is enabled when online', () => { - // useDhis2ConnectionStatus.mockImplementation(jest.fn(() => ({ isDisconnected: false }))) - - const store = { activeModalDimension: {}, itemFilters: {} } + useDhis2ConnectionStatus.mockImplementation( + jest.fn(() => ({ isDisconnected: false })) + ) const props = { allowedFilters: [], restrictFilters: false, } - const { container } = render( - + const { getByTestId } = render( + ) - expect(container).toMatchSnapshot() + expect(getByTestId('dhis2-uicore-button')).toBeEnabled() }) test('is null when no filters are restricted and no filters are allowed', () => { - const store = { activeModalDimension: {}, itemFilters: {} } - const props = { allowedFilters: [], restrictFilters: true, } const { container } = render( - + ) @@ -84,48 +68,43 @@ test('is null when no filters are restricted and no filters are allowed', () => }) test('is null when no filters are restricted and allowedFilters undefined', () => { - const store = { activeModalDimension: {}, itemFilters: {} } - const props = { restrictFilters: true, } const { container } = render( - + ) + expect(container.firstChild).toBeNull() }) test('shows button when filters are restricted and at least one filter is allowed', () => { - const store = { activeModalDimension: {}, itemFilters: {} } - const props = { allowedFilters: ['Moomin'], restrictFilters: true, } render( - + ) - expect(screen.getByRole('button')).toBeTruthy() + expect(screen.getByRole('button')).toBeVisible() }) test('shows button when filters are not restricted', () => { - const store = { activeModalDimension: {}, itemFilters: {} } - const props = { allowedFilters: [], restrictFilters: false, } render( - + ) - expect(screen.getByRole('button')).toBeTruthy() + expect(screen.getByRole('button')).toBeVisible() }) diff --git a/src/pages/view/TitleBar/apiStarDashboard.js b/src/components/DashboardsBar/InformationBlock/apiStarDashboard.js similarity index 100% rename from src/pages/view/TitleBar/apiStarDashboard.js rename to src/components/DashboardsBar/InformationBlock/apiStarDashboard.js diff --git a/src/components/DashboardsBar/InformationBlock/index.js b/src/components/DashboardsBar/InformationBlock/index.js new file mode 100644 index 000000000..20b922934 --- /dev/null +++ b/src/components/DashboardsBar/InformationBlock/index.js @@ -0,0 +1 @@ +export { default as InformationBlock } from './InformationBlock.js' diff --git a/src/components/DashboardsBar/InformationBlock/styles/ActionsBar.module.css b/src/components/DashboardsBar/InformationBlock/styles/ActionsBar.module.css new file mode 100644 index 000000000..7904c7a37 --- /dev/null +++ b/src/components/DashboardsBar/InformationBlock/styles/ActionsBar.module.css @@ -0,0 +1,18 @@ +.actions, +.hideOnSmallScreen { + display: flex; + gap: var(--spacers-dp4); + align-items: center; + justify-content: center; + block-size: 100%; +} + +.actions { + padding-inline-end: var(--spacers-dp8); +} + +@media only screen and (max-width: 480px) { + .hideOnSmallScreen { + display: none; + } +} diff --git a/src/components/DashboardsBar/InformationBlock/styles/InformationBlock.module.css b/src/components/DashboardsBar/InformationBlock/styles/InformationBlock.module.css new file mode 100644 index 000000000..4db96ab27 --- /dev/null +++ b/src/components/DashboardsBar/InformationBlock/styles/InformationBlock.module.css @@ -0,0 +1,33 @@ +.container { + display: flex; + flex-grow: 1; + align-items: center; + justify-content: space-between; + block-size: 100%; +} + +.titleContainer { + display: flex; + align-items: center; + justify-content: center; + block-size: 100%; + padding: 0 var(--spacers-dp8) 0 var(--spacers-dp12); + gap: var(--spacers-dp4); +} + +.title { + font-family: 'Roboto'; + font-size: 16px; + line-height: 16px; + font-weight: 500; + margin: 0; + color: var(--colors-grey900); + padding-block-start: var(--spacers-dp4); + padding-block-end: var(--spacers-dp4); +} + +@media only screen and (max-width: 480px) { + .title { + inset-block-start: 3px; + } +} diff --git a/src/components/DashboardsBar/InformationBlock/styles/StarDashboardButton.module.css b/src/components/DashboardsBar/InformationBlock/styles/StarDashboardButton.module.css new file mode 100644 index 000000000..c98693764 --- /dev/null +++ b/src/components/DashboardsBar/InformationBlock/styles/StarDashboardButton.module.css @@ -0,0 +1,47 @@ +.star { + display: inline-flex; + border: 0; + background: transparent; + padding: var(--spacers-dp4); + border-radius: 3px; + color: var(--colors-grey600); + cursor: pointer; + align-items: center; + justify-content: center; +} +.star:hover { + background: var(--colors-grey200); + color: var(--colors-grey800); +} +.star:active { + background: var(--colors-grey300); + color: var(--colors-grey900); +} +.star:focus { + outline: 3px solid var(--colors-blue600); + outline-offset: -3px; +} +/* Prevent focus styles when mouse clicking */ +.star:focus:not(:focus-visible) { + outline: none; + text-decoration: none; +} + +/* Prevent focus styles on active and disabled buttons */ +.star:active:focus, +.star:disabled:focus { + outline: none; + text-decoration: none; + cursor: not-allowed; +} +.iconWrap { + display: flex; + align-items: center; + justify-content: center; +} + +@media only screen and (max-width: 480px) { + button.star { + display: none; + } +} diff --git a/src/components/DashboardsBar/NavigationMenu/IconNavigation.js b/src/components/DashboardsBar/NavigationMenu/IconNavigation.js new file mode 100644 index 000000000..61e86161a --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/IconNavigation.js @@ -0,0 +1,12 @@ +import React from 'react' + +export const IconNavigation = () => ( + + + +) diff --git a/src/components/DashboardsBar/NavigationMenu/NavigationMenu.js b/src/components/DashboardsBar/NavigationMenu/NavigationMenu.js new file mode 100644 index 000000000..f06aea315 --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/NavigationMenu.js @@ -0,0 +1,94 @@ +import i18n from '@dhis2/d2-i18n' +import { Input, Menu } from '@dhis2/ui' +import cx from 'classnames' +import PropTypes from 'prop-types' +import React, { useCallback, useMemo, useEffect, useRef } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { acSetDashboardsFilter } from '../../../actions/dashboardsFilter.js' +import { sGetDashboardsSortedByStarred } from '../../../reducers/dashboards.js' +import { sGetDashboardsFilter } from '../../../reducers/dashboardsFilter.js' +import { NavigationMenuItem } from './NavigationMenuItem.js' +import styles from './styles/NavigationMenu.module.css' + +export const NavigationMenu = ({ close }) => { + const dispatch = useDispatch() + const scrollBoxRef = useRef(null) + const dashboards = useSelector(sGetDashboardsSortedByStarred) + const filterText = useSelector(sGetDashboardsFilter) + const onFilterChange = useCallback( + ({ value }) => { + dispatch(acSetDashboardsFilter(value)) + }, + [dispatch] + ) + const filteredDashboards = useMemo( + () => + dashboards.filter( + (dashboard) => + !filterText || + dashboard.displayName + .toLowerCase() + .includes(filterText.toLowerCase()) + ), + [filterText, dashboards] + ) + + useEffect(() => { + scrollBoxRef.current + ?.getElementsByClassName(styles.selectedItem) + ?.item(0) + ?.scrollIntoView({ + behavior: 'smooth', + block: 'end', + inline: 'nearest', + }) + }, []) + + if (dashboards.length === 0) { + return ( +
+

{i18n.t('No dashboards available.')}

+

{i18n.t('Create a new dashboard using the + button.')}

+
+ ) + } + + return ( +
+
+ +
+
+ + {filteredDashboards.length === 0 ? ( +
  • + {i18n.t('No dashboards found')} +
  • + ) : ( + filteredDashboards.map( + ({ displayName, id, starred }) => ( + + ) + ) + )} +
    +
    +
    + ) +} +NavigationMenu.propTypes = { + close: PropTypes.func.isRequired, +} diff --git a/src/components/DashboardsBar/NavigationMenu/NavigationMenuItem.js b/src/components/DashboardsBar/NavigationMenu/NavigationMenuItem.js new file mode 100644 index 000000000..1a0fc3741 --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/NavigationMenuItem.js @@ -0,0 +1,70 @@ +import { useDataEngine, useDhis2ConnectionStatus } from '@dhis2/app-runtime' +import { IconStarFilled16, MenuItem, colors } from '@dhis2/ui' +import debounce from 'lodash/debounce.js' +import PropTypes from 'prop-types' +import React, { useCallback } from 'react' +import { useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' +import { apiPostDataStatistics } from '../../../api/dataStatistics.js' +import { useCacheableSection } from '../../../modules/useCacheableSection.js' +import { sGetSelectedId } from '../../../reducers/selected.js' +import { IconOfflineSaved } from '../../IconOfflineSaved.js' +import styles from './styles/NavigationMenuItem.module.css' + +export const NavigationMenuItem = ({ + close, + displayName, + id, + starred, + tabIndex, +}) => { + const history = useHistory() + const { lastUpdated } = useCacheableSection(id) + const { isConnected } = useDhis2ConnectionStatus() + const engine = useDataEngine() + const selectedId = useSelector(sGetSelectedId) + const handleClick = useCallback(() => { + const debouncedPostStatistics = debounce( + () => apiPostDataStatistics('DASHBOARD_VIEW', id, engine), + 500 + ) + + history.push(`/${id}`) + close() + + if (isConnected) { + debouncedPostStatistics() + } + }, [close, engine, history, id, isConnected]) + + return ( + + {starred && ( + + )} + {displayName} + {!!lastUpdated && } + + } + ariaLabel={displayName} + className={id === selectedId ? styles.selectedItem : undefined} + /> + ) +} + +NavigationMenuItem.propTypes = { + close: PropTypes.func.isRequired, + displayName: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + starred: PropTypes.bool, + tabIndex: PropTypes.number, +} diff --git a/src/components/DashboardsBar/NavigationMenu/__tests__/NavigationMenu.spec.js b/src/components/DashboardsBar/NavigationMenu/__tests__/NavigationMenu.spec.js new file mode 100644 index 000000000..c8710ecd1 --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/__tests__/NavigationMenu.spec.js @@ -0,0 +1,90 @@ +import { render } from '@testing-library/react' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import { createStore } from 'redux' +import { NavigationMenu } from '../NavigationMenu.js' + +jest.mock('../NavigationMenuItem.js', () => ({ + NavigationMenuItem: ({ displayName }) => ( +
  • {displayName}
  • + ), +})) +const baseState = { + dashboards: { + nghVC4wtyzi: { + id: 'nghVC4wtyzi', + displayName: 'Antenatal Care', + starred: true, + }, + rmPiJIPFL4U: { + displayName: 'Antenatal Care data', + id: 'rmPiJIPFL4U', + starred: false, + }, + JW7RlN5xafN: { + displayName: 'Cases Malaria', + id: 'JW7RlN5xafN', + starred: false, + }, + iMnYyBfSxmM: { + displayName: 'Delivery', + id: 'iMnYyBfSxmM', + starred: false, + }, + vqh4MBWOTi4: { + displayName: 'Disease Surveillance', + id: 'vqh4MBWOTi4', + starred: false, + }, + }, + dashboardsFilter: '', +} + +const createMockStore = (state) => + createStore(() => Object.assign({}, baseState, state)) + +test('renders a list of dashboard menu items', () => { + const mockStore = createMockStore({}) + const { getAllByRole } = render( + + + {}} /> + + + ) + expect(getAllByRole('menu-item')).toHaveLength(5) +}) + +test('renders a notification if no dashboards are available', () => { + const mockStore = createMockStore({ dashboards: {} }) + const { getByText } = render( + + + {}} /> + + + ) + + expect(getByText('No dashboards available.')).toBeVisible() + expect( + getByText('Create a new dashboard using the + button.') + ).toBeVisible() +}) + +test('renders a placeholder list item if no dashboards meet the filter criteria', () => { + const filterStr = 'xxxxxxxxxxxxx' + const mockStore = createMockStore({ dashboardsFilter: filterStr }) + const { getByText, getByPlaceholderText } = render( + + + {}} /> + + + ) + expect(getByPlaceholderText('Search for a dashboard')).toHaveValue( + filterStr + ) + expect(getByText('No dashboards found')).toBeVisible() +}) diff --git a/src/components/DashboardsBar/NavigationMenu/__tests__/NavigationMenuItem.spec.js b/src/components/DashboardsBar/NavigationMenu/__tests__/NavigationMenuItem.spec.js new file mode 100644 index 000000000..fb1f29b51 --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/__tests__/NavigationMenuItem.spec.js @@ -0,0 +1,206 @@ +import { + useCacheableSection, + useDhis2ConnectionStatus, +} from '@dhis2/app-runtime' +import { render, fireEvent } from '@testing-library/react' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Provider } from 'react-redux' +import { Router, useHistory } from 'react-router-dom' +import { createStore } from 'redux' +import { apiPostDataStatistics } from '../../../../api/dataStatistics.js' +import { NavigationMenuItem } from '../NavigationMenuItem.js' + +jest.mock('@dhis2/app-runtime', () => ({ + useDhis2ConnectionStatus: jest.fn(() => ({ isConnected: true })), + useCacheableSection: jest.fn(), + useDataEngine: jest.fn(), +})) + +jest.mock('@dhis2/analytics', () => ({ + useCachedDataQuery: () => ({ + currentUser: { + username: 'rainbowDash', + id: 'r3nb0d5h', + }, + }), +})) + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: jest.fn(), +})) + +jest.mock('../../../../api/dataStatistics.js', () => ({ + apiPostDataStatistics: jest.fn(), +})) + +const mockOfflineDashboard = { + lastUpdated: 'Jan 10', +} + +const mockNonOfflineDashboard = { + lastUpdated: null, +} + +const defaultProps = { + starred: false, + displayName: 'Rainbow Dash', + id: 'rainbowdash', + close: Function.prototype, +} + +const selectedId = 'theselectedid' + +const defaultStoreFn = () => ({ + selected: { + id: selectedId, + }, +}) + +test('renders an inactive MenuItem for a dashboard', () => { + useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) + const mockStore = createStore(defaultStoreFn) + const { container } = render( + + + + + + ) + expect(container.querySelector('.container').childNodes).toHaveLength(1) +}) + +test('renders an inactive MenuItem with a star icon, for a starred dashboard', () => { + useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) + const mockStore = createStore(defaultStoreFn) + const { container, getByTestId } = render( + + + + + + ) + + expect(container.querySelector('.container').childNodes).toHaveLength(2) + expect(getByTestId('starred-dashboard')).toBeVisible() +}) + +test('renders an inactive MenuItem with an offline icon for a cached dashboard', () => { + useCacheableSection.mockImplementation(() => mockOfflineDashboard) + const mockStore = createStore(defaultStoreFn) + const { getByTestId, container } = render( + + + + + + ) + expect(container.querySelector('.container').childNodes).toHaveLength(2) + expect(getByTestId('dashboard-saved-offline')).toBeVisible() +}) + +test('renders an active MenuItem for the currently selected dashboard', () => { + useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) + const mockStore = createStore(defaultStoreFn) + const { getByTestId, container } = render( + + + + + + ) + + expect(container.querySelector('.container').childNodes).toHaveLength(1) + expect(getByTestId('dhis2-uicore-menuitem')).toBeVisible() +}) + +test('Navigates to the related menu item when an item is clicked', () => { + useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) + const historyPushMock = jest.fn() + useHistory.mockImplementation(() => ({ + push: historyPushMock, + })) + const mockStore = createStore(defaultStoreFn) + const { getByText } = render( + + + + + + ) + fireEvent.click(getByText(defaultProps.displayName)) + expect(historyPushMock).toHaveBeenCalledTimes(1) + expect(historyPushMock).toHaveBeenCalledWith(`/${defaultProps.id}`) +}) + +test('Closes the navigation menu if current dashboard is clicked', () => { + useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) + const historyPushMock = jest.fn() + useHistory.mockImplementation(() => ({ + push: historyPushMock, + })) + const mockStore = createStore(defaultStoreFn) + const props = { + ...defaultProps, + id: selectedId, + close: jest.fn(), + } + const { getByText } = render( + + + + + + ) + fireEvent.click(getByText(defaultProps.displayName)) + expect(historyPushMock).toHaveBeenCalledTimes(1) + expect(historyPushMock).toHaveBeenCalledWith(`/${selectedId}`) + expect(props.close).toHaveBeenCalledTimes(1) +}) + +it('Posts data statistics if connected', () => { + jest.useFakeTimers() + const apiPostDataStatisticsMock = jest.fn() + apiPostDataStatistics.mockImplementation(apiPostDataStatisticsMock) + const mockStore = createStore(defaultStoreFn) + const { getByText } = render( + + + + + + ) + fireEvent.click(getByText(defaultProps.displayName)) + jest.runAllTimers() + expect(apiPostDataStatisticsMock).toHaveBeenCalledWith( + 'DASHBOARD_VIEW', + 'rainbowdash', + undefined + ) +}) + +it('Does not post data statistics if not connected', async () => { + useDhis2ConnectionStatus.mockReturnValue({ isConnected: false }) + const historyPushMock = jest.fn() + useHistory.mockImplementation(() => ({ + push: historyPushMock, + })) + jest.useFakeTimers() + const apiPostDataStatisticsMock = jest.fn() + apiPostDataStatistics.mockImplementation(apiPostDataStatisticsMock) + const mockStore = createStore(defaultStoreFn) + const { getByText } = render( + + + + + + ) + fireEvent.click(getByText(defaultProps.displayName)) + jest.runAllTimers() + expect(apiPostDataStatisticsMock).not.toHaveBeenCalled() + // Navigation should still work + expect(historyPushMock).toHaveBeenCalledTimes(1) + expect(historyPushMock).toHaveBeenCalledWith(`/${defaultProps.id}`) +}) diff --git a/src/components/DashboardsBar/NavigationMenu/index.js b/src/components/DashboardsBar/NavigationMenu/index.js new file mode 100644 index 000000000..cdb995032 --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/index.js @@ -0,0 +1,2 @@ +export { IconNavigation } from './IconNavigation.js' +export { NavigationMenu } from './NavigationMenu.js' diff --git a/src/components/DashboardsBar/NavigationMenu/styles/NavigationMenu.module.css b/src/components/DashboardsBar/NavigationMenu/styles/NavigationMenu.module.css new file mode 100644 index 000000000..da5626d33 --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/styles/NavigationMenu.module.css @@ -0,0 +1,55 @@ +.container { + min-inline-size: 480px; + max-inline-size: 720px; + border: 1px solid var(--colors-grey200); + border-radius: 3px; + box-shadow: var(--elevations-e300); + background-color: var(--colors-white); +} +.filterWrap { + padding-block: 8px 4px; + padding-inline: 8px; +} +.scrollbox { + /* On larger screens the max is restricted to 1000px and if + * there is less space available, the max height is restricted + * to the window height. Main header is 48px, dashboards-bar + * is 45px and the filter-wrap is 44px, so total height above + * is 137px so 100vh - 152px ensures that 15px of whitespace + * is visible below the menu. */ + max-block-size: min(1000px, calc(100vh - 152px)); + overflow-y: auto; + scroll-behavior: smooth; +} +.noItems { + color: var(--colors-grey700); + font-size: 13px; + line-height: 16px; + padding-block: 8px; + padding-inline: 24px; + text-align: center; +} +.noDashboardsAvailable { + block-size: 240px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.noDashboardsAvailable > p { + font-size: 14px; + color: var(--colors-grey700); +} + +@media only screen and (max-width: 480px) { + .container { + min-inline-size: 320px; + max-inline-size: 460px; + } + .scrollbox { + /* 176px instead of 152px is needed because the online-status + * indicator is is showing below the main header and this has + * a height of 24px. */ + max-block-size: calc(100vh - 176px); + } +} diff --git a/src/components/DashboardsBar/NavigationMenu/styles/NavigationMenuItem.module.css b/src/components/DashboardsBar/NavigationMenu/styles/NavigationMenuItem.module.css new file mode 100644 index 000000000..dbbff5c3f --- /dev/null +++ b/src/components/DashboardsBar/NavigationMenu/styles/NavigationMenuItem.module.css @@ -0,0 +1,16 @@ +.container { + display: flex; + gap: 8px; +} +.displayName { + flex-wrap: wrap; +} +.container > :global(svg) { + inline-size: 16px; + block-size: 16px; + flex-shrink: 0; +} +.selectedItem { + background-color: var(--colors-teal600) !important; + color: var(--colors-white) !important; +} diff --git a/src/components/DashboardsBar/ShowMoreButton.js b/src/components/DashboardsBar/ShowMoreButton.js deleted file mode 100644 index b62bd5307..000000000 --- a/src/components/DashboardsBar/ShowMoreButton.js +++ /dev/null @@ -1,72 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { Tooltip } from '@dhis2/ui' -import PropTypes from 'prop-types' -import React, { useRef } from 'react' -import { ChevronDown, ChevronUp } from './assets/icons.js' -import classes from './styles/ShowMoreButton.module.css' - -const ShowMoreButton = ({ onClick, dashboardBarIsExpanded, disabled }) => { - const containerRef = useRef(null) - const buttonLabel = dashboardBarIsExpanded - ? i18n.t('Show fewer dashboards') - : i18n.t('Show more dashboards') - - const onButtonClicked = () => { - // The click may happen on the svg or path - // element of the button icon. - // In that case it is necessary to trigger - // the mouseout on the button element - // to ensure that the tooltip is removed - const buttonEl = containerRef.current.children[0] - const event = new MouseEvent('mouseout', { - bubbles: true, - cancelable: false, - }) - - onClick() - buttonEl.dispatchEvent(event) - } - - return ( -
    - {disabled ? ( -
    - -
    - ) : ( - - {({ onMouseOver, onMouseOut, ref }) => ( - - )} - - )} -
    - ) -} - -ShowMoreButton.propTypes = { - dashboardBarIsExpanded: PropTypes.bool, - disabled: PropTypes.bool, - onClick: PropTypes.func, -} - -export default ShowMoreButton diff --git a/src/components/DashboardsBar/__tests__/Chip.spec.js b/src/components/DashboardsBar/__tests__/Chip.spec.js deleted file mode 100644 index 6215f9c4c..000000000 --- a/src/components/DashboardsBar/__tests__/Chip.spec.js +++ /dev/null @@ -1,141 +0,0 @@ -import { useCacheableSection } from '@dhis2/app-runtime' -import { render } from '@testing-library/react' -import { createMemoryHistory } from 'history' -import React from 'react' -import { Router } from 'react-router-dom' -import Chip from '../Chip.js' - -/* eslint-disable react/prop-types */ -jest.mock('@dhis2/ui', () => { - const originalModule = jest.requireActual('@dhis2/ui') - - return { - __esModule: true, - ...originalModule, - Chip: function Mock({ children, icon, selected }) { - const componentProps = { - starred: icon ? 'yes' : 'no', - isselected: selected ? 'yes' : 'no', - } - - return ( -
    - {children} -
    - ) - }, - } -}) -/* eslint-enable react/prop-types */ - -jest.mock('@dhis2/app-runtime', () => ({ - useDhis2ConnectionStatus: () => ({ isConnected: true }), - useCacheableSection: jest.fn(), - useDataEngine: jest.fn(), -})) - -jest.mock('@dhis2/analytics', () => ({ - useCachedDataQuery: () => ({ - currentUser: { - username: 'rainbowDash', - id: 'r3nb0d5h', - }, - }), -})) - -const mockOfflineDashboard = { - lastUpdated: 'Jan 10', -} - -const mockNonOfflineDashboard = { - lastUpdated: null, -} - -const defaultProps = { - starred: false, - selected: false, - onClick: jest.fn(), - label: 'Rainbow Dash', - dashboardId: 'rainbowdash', - classes: { - icon: 'iconClass', - selected: 'selectedClass', - unselected: 'unselectedClass', - }, -} - -test('renders an unstarred, unselected chip for a non-cached dashboard', () => { - useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) - const { container } = render( - - - - ) - - expect(container).toMatchSnapshot() -}) - -test('renders an unstarred, unselected chip for cached dashboard', () => { - useCacheableSection.mockImplementation(() => mockOfflineDashboard) - const { container } = render( - - - - ) - - expect(container).toMatchSnapshot() -}) - -test('renders a starred, unselected chip for a non-cached dashboard', () => { - useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) - const props = Object.assign({}, defaultProps, { starred: true }) - const { container } = render( - - - - ) - - expect(container).toMatchSnapshot() -}) - -test('renders a starred, unselected chip for a cached dashboard', () => { - useCacheableSection.mockImplementation(() => mockOfflineDashboard) - const props = Object.assign({}, defaultProps, { starred: true }) - const { container } = render( - - - - ) - - expect(container).toMatchSnapshot() -}) - -test('renders a starred, selected chip for non-cached dashboard', () => { - useCacheableSection.mockImplementation(() => mockNonOfflineDashboard) - const props = Object.assign({}, defaultProps, { - starred: true, - selected: true, - }) - const { container } = render( - - - - ) - - expect(container).toMatchSnapshot() -}) - -test('renders a starred, selected chip for a cached dashboard', () => { - useCacheableSection.mockImplementation(() => mockOfflineDashboard) - const props = Object.assign({}, defaultProps, { - starred: true, - selected: true, - }) - const { container } = render( - - - - ) - - expect(container).toMatchSnapshot() -}) diff --git a/src/components/DashboardsBar/__tests__/ClearButton.spec.js b/src/components/DashboardsBar/__tests__/ClearButton.spec.js deleted file mode 100644 index 6f184b9bc..000000000 --- a/src/components/DashboardsBar/__tests__/ClearButton.spec.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render } from '@testing-library/react' -import React from 'react' -import ClearButton from '../ClearButton.js' - -test('ClearButton renders a button', () => { - const { container } = render() - expect(container).toMatchSnapshot() -}) diff --git a/src/components/DashboardsBar/__tests__/DashboardsBar.spec.js b/src/components/DashboardsBar/__tests__/DashboardsBar.spec.js deleted file mode 100644 index 2abd414ce..000000000 --- a/src/components/DashboardsBar/__tests__/DashboardsBar.spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import { within } from '@testing-library/dom' -import { render } from '@testing-library/react' -import { createMemoryHistory } from 'history' -import React from 'react' -import { Provider } from 'react-redux' -import { Router } from 'react-router-dom' -import configureMockStore from 'redux-mock-store' -import WindowDimensionsProvider from '../../../components/WindowDimensionsProvider.js' -import DashboardsBar, { - MIN_ROW_COUNT, - MAX_ROW_COUNT, -} from '../DashboardsBar.js' - -jest.mock('@dhis2/analytics', () => ({ - useCachedDataQuery: () => ({ - currentUser: { - username: 'rainbowDash', - id: 'r3nb0d5h', - }, - }), -})) - -const mockStore = configureMockStore() -const dashboards = { - rainbow123: { - id: 'rainbow123', - displayName: 'Rainbow Dash', - starred: false, - }, - fluttershy123: { - id: 'fluttershy123', - displayName: 'Fluttershy', - starred: true, - }, -} - -jest.mock('@dhis2/app-runtime', () => ({ - useDhis2ConnectionStatus: () => ({ isConnected: true }), - useCacheableSection: jest.fn(() => ({ - isCached: false, - recordingState: 'default', - })), - useDataEngine: jest.fn(), -})) - -test('minimized DashboardsBar has Show more/less button', () => { - const store = { - dashboards, - dashboardsFilter: '', - controlBar: { userRows: parseInt(MIN_ROW_COUNT) }, - selected: { id: 'rainbow123' }, - } - const { queryAllByRole, queryByLabelText } = render( - - - - - - - - ) - const links = queryAllByRole('link') - expect(links.length).toEqual(Object.keys(dashboards).length) - expect(queryByLabelText('Show more dashboards')).toBeTruthy() -}) - -test('maximized DashboardsBar does not have a Show more/less button', () => { - const store = { - dashboards, - dashboardsFilter: '', - controlBar: { userRows: parseInt(MAX_ROW_COUNT) }, - selected: { id: 'rainbow123' }, - } - const { queryByLabelText } = render( - - - - - - - - ) - expect(queryByLabelText('Show more dashboards')).toBeNull() -}) - -test('renders a DashboardsBar with selected item', () => { - const store = { - dashboards, - dashboardsFilter: '', - controlBar: { userRows: parseInt(MIN_ROW_COUNT) }, - selected: { id: 'fluttershy123' }, - } - - const { queryAllByRole } = render( - - - - - - - - ) - - const chips = queryAllByRole('link') - - const fluttershyChip = chips.find((lnk) => - within(lnk).queryByText('Fluttershy') - ) - - expect(fluttershyChip.firstChild.classList.contains('selected')).toBe(true) - - const rainbowChip = chips.find((lnk) => - within(lnk).queryByText('Rainbow Dash') - ) - expect(rainbowChip.firstChild.classList.contains('selected')).toBe(false) -}) - -test('renders a DashboardsBar with no items', () => { - const store = { - dashboards: {}, - dashboardsFilter: '', - controlBar: { userRows: parseInt(MIN_ROW_COUNT) }, - selected: { id: 'rainbow123' }, - } - - const { queryByRole } = render( - - - - - - - - ) - expect(queryByRole('link')).toBeNull() -}) diff --git a/src/components/DashboardsBar/__tests__/Filter.spec.js b/src/components/DashboardsBar/__tests__/Filter.spec.js deleted file mode 100644 index d7e2332ff..000000000 --- a/src/components/DashboardsBar/__tests__/Filter.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { render } from '@testing-library/react' -import React from 'react' -import { Provider } from 'react-redux' -import configureMockStore from 'redux-mock-store' -import WindowDimensionsProvider from '../../../components/WindowDimensionsProvider.js' -import Filter from '../Filter.js' - -const mockStore = configureMockStore() - -test('Filter renders with empty filter text', () => { - const store = { - dashboardsFilter: '', - } - const props = { classes: {} } - const { container } = render( - - - - - - ) - expect(container).toMatchSnapshot() -}) - -test('Filter renders with filter text', () => { - const store = { - dashboardsFilter: 'rainbow', - } - - const props = { classes: {} } - const { container } = render( - - - - - - ) - - expect(container).toMatchSnapshot() -}) diff --git a/src/components/DashboardsBar/__tests__/ShowMoreButton.spec.js b/src/components/DashboardsBar/__tests__/ShowMoreButton.spec.js deleted file mode 100644 index 5907609ed..000000000 --- a/src/components/DashboardsBar/__tests__/ShowMoreButton.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import { fireEvent } from '@testing-library/dom' -import { render } from '@testing-library/react' -import React from 'react' -import ShowMoreButton from '../ShowMoreButton.js' - -describe('ShowMoreButton', () => { - it('renders correctly when at maxHeight', () => { - const { container } = render( - {}} - isMaxHeight={true} - classes={{ showMore: {} }} - /> - ) - expect(container).toMatchSnapshot() - }) - - it('renders correctly when not at maxHeight', () => { - const { container } = render( - {}} - isMaxHeight={false} - classes={{ showMore: {} }} - /> - ) - - expect(container).toMatchSnapshot() - }) - - it('triggers onClick when button clicked', () => { - const onClick = jest.fn() - const { getByLabelText } = render( - - ) - fireEvent.click(getByLabelText('Show more dashboards')) - expect(onClick).toHaveBeenCalled() - }) -}) diff --git a/src/components/DashboardsBar/__tests__/__snapshots__/Chip.spec.js.snap b/src/components/DashboardsBar/__tests__/__snapshots__/Chip.spec.js.snap deleted file mode 100644 index 662a16a20..000000000 --- a/src/components/DashboardsBar/__tests__/__snapshots__/Chip.spec.js.snap +++ /dev/null @@ -1,166 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders a starred, selected chip for a cached dashboard 1`] = ` - -`; - -exports[`renders a starred, selected chip for non-cached dashboard 1`] = ` - -`; - -exports[`renders a starred, unselected chip for a cached dashboard 1`] = ` - -`; - -exports[`renders a starred, unselected chip for a non-cached dashboard 1`] = ` - -`; - -exports[`renders an unstarred, unselected chip for a non-cached dashboard 1`] = ` - -`; - -exports[`renders an unstarred, unselected chip for cached dashboard 1`] = ` - -`; diff --git a/src/components/DashboardsBar/__tests__/__snapshots__/ClearButton.spec.js.snap b/src/components/DashboardsBar/__tests__/__snapshots__/ClearButton.spec.js.snap deleted file mode 100644 index d602308a9..000000000 --- a/src/components/DashboardsBar/__tests__/__snapshots__/ClearButton.spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ClearButton renders a button 1`] = ` -
    - -
    -`; diff --git a/src/components/DashboardsBar/__tests__/__snapshots__/Filter.spec.js.snap b/src/components/DashboardsBar/__tests__/__snapshots__/Filter.spec.js.snap deleted file mode 100644 index 5294f62d2..000000000 --- a/src/components/DashboardsBar/__tests__/__snapshots__/Filter.spec.js.snap +++ /dev/null @@ -1,130 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Filter renders with empty filter text 1`] = ` -
    - -
    -`; - -exports[`Filter renders with filter text 1`] = ` -
    - -
    -`; diff --git a/src/components/DashboardsBar/__tests__/__snapshots__/ShowMoreButton.spec.js.snap b/src/components/DashboardsBar/__tests__/__snapshots__/ShowMoreButton.spec.js.snap deleted file mode 100644 index be3243aff..000000000 --- a/src/components/DashboardsBar/__tests__/__snapshots__/ShowMoreButton.spec.js.snap +++ /dev/null @@ -1,53 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ShowMoreButton renders correctly when at maxHeight 1`] = ` -
    -
    - -
    -
    -`; - -exports[`ShowMoreButton renders correctly when not at maxHeight 1`] = ` -
    -
    - -
    -
    -`; diff --git a/src/components/DashboardsBar/__tests__/getRowsFromHeight.js b/src/components/DashboardsBar/__tests__/getRowsFromHeight.js deleted file mode 100644 index 71ef6396a..000000000 --- a/src/components/DashboardsBar/__tests__/getRowsFromHeight.js +++ /dev/null @@ -1,6 +0,0 @@ -import { getRowsFromHeight } from '../getRowsFromHeight.js' - -test('getRowsFromHeight returns an integer', () => { - const res = getRowsFromHeight(100) - expect(Number.isInteger(res)).toBeTruthy() -}) diff --git a/src/components/DashboardsBar/assets/AddCircle.js b/src/components/DashboardsBar/assets/AddCircle.js deleted file mode 100644 index 0f1c92466..000000000 --- a/src/components/DashboardsBar/assets/AddCircle.js +++ /dev/null @@ -1,16 +0,0 @@ -import { colors } from '@dhis2/ui' -import React from 'react' - -const AddCircleIcon = () => ( - - - -) - -export default AddCircleIcon diff --git a/src/components/DashboardsBar/assets/Clear.js b/src/components/DashboardsBar/assets/Clear.js deleted file mode 100644 index e1d2c34a4..000000000 --- a/src/components/DashboardsBar/assets/Clear.js +++ /dev/null @@ -1,21 +0,0 @@ -import { colors } from '@dhis2/ui' -import PropTypes from 'prop-types' -import React from 'react' - -const ClearIcon = ({ className }) => ( - - - - -) - -ClearIcon.propTypes = { - className: PropTypes.string, -} - -export default ClearIcon diff --git a/src/components/DashboardsBar/assets/Search.js b/src/components/DashboardsBar/assets/Search.js deleted file mode 100644 index fa1a1fc75..000000000 --- a/src/components/DashboardsBar/assets/Search.js +++ /dev/null @@ -1,36 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' - -const SearchIcon = ({ className, small }) => - small ? ( - - - - ) : ( - - - - - ) - -SearchIcon.propTypes = { - className: PropTypes.string, - small: PropTypes.bool, -} - -export default SearchIcon diff --git a/src/components/DashboardsBar/assets/icons.js b/src/components/DashboardsBar/assets/icons.js deleted file mode 100644 index b5334e209..000000000 --- a/src/components/DashboardsBar/assets/icons.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' - -export const ChevronDown = () => ( - - - -) - -export const ChevronUp = () => ( - - - -) - -export const OfflineSaved = ({ className }) => ( - - - -) - -OfflineSaved.propTypes = { - className: PropTypes.string, -} diff --git a/src/components/DashboardsBar/getFilteredDashboards.js b/src/components/DashboardsBar/getFilteredDashboards.js deleted file mode 100644 index 8f0de965f..000000000 --- a/src/components/DashboardsBar/getFilteredDashboards.js +++ /dev/null @@ -1,16 +0,0 @@ -import arraySort from 'd2-utilizr/lib/arraySort.js' - -export const getFilteredDashboards = (dashboards, filterText) => { - 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), - ] -} diff --git a/src/components/DashboardsBar/getRowsFromHeight.js b/src/components/DashboardsBar/getRowsFromHeight.js deleted file mode 100644 index b9c6246b9..000000000 --- a/src/components/DashboardsBar/getRowsFromHeight.js +++ /dev/null @@ -1,9 +0,0 @@ -const ROW_HEIGHT = 40 -const PADDING_TOP = 10 -const SHOWMORE_BUTTON_HEIGHT = 21 // 27px - 6px below bottom edge of ctrlbar - -export const getRowsFromHeight = (height) => { - return Math.round( - (height - SHOWMORE_BUTTON_HEIGHT - PADDING_TOP) / ROW_HEIGHT - ) -} diff --git a/src/components/DashboardsBar/index.js b/src/components/DashboardsBar/index.js new file mode 100644 index 000000000..3e4f02e9b --- /dev/null +++ b/src/components/DashboardsBar/index.js @@ -0,0 +1,3 @@ +import { DashboardsBar } from './DashboardsBar.js' + +export default DashboardsBar diff --git a/src/components/DashboardsBar/styles/Chip.module.css b/src/components/DashboardsBar/styles/Chip.module.css deleted file mode 100644 index 5ce6bf86a..000000000 --- a/src/components/DashboardsBar/styles/Chip.module.css +++ /dev/null @@ -1,41 +0,0 @@ -.link { - display: inline-block; - text-decoration: none; - vertical-align: top; -} - -.labelWithAdornment { - position: relative; - inset-block-start: -2px; -} - -.adornment { - margin-inline-start: var(--spacers-dp4); - fill: var(--colors-grey600); -} - -.adornment.selected { - fill: var(--colors-white); -} - -.progressIndicator { - margin-block-start: 0 !important; - margin-block-end: 0 !important; - margin-inline-start: 4px !important; - margin-inline-end: 0 !important; - inline-size: 16px !important; - block-size: 16px !important; -} - -.progressIndicator.selected { - color: var(--colors-white); -} - -@media only screen and (max-width: 480px) { - .link { - margin-block-start: 0; - margin-block-end: 0; - margin-inline-start: -2px; - margin-inline-end: -2px; - } -} diff --git a/src/components/DashboardsBar/styles/ClearButton.module.css b/src/components/DashboardsBar/styles/ClearButton.module.css deleted file mode 100644 index 1340b985d..000000000 --- a/src/components/DashboardsBar/styles/ClearButton.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.button { - border: none; - border-radius: 50%; - cursor: pointer; - inline-size: 24px; - block-size: 24px; -} - -.button > span { - display: flex; - align-items: center; - justify-content: center; -} - -.button:focus { - outline: none; -} - -.icon { - inline-size: 16px; - block-size: 16px; -} - -@media only screen and (max-width: 480px) { - .button { - margin-block-end: var(--spacers-dp4); - margin-inline-end: 1px; - } -} diff --git a/src/components/DashboardsBar/styles/Content.module.css b/src/components/DashboardsBar/styles/Content.module.css deleted file mode 100644 index b1a997824..000000000 --- a/src/components/DashboardsBar/styles/Content.module.css +++ /dev/null @@ -1,89 +0,0 @@ -.container { - display: inline; -} - -.controlsSmall { - display: none; -} - -.controlsLarge { - display: inline-flex; - position: relative; - inset-block-start: 5px; -} - -.buttonPadding { - padding-block-start: 2px; - padding-block-end: 0; - padding-inline-start: var(--spacers-dp8); - padding-inline-end: var(--spacers-dp8); - display: inline-flex; -} - -.buttonPosition { - position: relative; - display: inline-flex; -} - -.chipsContainer { - min-block-size: 40px; -} - -@media only screen and (max-width: 480px) { - .newLink { - display: none; - } - - .controlsSmall { - display: block; - margin-block-end: var(--spacers-dp4); - } - - .controlsLarge { - display: none; - } - - .container.collapsed { - display: flex; - overflow-x: auto; - overflow-y: hidden; - padding-block-start: var(--spacers-dp4); - padding-block-end: var(--spacers-dp4); - padding-inline-start: var(--spacers-dp4); - padding-inline-end: 0; - } - - .container.expanded { - display: flex; - flex-direction: column; - overflow: hidden; - padding-block-start: var(--spacers-dp12); - padding-inline-start: var(--spacers-dp8); - } - - .expanded .chipsContainer .controls { - margin-inline-start: var(--spacers-dp4); - margin-inline-end: var(--spacers-dp8); - inline-size: 100%; - } - - .chipsContainer { - margin-block-end: -4px; - padding-inline-end: 2px; - min-block-size: 0; - } - - .expanded .chipsContainer { - overflow-x: hidden; - overflow-y: auto; - padding-inline-end: 6px; - flex: 1; - } - - .collapsed .chipsContainer { - overflow-x: auto; - overflow-y: hidden; - display: flex; - flex-wrap: nowrap; - } -} diff --git a/src/components/DashboardsBar/styles/DashboardsBar.module.css b/src/components/DashboardsBar/styles/DashboardsBar.module.css index 5455464e9..d8acae44b 100644 --- a/src/components/DashboardsBar/styles/DashboardsBar.module.css +++ b/src/components/DashboardsBar/styles/DashboardsBar.module.css @@ -1,92 +1,24 @@ -.bar { - position: relative; -} - -.container { - position: relative; - background-color: var(--colors-white); - box-shadow: rgba(0, 0, 0, 0.2) 0 0 6px 3px; - overflow: hidden; - box-sizing: border-box; - flex: none; +.toolbar { + background: var(--colors-white); + border-block-end: 1px solid var(--colors-grey400); display: flex; - flex-direction: column; - block-size: var(--user-rows-height); - inline-size: 100%; - z-index: 1; -} - -.content { - padding-block-start: 10px; - padding-block-end: 0; - padding-inline-start: 6px; - padding-inline-end: 6px; - overflow: hidden; - margin-block-end: 21px; /* to make space for the show more button */ -} - -.expanded .content { - overflow-y: auto; -} - -.expanded .container { - block-size: var(--max-rows-height); - z-index: 1999; -} - -.spacer { - display: none; - box-sizing: border-box; - flex: none; - block-size: var(--user-rows-height); + justify-content: space-between; + align-items: center; } -@media only screen and (min-width: 481px) { - .expanded .spacer { - display: block; - } - - .expanded .container { - position: absolute; - } +.blockCreationNavigation { + display: flex; + block-size: 100%; + align-items: center; + flex-shrink: 0; + gap: var(--spacers-dp4); + padding: var(--spacers-dp8) var(--spacers-dp12) 9px var(--spacers-dp8); + border-inline-end: 1px solid var(--colors-grey400); } @media only screen and (max-width: 480px) { - .content { - padding: 0; - display: flex; - flex-wrap: wrap; - } - - .collapsed .content { - flex-wrap: nowrap; - } - - .expanded .content { - overflow-y: hidden; - flex-direction: column; - } - - .expanded .spacer { - display: block; - block-size: var(--min-rows-height); - } -} - -/* phone LANDSCAPE MODE or small screen */ -@media only screen and (max-height: 480px), only screen and (max-width: 480px) { - .collapsed .container { - block-size: var(--min-rows-height); - } - - .expanded .container { - position: absolute; - display: flex; - flex-direction: column; - block-size: var(--sm-expanded-controlbar-height); - } - - .expanded .content { - flex-direction: column; + .blockCreationNavigation .createDashboardButton, + .navMenuButtonText { + display: none; } } diff --git a/src/components/DashboardsBar/styles/DragHandle.module.css b/src/components/DashboardsBar/styles/DragHandle.module.css deleted file mode 100644 index 6da7d0850..000000000 --- a/src/components/DashboardsBar/styles/DragHandle.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.draghandle { - position: absolute; - inset-block-end: 0; - inset-inline-start: 0; - inset-inline-end: 0; - cursor: ns-resize; - transition: all ease-out 75ms; - block-size: 7px; -} - -.draghandle:after { - content: ''; - position: absolute; - inset-block-end: 0; - block-size: 3px; - inline-size: 100%; - background: var(--colors-white); -} - -.draghandle:hover:after { - background: var(--colors-blue300); - transition: background 0.2s 0.1s; -} - -.draghandle:active:after { - background: var(--colors-blue500); - transition: none; -} - -@media only screen and (max-width: 480px) { - .draghandle { - display: none; - } -} diff --git a/src/components/DashboardsBar/styles/Filter.module.css b/src/components/DashboardsBar/styles/Filter.module.css deleted file mode 100644 index 8f4e43cf9..000000000 --- a/src/components/DashboardsBar/styles/Filter.module.css +++ /dev/null @@ -1,108 +0,0 @@ -.searchArea { - inline-size: 200px; - block-size: 30px; - position: relative; - align-items: center; - display: inline-flex; - line-height: 1.1875em; -} - -.input { - font-size: 14px; - border: none; - inline-size: 100%; - min-inline-size: 0; - margin: 0; -} - -.input::placeholder { - opacity: 0.7; -} - -.input:focus { - outline: 0; -} - -.searchArea::before { - content: '\00a0'; - inset-inline-start: 0; - inset-inline-end: 0; - inset-block-end: 0; - position: absolute; - border-block-end: 1px solid var(--colors-grey400); - pointer-events: none; -} - -.searchArea.focused::after { - transform: scaleX(1); -} - -.searchArea::after { - content: ''; - inset-inline-start: 0; - inset-inline-end: 0; - inset-block-end: 0; - position: absolute; - border-block-end: 1px solid var(--colors-grey500); - transform: scaleX(0); - pointer-events: none; -} - -.searchButton { - border: none; - background-color: transparent; - padding-block-start: 0; - padding-block-end: 0; - padding-inline-start: 0; - padding-inline-end: 6px; -} - -.searchButton:hover { - cursor: pointer; -} - -.searchButton { - display: none; -} - -.searchIconContainer { - block-size: 0.01em; - max-block-size: 2em; - align-items: center; - margin-inline-end: 8px; - margin-block-end: 16px; -} - -.clearButtonContainer { - block-size: 0.01em; - max-block-size: 2em; - display: flex; - align-items: center; - margin-inline-start: 8px; -} - -@media only screen and (max-width: 480px) { - .input { - inline-size: 100%; - padding-block-end: 2px; - } - - /* collapsed controlbar */ - .collapsed .searchArea { - display: none; - } - - .collapsed .searchButton { - display: inline; - padding-block-start: 8px; - } - - /* expanded controlbar */ - - .expanded .searchArea { - display: flex; - block-size: 24px; - padding-block-start: inherit; - inline-size: 100%; - } -} diff --git a/src/components/DashboardsBar/styles/ShowMoreButton.module.css b/src/components/DashboardsBar/styles/ShowMoreButton.module.css deleted file mode 100644 index 92af33251..000000000 --- a/src/components/DashboardsBar/styles/ShowMoreButton.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.container { - text-align: center; - flex: none; - block-size: 21px; - position: absolute; - inset-block-end: 0; - inset-inline-start: 0; - inline-size: 100%; -} - -.showMore { - cursor: pointer; - border: none; - background-color: transparent; - padding: 0px; - inline-size: 100%; - block-size: 21px; -} - -.showMore:hover { - background: var(--colors-grey200); - transition: background 0.2s 0.1s; -} -.showMore:active { - background: var(--colors-grey300); - transition: none; -} - -.showMore:focus { - outline: none; -} - -.disabled { - cursor: not-allowed; -} diff --git a/src/components/DropdownButton/DropdownButton.js b/src/components/DropdownButton/DropdownButton.js index ea4162c37..0e4125570 100644 --- a/src/components/DropdownButton/DropdownButton.js +++ b/src/components/DropdownButton/DropdownButton.js @@ -17,7 +17,7 @@ const DropdownButton = ({ const ArrowIconComponent = open ? ArrowUp : ArrowDown return ( -
    +