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/config/testSetup.js b/config/testSetup.js new file mode 100644 index 000000000..979703723 --- /dev/null +++ b/config/testSetup.js @@ -0,0 +1,6 @@ +import { configure } from '@testing-library/dom' +import '@testing-library/jest-dom' + +configure({ + testIdAttribute: 'data-test', +}) 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/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/i18n/en.pot b/i18n/en.pot index 76eae083b..047e5934d 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,26 +5,111 @@ 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-18T12:26:05.212Z\n" +"PO-Revision-Date: 2024-12-18T12:26:05.212Z\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 "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 "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 "Show more dashboards" -msgstr "Show more dashboards" +msgid "No dashboards found" +msgstr "No dashboards found" msgid "Remove this item" msgstr "Remove this item" @@ -86,6 +171,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" @@ -248,9 +336,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 +395,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 +422,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 +489,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 +539,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,91 +563,21 @@ 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 "Add filter" -msgstr "Add filter" - -msgid "Offline data last updated {{time}}" -msgstr "Offline data last updated {{time}}" - -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 "Loading dashboard – {{name}}" 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 +586,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 8a4846ff1..776154f55 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", @@ -67,6 +68,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/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..2c3e69760 100644 --- a/src/pages/view/TitleBar/ActionsBar.js +++ b/src/components/DashboardsBar/InformationBlock/ActionsBar.js @@ -1,135 +1,96 @@ 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 { 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 { 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 ConfirmActionDialog from '../../ConfirmActionDialog.js' +import DropdownButton from '../../DropdownButton/DropdownButton.js' +import MenuItem from '../../MenuItemWithTooltip.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, + toggleDashboardStarred, + showAlert, updateShowDescription, removeAllFilters, restrictFilters, allowedFilters, filtersLength, }) => { - 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 { 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) @@ -166,7 +127,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 + } return ( <>
- -
+
{userAccess.update ? (
+ 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, 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 ( -
    +
    @@ -111,10 +111,11 @@ const ItemContextMenu = (props) => {
    {menuIsOpen && ( @@ -137,9 +138,7 @@ const ItemContextMenu = (props) => { (allowVisOpenInApp && !isSmallScreen(width)) || fullscreenAllowed) && ( - - - + )} )} diff --git a/src/components/Item/VisualizationItem/__tests__/Item.spec.js b/src/components/Item/VisualizationItem/__tests__/Item.spec.js index 04ae4e044..92e374c76 100644 --- a/src/components/Item/VisualizationItem/__tests__/Item.spec.js +++ b/src/components/Item/VisualizationItem/__tests__/Item.spec.js @@ -23,7 +23,6 @@ jest.mock( } ) -/* eslint-disable react/prop-types, react/no-unknown-property */ jest.mock( '../Visualization/Visualization', () => @@ -41,7 +40,6 @@ jest.mock( ) } ) -/* eslint-enable react/prop-types, react/no-unknown-property */ const mockStore = configureMockStore() diff --git a/src/components/MenuItemWithTooltip.js b/src/components/MenuItemWithTooltip.js index 57d641605..f861b1ff0 100644 --- a/src/components/MenuItemWithTooltip.js +++ b/src/components/MenuItemWithTooltip.js @@ -23,6 +23,7 @@ const MenuItemWithTooltip = ({ return ( ( -
    {text}
    +
    + + + + +

    {text}

    +
    ) NoContentMessage.propTypes = { diff --git a/src/components/Notice.js b/src/components/Notice.js index 08de09a51..99b53e6db 100644 --- a/src/components/Notice.js +++ b/src/components/Notice.js @@ -14,7 +14,7 @@ const Notice = ({ title, message }) => ( ) Notice.propTypes = { - message: PropTypes.string, + message: PropTypes.node, title: PropTypes.string, } diff --git a/src/components/__tests__/App.spec.js b/src/components/__tests__/App.spec.js index 6146cab80..fb480b964 100644 --- a/src/components/__tests__/App.spec.js +++ b/src/components/__tests__/App.spec.js @@ -52,7 +52,7 @@ jest.mock('../../api/dataStatistics.js', () => { }) jest.mock( - '../DashboardsBar/DashboardsBar', + '../DashboardsBar/index.js', () => function MockDashboardsBar() { return
    DashboardsBar
    diff --git a/src/components/__tests__/ConfirmActionDialog.spec.js b/src/components/__tests__/ConfirmActionDialog.spec.js index b8d197aba..ea3fad804 100644 --- a/src/components/__tests__/ConfirmActionDialog.spec.js +++ b/src/components/__tests__/ConfirmActionDialog.spec.js @@ -2,7 +2,6 @@ import { render } from '@testing-library/react' import React from 'react' import ConfirmActionDialog from '../ConfirmActionDialog.js' -/* eslint-disable react/prop-types */ jest.mock('@dhis2/ui', () => { const originalModule = jest.requireActual('@dhis2/ui') @@ -29,7 +28,6 @@ jest.mock('@dhis2/ui', () => { }, } }) -/* eslint-enable react/prop-types */ test('ConfirmActionDialog renders confirm delete dialog', () => { const { container } = render( diff --git a/src/components/styles/App.css b/src/components/styles/App.css index b55fbef81..cc3e22b4b 100644 --- a/src/components/styles/App.css +++ b/src/components/styles/App.css @@ -1,21 +1,4 @@ :root { - /* control bar variables */ - --user-rows-count: 1; - --controlbar-padding: 31px; - --row-height: 40px; - --min-rows-height: calc( - var(--controlbar-padding) + (1 * var(--row-height)) - ); - --max-rows-height: calc( - var(--controlbar-padding) + (10 * var(--row-height)) - ); - --user-rows-height: calc( - var(--controlbar-padding) + (var(--user-rows-count) * var(--row-height)) - ); - --sm-expanded-controlbar-height: calc( - (var(--vh, 1vh) * 100) - var(--headerbar-height) - 32px - ); - /* item variables */ --item-header-margin-top: 8px; --item-header-margin-bottom: 8px; @@ -32,7 +15,7 @@ body { margin: 0; padding: 0; font-family: sans-serif; - background-color: #f4f6f8; + background-color: var(--colors-grey200); } .app-shell-app { diff --git a/src/components/styles/DashboardContainer.module.css b/src/components/styles/DashboardContainer.module.css index 46b16a1ae..d57fd17fb 100644 --- a/src/components/styles/DashboardContainer.module.css +++ b/src/components/styles/DashboardContainer.module.css @@ -1,5 +1,5 @@ .container { - background-color: #f4f6f8; + background-color: var(--colors-grey200); padding-inline-start: var(--spacers-dp16); padding-inline-end: var(--spacers-dp16); padding-block-end: var(--spacers-dp24); diff --git a/src/components/styles/ItemGrid.css b/src/components/styles/ItemGrid.css index 5048f6bcb..be7aa3757 100644 --- a/src/components/styles/ItemGrid.css +++ b/src/components/styles/ItemGrid.css @@ -4,8 +4,8 @@ } .react-grid-item { overflow: hidden !important; - box-shadow: 0px 0px 3px 0px #999999; - background-color: #ffffff; + border: 1px solid var(--colors-grey300); + background-color: var(--colors-white); border-radius: 3px; min-block-size: 70px; min-inline-size: 70px; @@ -23,6 +23,7 @@ .react-grid-item.edit:hover { cursor: move; + border-color: #bfcad5; } .react-grid-item.edit.react-resizable-hide { @@ -34,7 +35,11 @@ } .react-grid-item.SPACER.edit { - background-color: rgba(255, 255, 255, 0.3); + background-color: transparent; + border: 1px dashed #bfcad5; +} +.react-grid-item.SPACER.edit:hover { + border-color: var(--colors-grey500); } .react-grid-item.react-grid-placeholder { diff --git a/src/components/styles/NoContentMessage.module.css b/src/components/styles/NoContentMessage.module.css index 408f830d7..0454fcaf7 100644 --- a/src/components/styles/NoContentMessage.module.css +++ b/src/components/styles/NoContentMessage.module.css @@ -4,7 +4,7 @@ padding-inline-start: var(--spacers-dp8); padding-inline-end: var(--spacers-dp8); text-align: center; - font-size: 15px; - font-weight: 500; + font-size: 14px; + font-weight: 400; color: var(--colors-grey600); } diff --git a/src/pages/edit/ItemSelector/CategorizedMenuGroup.js b/src/pages/edit/ItemSelector/CategorizedMenuGroup.js index cedd085f6..ff339424e 100644 --- a/src/pages/edit/ItemSelector/CategorizedMenuGroup.js +++ b/src/pages/edit/ItemSelector/CategorizedMenuGroup.js @@ -47,6 +47,10 @@ const CategorizedMenuGroup = ({ onChangeItemsLimit(type) } + const showFewerMoreLabel = seeMore + ? i18n.t('Show fewer') + : i18n.t('Show more') + return ( <> @@ -65,14 +69,13 @@ const CategorizedMenuGroup = ({ })} {hasMore ? ( - {seeMore - ? i18n.t('Show fewer') - : i18n.t('Show more')} + {showFewerMoreLabel} } /> diff --git a/src/pages/edit/ItemSelector/ContentMenuItem.js b/src/pages/edit/ItemSelector/ContentMenuItem.js index 92fd67f2b..d9e93bf06 100644 --- a/src/pages/edit/ItemSelector/ContentMenuItem.js +++ b/src/pages/edit/ItemSelector/ContentMenuItem.js @@ -2,22 +2,25 @@ import { visTypeIcons } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import { MenuItem, colors, IconLaunch16 } from '@dhis2/ui' import PropTypes from 'prop-types' -import React from 'react' +import React, { useCallback } from 'react' import { getItemIcon, VISUALIZATION } from '../../../modules/itemTypes.js' import classes from './styles/ContentMenuItem.module.css' -const LaunchLink = ({ url }) => ( - e.stopPropagation()} - className={classes.launchLink} - target="_blank" - rel="noopener noreferrer" - href={url} - aria-label={i18n.t('Open visualization in new tab')} - > - - -) +const LaunchLink = ({ url }) => { + const handleClick = useCallback(() => { + window.open(url, '_blank', 'noopener noreferrer') + }, [url]) + + return ( + + + + ) +} LaunchLink.propTypes = { url: PropTypes.string, @@ -40,6 +43,7 @@ const ContentMenuItem = ({ type, name, onInsert, url, visType }) => { return ( ( {
    setCols(0)} > @@ -124,6 +124,7 @@ export const LayoutModal = ({ columns, onSaveLayout, onClose }) => {
    diff --git a/src/pages/edit/__tests__/ActionsBar.spec.js b/src/pages/edit/__tests__/ActionsBar.spec.js index b3332f910..dafb860e7 100644 --- a/src/pages/edit/__tests__/ActionsBar.spec.js +++ b/src/pages/edit/__tests__/ActionsBar.spec.js @@ -9,7 +9,6 @@ const mockStore = configureMockStore() jest.mock('@dhis2/app-runtime') -/* eslint-disable react/prop-types */ jest.mock('@dhis2/ui', () => { const originalModule = jest.requireActual('@dhis2/ui') @@ -24,9 +23,7 @@ jest.mock('@dhis2/ui', () => { }, } }) -/* eslint-enable react/prop-types */ -/* eslint-disable react/prop-types */ jest.mock('@dhis2/analytics', () => { const originalModule = jest.requireActual('@dhis2/analytics') @@ -44,7 +41,6 @@ jest.mock('@dhis2/analytics', () => { }), } }) -/* eslint-enable react/prop-types */ jest.mock( '../FilterSettingsDialog', @@ -54,7 +50,6 @@ jest.mock( } ) -/* eslint-disable react/prop-types */ jest.mock( '../../../components/ConfirmActionDialog', () => @@ -62,7 +57,6 @@ jest.mock( return open ?
    : null } ) -/* eslint-enable react/prop-types */ jest.mock('@dhis2/app-runtime', () => ({ useDhis2ConnectionStatus: jest.fn(() => ({ diff --git a/src/pages/edit/__tests__/EditDashboard.spec.js b/src/pages/edit/__tests__/EditDashboard.spec.js index 9e07c41e3..ef8144480 100644 --- a/src/pages/edit/__tests__/EditDashboard.spec.js +++ b/src/pages/edit/__tests__/EditDashboard.spec.js @@ -10,7 +10,6 @@ import EditDashboard from '../EditDashboard.js' jest.mock('../../../api/fetchDashboard') -/* eslint-disable react/prop-types */ jest.mock('@dhis2/ui', () => { const originalModule = jest.requireActual('@dhis2/ui') @@ -23,8 +22,6 @@ jest.mock('@dhis2/ui', () => { } }) -/* eslint-enable react/prop-types */ - jest.mock( '../../../components/Notice.js', () => diff --git a/src/pages/edit/__tests__/FilterSettingsDialog.spec.js b/src/pages/edit/__tests__/FilterSettingsDialog.spec.js index bceed96fd..cf402be69 100644 --- a/src/pages/edit/__tests__/FilterSettingsDialog.spec.js +++ b/src/pages/edit/__tests__/FilterSettingsDialog.spec.js @@ -6,7 +6,6 @@ jest.mock('@dhis2/app-runtime', () => ({ useDhis2ConnectionStatus: () => ({ isConnected: true }), })) -/* eslint-disable react/prop-types, react/no-unknown-property */ jest.mock('@dhis2/ui', () => { const originalModule = jest.requireActual('@dhis2/ui') @@ -30,7 +29,6 @@ jest.mock('@dhis2/ui', () => { Button: function Mock({ children }) { return
    {children}
    }, - //eslint-disable-next-line no-unused-vars Radio: function Mock({ checked, dense, ...props }) { return (
    @@ -50,7 +48,6 @@ jest.mock('@dhis2/ui', () => { }, } }) -/* eslint-enable react/prop-types, react/no-unknown-property */ jest.mock('../../../modules/useDimensions', () => ({ __esModule: true, diff --git a/src/pages/edit/__tests__/NewDashboard.spec.js b/src/pages/edit/__tests__/NewDashboard.spec.js index 56f3b8f3c..ed7b3ceeb 100644 --- a/src/pages/edit/__tests__/NewDashboard.spec.js +++ b/src/pages/edit/__tests__/NewDashboard.spec.js @@ -5,7 +5,6 @@ import configureMockStore from 'redux-mock-store' import WindowDimensionsProvider from '../../../components/WindowDimensionsProvider.js' import NewDashboard from '../NewDashboard.js' -/* eslint-disable react/prop-types */ jest.mock('@dhis2/ui', () => { const originalModule = jest.requireActual('@dhis2/ui') @@ -26,7 +25,6 @@ jest.mock('@dhis2/ui', () => { }, } }) -/* eslint-enable react/prop-types */ jest.mock( '../ActionsBar', diff --git a/src/pages/edit/__tests__/TitleBar.spec.js b/src/pages/edit/__tests__/TitleBar.spec.js index 88d2d0bbd..64694a526 100644 --- a/src/pages/edit/__tests__/TitleBar.spec.js +++ b/src/pages/edit/__tests__/TitleBar.spec.js @@ -14,7 +14,6 @@ jest.mock( } ) -/* eslint-disable react/prop-types, no-unused-vars*/ jest.mock('@dhis2/ui', () => { const originalModule = jest.requireActual('@dhis2/ui') // InputField, TextAreaField, Radio, @@ -39,7 +38,6 @@ jest.mock('@dhis2/ui', () => { }, } }) -/* eslint-enable react/prop-types, no-unused-vars */ describe('TitleBar', () => { it('renders correctly with name and description', () => { diff --git a/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap b/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap index 8d9c3b7a6..de270243e 100644 --- a/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap +++ b/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap @@ -51,7 +51,21 @@ exports[`EditDashboard renders message when not enough access 1`] = `
    - No access + + + +

    + No access +

    @@ -142,7 +142,7 @@ exports[`TitleBar renders correctly with name and description 1`] = ` class="ui-TextAreaField" label="Dashboard description" name="Dashboard description input" - rows="6" + rows="5" value="A very colorful pony" />
    diff --git a/src/pages/edit/styles/ActionsBar.module.css b/src/pages/edit/styles/ActionsBar.module.css index 39a098b73..e5f546d3f 100644 --- a/src/pages/edit/styles/ActionsBar.module.css +++ b/src/pages/edit/styles/ActionsBar.module.css @@ -7,8 +7,7 @@ padding-inline-start: 0; padding-inline-end: 0; background-color: var(--colors-yellow050); - box-shadow: 0px 1px 2px rgba(33, 41, 52, 0.06), - 0px 1px 3px rgba(33, 41, 52, 0.1); + border-block-end: 1px solid var(--colors-grey400); position: relative; z-index: 10; flex: none; diff --git a/src/pages/edit/styles/LayoutModal.module.css b/src/pages/edit/styles/LayoutModal.module.css index 4284e3886..158538bea 100644 --- a/src/pages/edit/styles/LayoutModal.module.css +++ b/src/pages/edit/styles/LayoutModal.module.css @@ -4,18 +4,18 @@ cursor: pointer; padding: var(--spacers-dp16); margin-block-end: var(--spacers-dp8); - border: 1px solid var(--colors-grey300); - border-radius: 3px; + border: 1px solid var(--colors-grey400); + border-radius: 6px; } .activeOption { - border-color: var(--colors-teal400); - background: var(--colors-teal050); + border-color: var(--colors-teal500); + background: var(--colors-teal100); + box-shadow: var(--elevations-e100); } .radio { - margin-block-start: var(--spacers-dp16); - margin-inline-end: var(--spacers-dp8); + margin-block-start: var(--spacers-dp12); } .iconWrapper { @@ -32,21 +32,23 @@ .title { margin-block-start: 0; - margin-block-end: var(--spacers-dp8); + margin-block-end: var(--spacers-dp4); margin-inline-start: 0; margin-inline-end: 0; - font-size: 20px; + font-size: 15px; + font-weight: 500; } .description { margin: 0; - font-size: 14px; + font-size: 13px; + line-height: 18px; color: var(--colors-grey700); } .columnOptions { display: flex; - margin-block-start: var(--spacers-dp16); + margin-block-start: var(--spacers-dp8); } .gap { diff --git a/src/pages/edit/styles/TitleBar.module.css b/src/pages/edit/styles/TitleBar.module.css index 8a2157b71..34540bf67 100644 --- a/src/pages/edit/styles/TitleBar.module.css +++ b/src/pages/edit/styles/TitleBar.module.css @@ -1,7 +1,3 @@ -.container { - background: var(--colors-grey100); -} - .inputWrapper { display: flex; flex-wrap: wrap; @@ -21,7 +17,7 @@ .inputFieldWrapper { display: flex; flex-direction: column; - justify-content: space-between; + gap: 6px; } .inputFieldWrapper > :not(:first-child) { @@ -31,13 +27,13 @@ .searchContainer { display: flex; flex-wrap: wrap; - background: var(--colors-grey200); + background: var(--colors-grey050); border: 1px solid var(--colors-grey400); border-radius: 6px; - padding-block-start: var(--spacers-dp4); - padding-block-end: 0; + padding-block-start: var(--spacers-dp8); + padding-block-end: var(--spacers-dp4); padding-inline-start: var(--spacers-dp12); - padding-inline-end: var(--spacers-dp4); + padding-inline-end: var(--spacers-dp12); } .searchContainer > :not(:last-child) { diff --git a/src/pages/start/DashboardLink.js b/src/pages/start/DashboardLink.js index 5414fa985..29b094891 100644 --- a/src/pages/start/DashboardLink.js +++ b/src/pages/start/DashboardLink.js @@ -2,7 +2,7 @@ import { IconDashboardWindow16, colors } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import { Link } from 'react-router-dom' -import { OfflineSaved } from '../../components/DashboardsBar/assets/icons.js' +import { IconOfflineSaved } from '../../components/IconOfflineSaved.js' import { useCacheableSection } from '../../modules/useCacheableSection.js' import styles from './styles/DashboardLink.module.css' @@ -15,7 +15,7 @@ const DashboardLink = ({ id, name }) => { {name} - {isCached && } + {isCached && } ) } diff --git a/src/pages/start/LandingPage.js b/src/pages/start/LandingPage.js index 9a6d61463..4b238f41c 100644 --- a/src/pages/start/LandingPage.js +++ b/src/pages/start/LandingPage.js @@ -1,23 +1,16 @@ import PropTypes from 'prop-types' -import React, { useEffect, useState } from 'react' -import DashboardsBar from '../../components/DashboardsBar/DashboardsBar.js' +import React, { useEffect } from 'react' +import DashboardsBar from '../../components/DashboardsBar/index.js' import StartScreen from './StartScreen.js' const LandingPage = ({ username, onMount }) => { - const [controlbarExpanded, setControlbarExpanded] = useState(false) - useEffect(() => { onMount() }, []) return ( <> - - setControlbarExpanded(expanded) - } - /> + ) diff --git a/src/pages/start/styles/StartScreen.module.css b/src/pages/start/styles/StartScreen.module.css index 5b98fa744..6648968a2 100644 --- a/src/pages/start/styles/StartScreen.module.css +++ b/src/pages/start/styles/StartScreen.module.css @@ -53,22 +53,25 @@ margin-inline-end: auto; } .title { - font-weight: bold; + font-weight: 500; + font-size: 17px; margin-block-start: 0; line-height: 22px; - color: var(--colors-grey700); + color: var(--colors-grey800); } .section { text-align: start; - background: #fff; - padding: 20px; + background: var(--colors-white); + padding: var(--spacers-dp16); margin-block-end: var(--spacers-dp24); border-radius: 5px; + border: 1px solid var(--colors-grey300); } .guide { line-height: 18px; letter-spacing: 0.1px; list-style-position: outside; + list-style: circle; margin-block-start: 0; margin-block-end: 0; margin-inline-start: var(--spacers-dp12); diff --git a/src/pages/view/CacheableViewDashboard.js b/src/pages/view/CacheableViewDashboard.js index 27e43f322..8f951c68d 100644 --- a/src/pages/view/CacheableViewDashboard.js +++ b/src/pages/view/CacheableViewDashboard.js @@ -3,9 +3,10 @@ import { CacheableSection } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import isEmpty from 'lodash/isEmpty.js' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useEffect } from 'react' import { connect } from 'react-redux' -import DashboardsBar from '../../components/DashboardsBar/DashboardsBar.js' +import { acClearSelected } from '../../actions/selected.js' +import DashboardsBar from '../../components/DashboardsBar/index.js' import LoadingMask from '../../components/LoadingMask.js' import NoContentMessage from '../../components/NoContentMessage.js' import getCacheableSectionId from '../../modules/getCacheableSectionId.js' @@ -15,16 +16,24 @@ import { sGetDashboardById, sGetDashboardsSortedByStarred, } from '../../reducers/dashboards.js' +import { sGetSelectedId } from '../../reducers/selected.js' import ViewDashboard from './ViewDashboard.js' const CacheableViewDashboard = ({ - id, - dashboardsLoaded, + clearSelectedDashboard, dashboardsIsEmpty, + dashboardsLoaded, + id, + selectedId, }) => { - const [dashboardsBarExpanded, setDashboardsBarExpanded] = useState(false) const { currentUser } = useCachedDataQuery() + useEffect(() => { + if (id === null && selectedId !== null) { + clearSelectedDashboard() + } + }, [id, selectedId, clearSelectedDashboard]) + if (!dashboardsLoaded) { return } @@ -32,12 +41,7 @@ const CacheableViewDashboard = ({ if (dashboardsIsEmpty || id === null) { return ( <> - - setDashboardsBarExpanded(expanded) - } - /> + { @@ -88,7 +94,15 @@ const mapStateToProps = (state, ownProps) => { dashboardsIsEmpty: isEmpty(dashboards), dashboardsLoaded: !sDashboardsIsFetching(state), id: dashboardToSelect?.id || null, + selectedId: sGetSelectedId(state) || null, } } -export default connect(mapStateToProps, null)(CacheableViewDashboard) +const mapDispatchToProps = { + clearSelectedDashboard: acClearSelected, +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CacheableViewDashboard) diff --git a/src/pages/view/Description.js b/src/pages/view/Description.js new file mode 100644 index 000000000..8f97fb761 --- /dev/null +++ b/src/pages/view/Description.js @@ -0,0 +1,23 @@ +import i18n from '@dhis2/d2-i18n' +import cx from 'classnames' +import React from 'react' +import { useSelector } from 'react-redux' +import { sGetSelectedDisplayDescription } from '../../reducers/selected.js' +import { sGetShowDescription } from '../../reducers/showDescription.js' +import classes from './styles/Description.module.css' + +export const Description = () => { + const showDescription = useSelector(sGetShowDescription) + const description = useSelector(sGetSelectedDisplayDescription) + + return showDescription ? ( +

    + {description || i18n.t('No description')} +

    + ) : null +} diff --git a/src/pages/view/FilterBar/FilterBadge.js b/src/pages/view/FilterBar/FilterBadge.js index eec8bec47..1f617ff65 100644 --- a/src/pages/view/FilterBar/FilterBadge.js +++ b/src/pages/view/FilterBar/FilterBadge.js @@ -1,33 +1,32 @@ import { useDhis2ConnectionStatus } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { Tooltip } from '@dhis2/ui' +import { IconCross16, Tooltip } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' import { acSetActiveModalDimension } from '../../../actions/activeModalDimension.js' -import { useCacheableSection } from '../../../modules/useCacheableSection.js' +import { useWindowDimensions } from '../../../components/WindowDimensionsProvider.js' import { sGetSelectedId } from '../../../reducers/selected.js' import classes from './styles/FilterBadge.module.css' -const FilterBadge = ({ dashboardId, filter, openFilterModal, onRemove }) => { - const { isConnected: online } = useDhis2ConnectionStatus() - const { isCached } = useCacheableSection(dashboardId) +const getFilterValuesText = (values) => + values.length === 1 + ? values[0].name + : i18n.t('{{count}} selected', { + count: values.length, + defaultValue: '{{count}} selected', + defaultValue_plural: '{{count}} selected', + }) - const notAllowed = !isCached && !online +const EditFilterButton = ({ tooltipContent, filter, openFilterModal }) => { + const buttonText = `${filter.name}: ${getFilterValuesText(filter.values)}` - const filterText = `${filter.name}: ${ - filter.values.length > 1 - ? i18n.t('{{count}} selected', { - count: filter.values.length, - }) - : filter.values[0].name - }` - - return ( -
    - openFilterModal({ id: filter.id, @@ -35,40 +34,67 @@ const FilterBadge = ({ dashboardId, filter, openFilterModal, onRemove }) => { }) } > - {filterText} - - {filterText} - - {({ onMouseOver, onMouseOut, ref }) => ( - notAllowed && onMouseOver()} - onMouseOut={() => notAllowed && onMouseOut()} - ref={ref} - > - - - )} - + {buttonText} + + ) + } + + return ( + + {(props) => ( + + )} + + ) +} +EditFilterButton.propTypes = { + filter: PropTypes.object, + openFilterModal: PropTypes.func, + tooltipContent: PropTypes.string, +} + +const FilterBadge = ({ filter, openFilterModal, onRemove }) => { + const { isConnected } = useDhis2ConnectionStatus() + const { width } = useWindowDimensions() + const isEditDisabled = !isConnected || width <= 480 + const tooltipContent = !isConnected + ? i18n.t('Cannot edit filters while offline') + : i18n.t('Cannot edit filters on a small screen') + + return ( +
    + + {isConnected && ( + + )}
    ) } FilterBadge.propTypes = { - dashboardId: PropTypes.string.isRequired, filter: PropTypes.object.isRequired, openFilterModal: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired, diff --git a/src/pages/view/FilterBar/__tests__/FilterBadge.spec.js b/src/pages/view/FilterBar/__tests__/FilterBadge.spec.js index 1164dca2d..b183c4643 100644 --- a/src/pages/view/FilterBar/__tests__/FilterBadge.spec.js +++ b/src/pages/view/FilterBar/__tests__/FilterBadge.spec.js @@ -1,38 +1,72 @@ +import { useDhis2ConnectionStatus } from '@dhis2/app-runtime' import { render } from '@testing-library/react' import React from 'react' import { Provider } from 'react-redux' -import configureMockStore from 'redux-mock-store' +import { createStore } from 'redux' +import { useWindowDimensions } from '../../../../components/WindowDimensionsProvider.js' import FilterBadge from '../FilterBadge.js' -const mockStore = configureMockStore() - -const store = { selected: { id: 'dashboard1' } } - -jest.mock('@dhis2/analytics', () => ({ - useCachedDataQuery: () => ({ - currentUser: { - username: 'rainbowDash', - id: 'r3nb0d5h', - }, - }), -})) - jest.mock('@dhis2/app-runtime', () => ({ - useDhis2ConnectionStatus: () => ({ isConnected: true }), - useCacheableSection: jest.fn(() => ({ - isCached: false, - recordingState: 'default', - })), + useDhis2ConnectionStatus: jest.fn(() => ({ isConnected: true })), })) +jest.mock('../../../../components/WindowDimensionsProvider.js', () => ({ + useWindowDimensions: jest.fn(() => ({ width: 1920, height: 1080 })), +})) + +const baseState = { selected: { id: 'dashboard1' } } +const createMockStore = (state) => + createStore(() => Object.assign({}, baseState, state)) -test('Filter Badge displays badge containing number of items in filter', () => { +test('Displays badge containing number of filter items when filtered on multiple', () => { const filter = { id: 'ponies', name: 'Ponies', values: [{ name: 'Rainbow Dash' }, { name: 'Twilight Sparkle' }], } - const { container } = render( - + const { getByTestId } = render( + + + + ) + expect(getByTestId('filter-badge-button')).toHaveTextContent( + 'Ponies: 2 selected' + ) +}) + +test('Displays badge with filter item name when only one filter item is present', () => { + const filter = { + id: 'ponies', + name: 'Ponies', + values: [{ name: 'Twilight Sparkle' }], + } + const { getByTestId } = render( + + + + ) + expect(getByTestId('filter-badge-button')).toHaveTextContent( + 'Ponies: Twilight Sparkle' + ) +}) + +test('Has enabled buttons when online', () => { + const filter = { + id: 'ponies', + name: 'Ponies', + values: [{ name: 'Twilight Sparkle' }], + } + const { getByTestId } = render( + { /> ) - expect(container).toMatchSnapshot() + expect(getByTestId('filter-badge-button')).toBeEnabled() + expect(getByTestId('filter-badge-clear-button')).toBeEnabled() }) -test('FilterBadge displays badge with filter item name when only one filter item', () => { +test('Shows only a disabled edit-filter button when offline', () => { + useDhis2ConnectionStatus.mockImplementationOnce(() => ({ + isConnected: false, + })) const filter = { id: 'ponies', name: 'Ponies', values: [{ name: 'Twilight Sparkle' }], } + const { getByTestId, queryByTestId } = render( + + + + ) + expect(getByTestId('filter-badge-button')).toBeDisabled() + expect(queryByTestId('filter-badge-clear-button')).not.toBeInTheDocument() +}) - const { container } = render( - +test('Shows a disabled edit-filter button and enabled clear-filter when on small screen', () => { + useWindowDimensions.mockImplementationOnce(() => ({ + width: 440, + height: 780, + })) + const filter = { + id: 'ponies', + name: 'Ponies', + values: [{ name: 'Twilight Sparkle' }], + } + const { getByTestId } = render( + ) - expect(container).toMatchSnapshot() + expect(getByTestId('filter-badge-button')).toBeDisabled() + expect(getByTestId('filter-badge-clear-button')).toBeEnabled() }) diff --git a/src/pages/view/FilterBar/__tests__/__snapshots__/FilterBadge.spec.js.snap b/src/pages/view/FilterBar/__tests__/__snapshots__/FilterBadge.spec.js.snap deleted file mode 100644 index bdd73e689..000000000 --- a/src/pages/view/FilterBar/__tests__/__snapshots__/FilterBadge.spec.js.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Filter Badge displays badge containing number of items in filter 1`] = ` -
    -
    - - Ponies: 2 selected - - - Ponies: 2 selected - - - - -
    -
    -`; - -exports[`FilterBadge displays badge with filter item name when only one filter item 1`] = ` -
    -
    - - Ponies: Twilight Sparkle - - - Ponies: Twilight Sparkle - - - - -
    -
    -`; diff --git a/src/pages/view/FilterBar/styles/FilterBadge.module.css b/src/pages/view/FilterBar/styles/FilterBadge.module.css index c60d3d292..3b853fc7b 100644 --- a/src/pages/view/FilterBar/styles/FilterBadge.module.css +++ b/src/pages/view/FilterBar/styles/FilterBadge.module.css @@ -1,59 +1,41 @@ .container { - margin: 2px; - padding-block-start: 0; - padding-block-end: 0; - padding-inline-start: var(--spacers-dp16); - padding-inline-end: var(--spacers-dp16); - border-radius: 4px; - color: var(--colors-white); - background-color: #212934; - block-size: 36px; display: flex; - align-items: center; } -.badge { +.button { + all: unset; + display: inline-flex; + align-items: center; + justify-content: center; + block-size: 24px; + color: var(--colors-white); font-size: 13px; + background-color: var(--colors-grey900); cursor: pointer; - white-space: nowrap; -} - -.badgeSmall { - display: none; - font-size: 13px; - white-space: nowrap; } -.removeButton { - background-color: transparent; - color: var(--colors-white); - border: none; - padding: 0; - font-size: 12px; - text-decoration: underline; - margin-inline-start: var(--spacers-dp24); - cursor: pointer; +.button:disabled { + cursor: not-allowed; } -.span { - display: inline-flex; - pointer-events: all; +.button:hover:not(:disabled) { + background-color: var(--colors-grey800); } -.span > :global(button:disabled) { - pointer-events: none; +.filterButton { + padding-inline: 6px 4px; + border-start-start-radius: 4px; + border-end-start-radius: 4px; } -.notAllowed { - cursor: not-allowed; +.filterButton:only-child { + padding-inline-end: 6px; + border-start-end-radius: 4px; + border-end-end-radius: 4px; } -@media only screen and (max-width: 480px) { - .badge { - display: none; - } - - .badgeSmall { - display: block; - } +.removeButton { + padding-inline: 4px 6px; + border-start-end-radius: 4px; + border-end-end-radius: 4px; } diff --git a/src/pages/view/FilterBar/styles/FilterBar.module.css b/src/pages/view/FilterBar/styles/FilterBar.module.css index 39d4c1092..f6cfc89cf 100644 --- a/src/pages/view/FilterBar/styles/FilterBar.module.css +++ b/src/pages/view/FilterBar/styles/FilterBar.module.css @@ -1,9 +1,10 @@ .bar { position: sticky; - inset-block-start: var(--spacers-dp12); + inset-block-start: var(--spacers-dp8); z-index: 7; margin-block-start: var(--spacers-dp8); display: flex; justify-content: center; flex-wrap: wrap; + gap: var(--spacers-dp8); } diff --git a/src/pages/view/TitleBar/Description.js b/src/pages/view/TitleBar/Description.js deleted file mode 100644 index 895fc5a55..000000000 --- a/src/pages/view/TitleBar/Description.js +++ /dev/null @@ -1,23 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React from 'react' -import classes from './styles/Description.module.css' - -const Description = ({ description }) => ( -
    - {description || i18n.t('No description')} -
    -) - -Description.propTypes = { - description: PropTypes.string, -} - -export default Description diff --git a/src/pages/view/TitleBar/LastUpdatedTag.js b/src/pages/view/TitleBar/LastUpdatedTag.js deleted file mode 100644 index da47c9871..000000000 --- a/src/pages/view/TitleBar/LastUpdatedTag.js +++ /dev/null @@ -1,30 +0,0 @@ -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 { useCacheableSection } from '../../../modules/useCacheableSection.js' -import classes from './styles/LastUpdatedTag.module.css' - -const LastUpdatedTag = ({ id }) => { - const { lastUpdated } = useCacheableSection(id) - - return lastUpdated?.toString ? ( - {moment(lastUpdated).format('llll')}} - openDelay={200} - closeDelay={100} - > - - {i18n.t('Offline data last updated {{time}}', { - time: moment(lastUpdated).fromNow(), - })} - - - ) : null -} -LastUpdatedTag.propTypes = { - id: PropTypes.string, -} - -export default LastUpdatedTag diff --git a/src/pages/view/TitleBar/TitleBar.js b/src/pages/view/TitleBar/TitleBar.js deleted file mode 100644 index 04d90b7cf..000000000 --- a/src/pages/view/TitleBar/TitleBar.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { connect } from 'react-redux' -import { sGetSelected } from '../../../reducers/selected.js' -import { sGetShowDescription } from '../../../reducers/showDescription.js' -import ActionsBar from './ActionsBar.js' -import Description from './Description.js' -import LastUpdatedTag from './LastUpdatedTag.js' -import classes from './styles/TitleBar.module.css' - -const ViewTitleBar = ({ - id, - displayName, - displayDescription, - showDescription, -}) => { - return ( -
    -
    - - {displayName} - - -
    - {showDescription && ( - - )} - {} -
    - ) -} - -ViewTitleBar.propTypes = { - displayDescription: PropTypes.string, - displayName: PropTypes.string, - id: PropTypes.string, - showDescription: PropTypes.bool, -} - -const mapStateToProps = (state) => { - const dashboard = sGetSelected(state) - - return { - ...dashboard, - showDescription: sGetShowDescription(state), - } -} - -export default connect(mapStateToProps)(ViewTitleBar) diff --git a/src/pages/view/TitleBar/__tests__/__snapshots__/FilterSelector.spec.js.snap b/src/pages/view/TitleBar/__tests__/__snapshots__/FilterSelector.spec.js.snap deleted file mode 100644 index 9d53db4ad..000000000 --- a/src/pages/view/TitleBar/__tests__/__snapshots__/FilterSelector.spec.js.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`is disabled when offline 1`] = ` -
    - - - -
    -`; - -exports[`is enabled when online 1`] = ` -
    - - - -
    -`; diff --git a/src/pages/view/TitleBar/styles/ActionsBar.module.css b/src/pages/view/TitleBar/styles/ActionsBar.module.css deleted file mode 100644 index 405ff0e44..000000000 --- a/src/pages/view/TitleBar/styles/ActionsBar.module.css +++ /dev/null @@ -1,37 +0,0 @@ -.actions { - display: flex; - align-items: center; - margin-inline-start: 8px; -} - -.strip { - display: flex; - margin-inline-start: var(--spacers-dp8); -} - -.strip > * { - margin-inline-start: var(--spacers-dp8); -} - -.strip .moreButtonSmall { - display: none; -} - -@media only screen and (max-width: 480px) { - .strip .editButton, - .strip .shareButton { - display: none; - } - - .strip .moreButton { - display: none; - } - - .strip .moreButtonSmall { - display: inline-flex; - } -} - -.link { - text-decoration: none; -} diff --git a/src/pages/view/TitleBar/styles/Description.module.css b/src/pages/view/TitleBar/styles/Description.module.css deleted file mode 100644 index 0d7071092..000000000 --- a/src/pages/view/TitleBar/styles/Description.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.desc { - color: var(--colors-grey700); -} - -.noDesc { - color: #888; -} - -.descContainer { - font-size: 14px; - padding-block-start: 5px; - padding-block-end: 5px; -} diff --git a/src/pages/view/TitleBar/styles/FilterSelector.module.css b/src/pages/view/TitleBar/styles/FilterSelector.module.css deleted file mode 100644 index 7300c0839..000000000 --- a/src/pages/view/TitleBar/styles/FilterSelector.module.css +++ /dev/null @@ -1,5 +0,0 @@ -@media only screen and (max-width: 480px) { - .buttonContainer { - display: none; - } -} diff --git a/src/pages/view/TitleBar/styles/LastUpdatedTag.module.css b/src/pages/view/TitleBar/styles/LastUpdatedTag.module.css deleted file mode 100644 index ea750693f..000000000 --- a/src/pages/view/TitleBar/styles/LastUpdatedTag.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.lastUpdatedTag { - margin-block-start: var(--spacers-dp8); -} diff --git a/src/pages/view/TitleBar/styles/StarDashboardButton.module.css b/src/pages/view/TitleBar/styles/StarDashboardButton.module.css deleted file mode 100644 index 4fa91f643..000000000 --- a/src/pages/view/TitleBar/styles/StarDashboardButton.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.star { - margin-block-start: 0; - margin-block-end: 0; - margin-inline-start: var(--spacers-dp4); - margin-inline-end: 0; - padding: 0; - border: none; - position: relative; - inset-block-start: 1px; - background: transparent; - outline: none; - cursor: pointer; -} -.star:disabled { - cursor: not-allowed; -} diff --git a/src/pages/view/TitleBar/styles/TitleBar.module.css b/src/pages/view/TitleBar/styles/TitleBar.module.css deleted file mode 100644 index f2800f974..000000000 --- a/src/pages/view/TitleBar/styles/TitleBar.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.container { - margin-block-start: var(--spacers-dp12); -} - -.titleBar { - display: flex; - align-items: flex-start; - position: relative; -} - -.title { - position: relative; - font-size: 21px; - font-weight: 500; - color: var(--colors-black); - min-inline-size: 50px; - cursor: default; - user-select: text; - inset-block-start: 7px; -} - -@media only screen and (max-width: 480px) { - .title { - inset-block-start: 3px; - } -} diff --git a/src/pages/view/ViewDashboard.js b/src/pages/view/ViewDashboard.js index 62baa8438..23aece7c8 100644 --- a/src/pages/view/ViewDashboard.js +++ b/src/pages/view/ViewDashboard.js @@ -1,11 +1,13 @@ -import { useDhis2ConnectionStatus, useDataEngine } from '@dhis2/app-runtime' +import { + useAlert, + useDhis2ConnectionStatus, + useDataEngine, +} from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { AlertStack, AlertBar } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback, useRef } from 'react' import { connect } from 'react-redux' -import { Link } from 'react-router-dom' import { acClearEditDashboard } from '../../actions/editDashboard.js' import { acSetPassiveViewRegistered } from '../../actions/passiveViewRegistered.js' import { acClearPrintDashboard } from '../../actions/printDashboard.js' @@ -15,191 +17,138 @@ import { } from '../../actions/selected.js' import { apiPostDataStatistics } from '../../api/dataStatistics.js' import DashboardContainer from '../../components/DashboardContainer.js' -import DashboardsBar from '../../components/DashboardsBar/DashboardsBar.js' -import LoadingMask from '../../components/LoadingMask.js' -import Notice from '../../components/Notice.js' +import DashboardsBar from '../../components/DashboardsBar/index.js' import { setHeaderbarVisible } from '../../modules/setHeaderbarVisible.js' import { useCacheableSection } from '../../modules/useCacheableSection.js' import { sGetDashboardById } from '../../reducers/dashboards.js' import { sGetPassiveViewRegistered } from '../../reducers/passiveViewRegistered.js' import { sGetSelectedId } from '../../reducers/selected.js' -import { ROUTE_START_PATH } from '../start/index.js' -import FilterBar from './FilterBar/FilterBar.js' -import ItemGrid from './ItemGrid.js' import classes from './styles/ViewDashboard.module.css' -import TitleBar from './TitleBar/TitleBar.js' - -const ViewDashboard = (props) => { - const [controlbarExpanded, setControlbarExpanded] = useState(false) - const [loadingMessage, setLoadingMessage] = useState(null) +import { ViewDashboardContent } from './ViewDashboardContent.js' + +const ViewDashboard = ({ + clearEditDashboard, + clearPrintDashboard, + fetchDashboard, + passiveViewRegistered, + registerPassiveView, + requestedDashboardName, + requestedId, + setSelectedAsOffline, + username, +}) => { + const alertTimeoutRef = useRef(null) + const [loading, setLoading] = useState(false) const [loaded, setLoaded] = useState(false) const [loadFailed, setLoadFailed] = useState(false) const { isConnected: online } = useDhis2ConnectionStatus() - const { isCached } = useCacheableSection(props.requestedId) + const { isCached } = useCacheableSection(requestedId) const engine = useDataEngine() + const { show: showAlert, hide: hideAlert } = useAlert( + ({ message }) => message + ) + const loadDashboard = useCallback(async () => { + setLoading(true) + + alertTimeoutRef.current = setTimeout(() => { + const message = requestedDashboardName + ? i18n.t('Loading dashboard – {{name}}', { + name: requestedDashboardName, + }) + : i18n.t('Loading dashboard') + showAlert({ message }) + }, 500) + + try { + await fetchDashboard(requestedId, username) + setLoaded(true) + } catch (e) { + setLoadFailed(true) + setSelectedAsOffline(requestedId, username) + } finally { + setLoading(false) + clearTimeout(alertTimeoutRef.current) + } + }, [ + fetchDashboard, + requestedDashboardName, + requestedId, + setSelectedAsOffline, + showAlert, + username, + ]) useEffect(() => { - setHeaderbarVisible(true) - props.clearEditDashboard() - props.clearPrintDashboard() - }, []) - - useEffect(() => { - setLoaded(false) - - Array.from( - document.getElementsByClassName('dashboard-scroll-container') - ).forEach((container) => { - container.scroll(0, 0) - }) - }, [props.requestedId]) + if (!loading && !loaded && !loadFailed) { + setHeaderbarVisible(true) + clearEditDashboard() + clearPrintDashboard() + if (online || isCached) { + loadDashboard() + } else { + setSelectedAsOffline(requestedId, username) + } + } + }, [ + clearEditDashboard, + clearPrintDashboard, + isCached, + loadDashboard, + loaded, + loadFailed, + loading, + online, + requestedId, + setSelectedAsOffline, + username, + ]) useEffect(() => { - if (!props.passiveViewRegistered && online) { - apiPostDataStatistics( - 'PASSIVE_DASHBOARD_VIEW', - props.requestedId, - engine - ) + if (!passiveViewRegistered && online) { + apiPostDataStatistics('PASSIVE_DASHBOARD_VIEW', requestedId, engine) .then(() => { - props.registerPassiveView() + registerPassiveView() }) .catch((error) => console.info(error)) } - }, [props.passiveViewRegistered, engine]) - - useEffect(() => { - const loadDashboard = async () => { - const alertTimeout = setTimeout(() => { - if (props.requestedDashboardName) { - setLoadingMessage( - i18n.t('Loading dashboard – {{name}}', { - name: props.requestedDashboardName, - }) - ) - } else { - setLoadingMessage(i18n.t('Loading dashboard')) - } - }, 500) - - try { - await props.fetchDashboard(props.requestedId, props.username) - setLoaded(true) - setLoadFailed(false) - setLoadingMessage(null) - clearTimeout(alertTimeout) - } catch (e) { - setLoaded(false) - setLoadFailed(true) - setLoadingMessage(null) - clearTimeout(alertTimeout) - props.setSelectedAsOffline(props.requestedId, props.username) - } - } - - const requestedIsAvailable = online || isCached - const switchingDashboard = props.requestedId !== props.currentId - - if (requestedIsAvailable && !loaded) { - loadDashboard() - } else if (!requestedIsAvailable && switchingDashboard) { - setLoaded(false) - props.setSelectedAsOffline(props.requestedId, props.username) - } - }, [props.requestedId, props.currentId, loaded, online]) - - const onExpandedChanged = (expanded) => setControlbarExpanded(expanded) - - const getContent = () => { - if ( - !online && - !isCached && - (props.requestedId !== props.currentId || !loaded) - ) { - return ( - -

    - {i18n.t( - 'This dashboard cannot be loaded while offline.' - )} -

    -
    - - {i18n.t('Go to start page')} - -
    - - } - /> - ) - } - - if (loadFailed) { - return ( - - ) - } - - return props.requestedId !== props.currentId ? ( - - ) : ( - <> - - - - - ) - } + }, [ + engine, + online, + passiveViewRegistered, + registerPassiveView, + requestedId, + ]) + + /* Cleanup effect: Hide current alert and prevent pending alert + * from showing after the component unmounts due to navigation */ + useEffect( + () => () => { + hideAlert() + clearTimeout(alertTimeoutRef.current) + }, + [hideAlert] + ) return ( - <> -
    - + + + - - {controlbarExpanded && ( -
    setControlbarExpanded(false)} - /> - )} - {getContent()} - -
    - - {loadingMessage && ( - setLoadingMessage(null)} - permanent - > - {loadingMessage} - - )} - - +
    +
    ) } ViewDashboard.propTypes = { clearEditDashboard: PropTypes.func, clearPrintDashboard: PropTypes.func, - currentId: PropTypes.string, fetchDashboard: PropTypes.func, passiveViewRegistered: PropTypes.bool, registerPassiveView: PropTypes.func, diff --git a/src/pages/view/ViewDashboardContent.js b/src/pages/view/ViewDashboardContent.js new file mode 100644 index 000000000..83c03fc02 --- /dev/null +++ b/src/pages/view/ViewDashboardContent.js @@ -0,0 +1,64 @@ +import i18n from '@dhis2/d2-i18n' +import PropTypes from 'prop-types' +import React from 'react' +import { Link } from 'react-router-dom' +import LoadingMask from '../../components/LoadingMask.js' +import Notice from '../../components/Notice.js' +import { ROUTE_START_PATH } from '../start/index.js' +import { Description } from './Description.js' +import FilterBar from './FilterBar/FilterBar.js' +import ItemGrid from './ItemGrid.js' +import classes from './styles/ViewDashboard.module.css' + +export const ViewDashboardContent = ({ loading, loaded, loadFailed }) => { + if (loading) { + return + } + + if (loadFailed) { + return ( + + ) + } + + if (loaded) { + return ( + <> + + + + + ) + } + + return ( + +

    + {i18n.t( + 'This dashboard cannot be loaded while offline.' + )} +

    +
    + + {i18n.t('Go to start page')} + +
    + + } + /> + ) +} + +ViewDashboardContent.propTypes = { + loadFailed: PropTypes.bool, + loaded: PropTypes.bool, + loading: PropTypes.bool, +} diff --git a/src/pages/view/__tests__/ViewDashboard.spec.js b/src/pages/view/__tests__/ViewDashboard.spec.js index 0c417e7d3..149a31f63 100644 --- a/src/pages/view/__tests__/ViewDashboard.spec.js +++ b/src/pages/view/__tests__/ViewDashboard.spec.js @@ -1,6 +1,8 @@ import { render, act } 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 thunk from 'redux-thunk' import { apiPostDataStatistics } from '../../../api/dataStatistics.js' @@ -24,26 +26,22 @@ jest.mock('@dhis2/app-runtime', () => ({ recordingState: 'default', })), useDataEngine: jest.fn(), + useAlert: jest.fn(() => ({ + show: () => {}, + hide: () => {}, + })), })) jest.mock('../../../api/fetchDashboard') jest.mock( - '../../../components/DashboardsBar/DashboardsBar', + '../../../components/DashboardsBar/index.js', () => function MockDashboardsBar() { return
    DashboardsBar
    } ) -jest.mock( - '../TitleBar/TitleBar', - () => - function MockTitleBar() { - return
    TitleBar
    - } -) - jest.mock( '../FilterBar/FilterBar', () => @@ -98,7 +96,9 @@ test('ViewDashboard renders dashboard', async () => { <>
    - + + + ) @@ -120,7 +120,9 @@ test('ViewDashboard does not post passive view to api if passive view has been r <>
    - + + + ) @@ -142,7 +144,9 @@ test('ViewDashboard posts passive view to api if passive view has not been regis <>
    - + + + ) diff --git a/src/pages/view/__tests__/__snapshots__/ViewDashboard.spec.js.snap b/src/pages/view/__tests__/__snapshots__/ViewDashboard.spec.js.snap index 0f1515cf9..f9540fb80 100644 --- a/src/pages/view/__tests__/__snapshots__/ViewDashboard.spec.js.snap +++ b/src/pages/view/__tests__/__snapshots__/ViewDashboard.spec.js.snap @@ -14,9 +14,6 @@ exports[`ViewDashboard renders dashboard 1`] = ` class="container dashboard-scroll-container" data-test="inner-scroll-container" > -
    - TitleBar -
    MockFilterBar
    diff --git a/src/pages/view/index.js b/src/pages/view/index.js index e597dc30b..207ea07a8 100644 --- a/src/pages/view/index.js +++ b/src/pages/view/index.js @@ -1,4 +1,3 @@ -import DashboardsBar from '../../components/DashboardsBar/DashboardsBar.js' import ViewDashboard from './CacheableViewDashboard.js' -export { ViewDashboard, DashboardsBar } +export { ViewDashboard } diff --git a/src/pages/view/styles/Description.module.css b/src/pages/view/styles/Description.module.css new file mode 100644 index 000000000..c37420b61 --- /dev/null +++ b/src/pages/view/styles/Description.module.css @@ -0,0 +1,11 @@ +.description { + font-size: 14px; + color: var(--colors-grey700); + margin-block-start: var(--spacers-dp8); + margin-block-end: 0; +} + +.empty { + color: var(--colors-grey600); + font-style: italic; +} diff --git a/src/pages/view/styles/ItemGrid.module.css b/src/pages/view/styles/ItemGrid.module.css index 0a534e8e0..58d2e742f 100644 --- a/src/pages/view/styles/ItemGrid.module.css +++ b/src/pages/view/styles/ItemGrid.module.css @@ -1,3 +1,3 @@ .grid { - margin-block-start: var(--spacers-dp16); + margin-block-start: var(--spacers-dp8); } diff --git a/src/reducers/__tests__/controlBar.spec.js b/src/reducers/__tests__/controlBar.spec.js deleted file mode 100644 index f94fc63d3..000000000 --- a/src/reducers/__tests__/controlBar.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import reducer, { - SET_CONTROLBAR_USER_ROWS, - DEFAULT_STATE_CONTROLBAR_ROWS, -} from '../controlBar.js' - -describe('controlbar reducer', () => { - it('should return the default state', () => { - const actualState = reducer(undefined, {}) - - expect(actualState).toEqual({ - userRows: DEFAULT_STATE_CONTROLBAR_ROWS, - }) - }) - - it('should handle SET_CONTROLBAR_USER_ROWS', () => { - const rows = 4 - const action = { - type: SET_CONTROLBAR_USER_ROWS, - value: rows, - } - - const expectedState = { userRows: rows } - - const actualState = reducer(undefined, action) - - expect(actualState).toEqual(expectedState) - }) -}) diff --git a/src/reducers/controlBar.js b/src/reducers/controlBar.js deleted file mode 100644 index def8b257b..000000000 --- a/src/reducers/controlBar.js +++ /dev/null @@ -1,33 +0,0 @@ -/** @module reducers/controlBar */ -import { combineReducers } from 'redux' -import { validateReducer } from '../modules/util.js' - -export const SET_CONTROLBAR_USER_ROWS = 'SET_CONTROLBAR_USER_ROWS' - -export const DEFAULT_STATE_CONTROLBAR_ROWS = 1 - -const userRows = (state = DEFAULT_STATE_CONTROLBAR_ROWS, action) => { - switch (action.type) { - case SET_CONTROLBAR_USER_ROWS: - return validateReducer(action.value, DEFAULT_STATE_CONTROLBAR_ROWS) - default: - return state - } -} - -export default combineReducers({ - userRows, -}) - -/** - * Selectors that point to specific props in the state object - * @function - * @param {Object} state - * @returns {Object} - */ -const sGetControlBarRoot = (state) => state.controlBar - -// Selector dependency level 2 - -export const sGetControlBarUserRows = (state) => - sGetControlBarRoot(state).userRows diff --git a/src/reducers/index.js b/src/reducers/index.js index 9f839cbb2..a2d46cd07 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,6 +1,5 @@ import { combineReducers } from 'redux' import activeModalDimension from './activeModalDimension.js' -import controlBar from './controlBar.js' import dashboards from './dashboards.js' import dashboardsFilter from './dashboardsFilter.js' import dimensions from './dimensions.js' @@ -19,7 +18,6 @@ export default combineReducers({ dashboards, selected, dashboardsFilter, - controlBar, visualizations, messages, editDashboard, diff --git a/yarn.lock b/yarn.lock index 1a5a667ff..d046039e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@adobe/css-tools@^4.3.0": - version "4.3.2" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11" - integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw== +"@adobe/css-tools@^4.4.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.1.tgz#2447a230bfe072c1659e6815129c03cf170710e3" + integrity sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ== "@ampproject/remapping@^2.1.0": version "2.2.0" @@ -1721,593 +1721,592 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@dhis2-ui/alert@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/alert/-/alert-9.11.3.tgz#b97433f4ebe43171e31709bd18979aa7c49f6065" - integrity sha512-TmY5x0+lQAcZ8boJUcQGXnTrXvHKw2A8tgve19H8AeCBSHNjMgrefmFDB+CboYWb48aM72RogviTAOlb/c+bbw== +"@dhis2-ui/alert@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/alert/-/alert-10.1.4.tgz#a5eb57fc5f6e6dfcbd0c09ba959167be8bea1904" + integrity sha512-9vy3+8WHeR7GuOyVM34+6iK8FIkBgji9mm1bq6TWnNozFe28JWUpAO1yuYKlEQ38qN2foO2J0x2ON0JaTFe+AA== dependencies: - "@dhis2-ui/portal" "9.11.3" + "@dhis2-ui/portal" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/box@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/box/-/box-9.11.3.tgz#4f378c10d0e80b8aa2e087a7e82958dd0bdc35d1" - integrity sha512-eSjLDaZW1DeFBHX9iKS+CMGa2HP8sY3cVE4e1i0D80/zkVjnFD67qO+3qhgYxgJFq2Hu195BAaWKdvNUBYFv6w== +"@dhis2-ui/box@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/box/-/box-10.1.4.tgz#7d6da3b2801470108c8e90da83fddf790f0799d8" + integrity sha512-irGpjbKOyqSfWkem+jSkMD0yy4wwnZbb/JHpS9KLmRkXet22V/HiqYFVyVsvKbKV6/LgY7GQBsxEtTa0RIK+fA== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/button@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/button/-/button-9.11.3.tgz#250bf4d71916d15d149aa552a155bbfcf53922e1" - integrity sha512-F9wM97vSun+oomZ2PNx56sN9VUbRsm4fJ5b5LmIaRLZqEbISLL6w4+By/Hxv/GAtbOIjqqBH9Z/82gSUIEY+1Q== +"@dhis2-ui/button@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/button/-/button-10.1.4.tgz#02f1c596c81ec8f1ce5db2ecf17f92cc5cbc56d2" + integrity sha512-twodN2cFrs5QwkwFSQ1CC6T8FYRBYSp3EZ1IBKxvCoo2sVx1wiwujLLm7/u31o8evjf0eqDB3PSxrQsWddnGQQ== dependencies: - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/popper" "9.11.3" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/popper" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/calendar@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/calendar/-/calendar-9.11.3.tgz#a1977f8ec4b43d4153fd6cbaba94e05ec34b43a4" - integrity sha512-WeuwPuritpzKWwhbU2x126mi6dwT83u6bM/Nxye7Y7NMoSbvTpR6fFbWGh8t0OoYe+Hf08jzjqvNu9tiuoRjiA== - dependencies: - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/popper" "9.11.3" - "@dhis2/multi-calendar-dates" "^1.2.3" +"@dhis2-ui/calendar@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/calendar/-/calendar-10.1.4.tgz#aceafe4b006f590270abbbd45c56a713fb8d8c02" + integrity sha512-insqSM5sG5R7jQw7N8/S+fVxUsHf5cxm3VwVDPAoQxWG1kXHXDcaT0Dz75bOm4WdFRnSIDCqt388Tmb2YIbL5g== + dependencies: + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/popper" "10.1.4" + "@dhis2/multi-calendar-dates" "2.0.0" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/card@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/card/-/card-9.11.3.tgz#c58b95015cefccd5797f0d730c49677256ce9823" - integrity sha512-cDrO2UIjaiXVBIVAIgNUWISECaukD1RNrUDZ9wiBQdwiwNhlrVs0uiydZ2Cn4PSneSi8aUKWQvsAlEmOMFfHSg== +"@dhis2-ui/card@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/card/-/card-10.1.4.tgz#847a4bd05a12ba117ab0dcf4fc6a2edbd6072d72" + integrity sha512-u70JG/vHX/cpCZV+n5zsFklzrH6SF3cdiClt5dRg44YR18Xv9XArMmInLCSjueYYe5gusw0pvQ7WEpJzVsmKqQ== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/center@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/center/-/center-9.11.3.tgz#b89c9524b87f6a270710842fdcdd9ec606dff4de" - integrity sha512-XDbtUo8ljfTT6plp8b5VyCMuv4i5t0/JcliGvv5ipZ3kDDuK1whGrhTHiS/bzZRDvz+tD4ca3cewTWReERonaQ== +"@dhis2-ui/center@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/center/-/center-10.1.4.tgz#6b8a3e8624d9d938b3e71c5034368fee7bc6b433" + integrity sha512-Wa5jRijRSw6+YIK3bl6JEzSJe/8FNPwYpWVXTL91S7QP8AKoYgLBXsWMU3LtuyP+M9MRyuNt/PFEXGUhGOHnJg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/checkbox@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/checkbox/-/checkbox-9.11.3.tgz#9541a06a2b78bfa833396deee61a601bb6e1cc45" - integrity sha512-p5dbGsOUf8tbrLec2CwmZJW/DSuIKDVncwZEA7M9+piR5qEgWbc+s9BAv8bTV5dHGCMXA1kgb2LPa1dxO+PRtA== +"@dhis2-ui/checkbox@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/checkbox/-/checkbox-10.1.4.tgz#4df001e3336a62f33de31463983edd2fa0314379" + integrity sha512-mTqY8cgiAQ7Ku1Zr0P4SXdXx6QhY2sdrDajxPSetyHgnXkdZfW9sGhrtVaglTyWa8M3pP3m8pklVkxW8JQ4X+g== dependencies: - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/required" "9.11.3" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/required" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/chip@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/chip/-/chip-9.11.3.tgz#9c6362fa72987ae82da2b88ce59c5b4eafb4810d" - integrity sha512-hCrkypPVa1YJLa2+CZsACDz0MHHcwNOk4qsRNvY4p89/qFYW2UW2ugQMNp7lN7k8gO/AmpDP7dmi/Q9zXA69tg== +"@dhis2-ui/chip@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/chip/-/chip-10.1.4.tgz#32bf2a49d952d22d5bfef56f66322a0c3d5b762f" + integrity sha512-eqk2o4peTMcgAGxCyzg9BfCWzZDJqOZjO62jCI8LCy/vGo29whfTVsrJ4qsegvGZegCzAiX5xRQS5idB5nD3OA== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/cover@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/cover/-/cover-9.11.3.tgz#5b6cdfc5c039d878c2ac62762e7443b10feada66" - integrity sha512-Dazy7ovS4vlzLk0KUSt9/xraZ1epyh4az2hE3J3BsmU05JKL3cXBg/yP6y8c9vvHjzRS8+NSbkv4xeZCKVWG4Q== +"@dhis2-ui/cover@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/cover/-/cover-10.1.4.tgz#dbfbc515346a18f4a287577fae8cf20462a43651" + integrity sha512-WeBNo4KsFCU9WC9feHxqWZEgBnsxPcSug0WyWtI4B4wjpU9ORch/wnd2vxqlV0TMIHYomkCdfmtifzyxW/BeHw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/css@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/css/-/css-9.11.3.tgz#ea891341ef936582dd62d8b41e1f8aeee90c6597" - integrity sha512-N3d0zJzJw2wscIPWpkV+JmWPOxt782XNUGe91ozbPO/kog/yUH+wGfd/bSpLkxVZd/MEAorZOw1lVzRqVseZ+w== +"@dhis2-ui/css@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/css/-/css-10.1.4.tgz#93b90d9225ee13420816fa452e6d69f790d28823" + integrity sha512-NvBUPFBwLKu9OqcR8QfUZOhzB7T1QRE/uqcW0e81Mlwu/NY9ra7BeYZLa7GEACNoSO1K6u/jNCou91pARXEZ5g== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/divider@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/divider/-/divider-9.11.3.tgz#41db46086e1b0abfed7feba143c5c599a5a64889" - integrity sha512-dQcze4RK3YIHvYmpPQW/mcOlnjpOOUfoqrKgHM+JmjiDwynBK+7A2utqCAVBhUJI3oKyEJgmLv5//JmSahVySQ== +"@dhis2-ui/divider@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/divider/-/divider-10.1.4.tgz#43b4d23af11d6f82080e96f064e649f98101253d" + integrity sha512-tqcY+gXQnVScisaqKFJk3tfrxHQ2ik8ILVyahEl8S0a8QOmZGnkzDigOsB+/nzD7HU02Me+6H573EEhZP4tGTg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/field@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/field/-/field-9.11.3.tgz#052121e1619f281736a26cfe03582c3f5dccec65" - integrity sha512-m9Ym9MLwLjkp7ffpK/9ECyfAqc3n0DBsgrUgFuWBpFbx3zHGik1KXpUHFXjHB2QQji5ck4y13kLtwYnOu++E/A== +"@dhis2-ui/field@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/field/-/field-10.1.4.tgz#cc0a19ebaec9bc2155a9f52e3204cee2d5390680" + integrity sha512-x5BL0zXpeG1JAjGZpKa2CQnauh40XaKWEiczzkk7A967VFSyMWS8ndraqO1yBruUPwJZkrYtmIk/hI6pzViahg== dependencies: - "@dhis2-ui/box" "9.11.3" - "@dhis2-ui/help" "9.11.3" - "@dhis2-ui/label" "9.11.3" + "@dhis2-ui/box" "10.1.4" + "@dhis2-ui/help" "10.1.4" + "@dhis2-ui/label" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/file-input@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/file-input/-/file-input-9.11.3.tgz#ad522a5ec8ccaa2bd222f0a710ad2f69ca1aa5ec" - integrity sha512-LIdgPG1haco5Zqdpk7wiKJTdiRIv4j5v1OcoZ46YmOnGYWk/H9u6UuBvGIwidYCR8qRNywwxX/oBwLqC86jkhA== +"@dhis2-ui/file-input@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/file-input/-/file-input-10.1.4.tgz#818810c78ebcd3c51de0a105b6e993b0a6692418" + integrity sha512-MjDCHtzxbF40mPmttczPe+0s+bwyYp6JnPAmuw8QBtmuotdRxQfgzp7sNogVsEOI8GIQ1gUs9Vs+tfRaaf+Q/Q== dependencies: - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/label" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/status-icon" "9.11.3" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/label" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/status-icon" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/header-bar@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/header-bar/-/header-bar-9.11.3.tgz#16f19fba4499b960263da0faa98639c647d68d18" - integrity sha512-nmxW8ws8L+6QSUOgPGWWXQdsbRaRdruNUUULCbHRAPPl3/0m96tL0LKT0hqxXIB5HHZ7eATOkd0aNfcT0KOIzQ== - dependencies: - "@dhis2-ui/box" "9.11.3" - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/center" "9.11.3" - "@dhis2-ui/divider" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/logo" "9.11.3" - "@dhis2-ui/menu" "9.11.3" - "@dhis2-ui/modal" "9.11.3" - "@dhis2-ui/user-avatar" "9.11.3" +"@dhis2-ui/header-bar@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/header-bar/-/header-bar-10.1.4.tgz#ef33e9dca452243927a4fbcc036df3866e8c64f0" + integrity sha512-w0fqIr1V1T4lcyCWDQG3N09j3su4k/Og5mXFIreKz5kPZshJY3j9NH9PzD5hiPsaOcxRn8JwUp/B8ZAcvOvJSQ== + dependencies: + "@dhis2-ui/box" "10.1.4" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/center" "10.1.4" + "@dhis2-ui/divider" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/logo" "10.1.4" + "@dhis2-ui/menu" "10.1.4" + "@dhis2-ui/modal" "10.1.4" + "@dhis2-ui/user-avatar" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" moment "^2.29.1" prop-types "^15.7.2" -"@dhis2-ui/help@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/help/-/help-9.11.3.tgz#d0fb774bb7be6b9fcd6c80f3d7249295b4d4c03e" - integrity sha512-YVjYFuUnSMDhode0gGed+J1RyadD2QeOMQjpHbGkD5PZBxjbbs9dt+/BiOZQbvXL6rrWouQlx6DKFZzD0fGoDw== +"@dhis2-ui/help@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/help/-/help-10.1.4.tgz#18ec5d32b5c5903d63c11efb3f76012f6824ba20" + integrity sha512-8+c8KaTmFwCSdn3/V54JBu0Vu1L0cx8ZTYRQQE/VmUzYN5qPyn++n1X40d46do7D2ZXXBZaOe+DmIebSdRqalw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/input@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/input/-/input-9.11.3.tgz#d3b58eab8a06a62ced16c350820f578c416becc3" - integrity sha512-zSGZUjO1rz6LMNrTDoNikryDhbPu7uCdG7PhEXVqOC6k7fOzPh5OAqNMClP4VAjABJupBComYU3gu27p0Gn5pw== +"@dhis2-ui/input@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/input/-/input-10.1.4.tgz#e97514314827d826be4c70c4f154d349612d34db" + integrity sha512-06SxCelvY1RhV38cUngGQxgOvj5UxqVUPmk4ty4ZbEvMarZqveEGQgVfsticHPrYTuYwHohnXhS+/V1PL6CuQA== dependencies: - "@dhis2-ui/box" "9.11.3" - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/status-icon" "9.11.3" + "@dhis2-ui/box" "10.1.4" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/status-icon" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/intersection-detector@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/intersection-detector/-/intersection-detector-9.11.3.tgz#ea8a1044e4b4f2219e08832029ad8871cae325a8" - integrity sha512-Qqrh+hk4rzbSbDew8RHwo3OcmYQJIxsAv4ecz34wfRcBReulXZWUD0+ATWtbKZLRepsFYg0fFGMVCNljmtNQoQ== +"@dhis2-ui/intersection-detector@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/intersection-detector/-/intersection-detector-10.1.4.tgz#0e839693d384a979eb4640ff9fd0eeb2e9a0b760" + integrity sha512-UEfzGNtucfDYGw96kwbxET3VDbNZ3pfKJ0qRWcZi79uf+1sAy8iuS9EGf+OhjWqSZneOxEoyHzvw9KaSnNOFVA== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/label@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/label/-/label-9.11.3.tgz#98d42328314bfc3edcdd8b84f161dbd6fee9f555" - integrity sha512-I0Gbyh3DZY6zP4c1fisSkNRGP2cAyQXoKW8o1S1dJoHZULX7lWCgZGq+SsCXxoJUn33nRmzMWVz4DiPewjWjgA== +"@dhis2-ui/label@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/label/-/label-10.1.4.tgz#5e1e85c717c0f00a8f801221444c1486c9bededd" + integrity sha512-jE4digfju/BWmQ9q4hGLlsRcWth14UuExq83opVR0zwwXV5Wftjj2Fwilr5LG3zf4Nog6KPlqEAdRZAAflRpBw== dependencies: - "@dhis2-ui/required" "9.11.3" + "@dhis2-ui/required" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/layer@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/layer/-/layer-9.11.3.tgz#b4677122661045ad08bce44240fd3f86ae2dca1d" - integrity sha512-8rpi3GBvc+RCOM8XDhnrw5MNiTICnuTg8pyQX0hCNrb10cw37B2cOI5uui5uzRYfHJOOvEOt/5717CwUxQo6Tg== +"@dhis2-ui/layer@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/layer/-/layer-10.1.4.tgz#aaff81030f9a65ad99743133f66d6adffc780fb2" + integrity sha512-+ywquGV+9YTzytiwvut2ZUJSm2jMScA0nbd0GXM4JUvwMvYUX92t9u396138fVdRRb7+eHmdl9J0avjBiT3xow== dependencies: - "@dhis2-ui/portal" "9.11.3" + "@dhis2-ui/portal" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/legend@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/legend/-/legend-9.11.3.tgz#926fd7b1ad04c7489bc9aab33c91579815b0c4ae" - integrity sha512-UZzrzkutdxoMDylx0bJbO/rhjVupLQQSVIUbK/wk74RXaSQyJe5RGteisQ5fenUS9qrQavx6LjSrSjYxxhbZwA== +"@dhis2-ui/legend@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/legend/-/legend-10.1.4.tgz#273e8523042725bf051ebfe3f4dec7ef7ded5e23" + integrity sha512-bLeIOZYrwd14Ap+VzYN12i6zWqloMoIcmxYW9ovOrdbqEsQ6i5M3m9SDroZuTFWaR9ALrYk5Rp6/RHcHPKedyw== dependencies: - "@dhis2-ui/required" "9.11.3" + "@dhis2-ui/required" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/loader@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/loader/-/loader-9.11.3.tgz#699da865c7c086cea71e62725dd26fbdf64e8e15" - integrity sha512-Gnlb8EVSUmZHz7x+bqKo/SJNDeAaYvXjeJ7unpUyqD2RPKGoT0NQziTOXBYl37mSXhyvQq2JQWoW6csQ0iQZvQ== +"@dhis2-ui/loader@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/loader/-/loader-10.1.4.tgz#8344988bd3f18ad8c6de40f015f69bc95e4ca6a2" + integrity sha512-vGpSn6ifUzilBFrOo94TWcHkYD28q1AyJYd2TuQPadOKPbAVYgXlbm9UKM7AQaOfIj3OH6BcV2Sweskc2nmhbQ== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/logo@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/logo/-/logo-9.11.3.tgz#3144759b990de416a4aad6d8ca2a69353461ee18" - integrity sha512-ZExcTOFMP6nK/vtnux3+gdzkyWWYegRSx70gqUkGGu+D3oaSwdz8twNgRTUciCD6iFjnNfXoNKKFHgF/FzTzzw== +"@dhis2-ui/logo@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/logo/-/logo-10.1.4.tgz#3c4844adc3725b5f5cc29faea1482f1240f272bc" + integrity sha512-hkByyepjmEOGnRforZ5kq4FUmcCQ/cJpUgWI3ximJJ3Yf+YR4ao4C+fzWL8WyYBUwDzdKAqwshDcAAylurcUbg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/menu@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/menu/-/menu-9.11.3.tgz#e25697cc5dcce124cda934a3be3ee04c46570038" - integrity sha512-2krE1howGi6zJBTZ9pSLJe2Yj8UUL6jc8+V1A0fvUpfQjaTBGjw5jsspbnB3oODcBctmD4Sb8qUeHU7CZjLgfQ== +"@dhis2-ui/menu@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/menu/-/menu-10.1.4.tgz#3506118c611fbaf66221c0a9e0e1c84f55c1a0ae" + integrity sha512-if1ra4igTTMitpxIlq6l2alJhWWDq4+tu5FH8HIK3eRgsr4F1ee4AYg7m7b4N7Nq1DU1EKTIdE5HjvF/51q7Mg== dependencies: - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/divider" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/popper" "9.11.3" - "@dhis2-ui/portal" "9.11.3" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/divider" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/popper" "10.1.4" + "@dhis2-ui/portal" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/modal@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/modal/-/modal-9.11.3.tgz#0dd5a2ab81ce85fbf5f41b94b53249d478cb2f05" - integrity sha512-OezEX+fdzbyjbTKcUwYAoHOOHyyagN2N+tsEge0lE05qRvjU/jPTOJhMe4xu+yIivWAZkbNPjThlmx6Wc84FUQ== +"@dhis2-ui/modal@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/modal/-/modal-10.1.4.tgz#794b940c4f827b21600251341b0aedfcb0482038" + integrity sha512-sEGtWXeoX/fzvARcU4YKAIzZougbWMM8uJfYg0h5t0nhGFphkF6hX9YCfZzdX6/Dvrd1R2jtd/x2s2uTD3cusQ== dependencies: - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/center" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/portal" "9.11.3" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/center" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/portal" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/node@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/node/-/node-9.11.3.tgz#b4694e14e46ab0ecf974d9e94b4816f879737993" - integrity sha512-ORFsPFgOMyXmeGY+HqnB/iwkLqTctNaMFvg7Q6YvczncROT7EGEqBqS0xJmvMp6OzLmE3QlY3LFs8n2QDm17Jg== +"@dhis2-ui/node@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/node/-/node-10.1.4.tgz#ecd9b093f926471669559fc5a1d0c96b422f83c2" + integrity sha512-oy2Pxnm21W5woWZxwRsTRNYI6YvrZieHSQcUB5AVcCrN+J4IQYbOXMtUrju0mMEjnywUBEL/NNrCWKEFgqYDiQ== dependencies: - "@dhis2-ui/loader" "9.11.3" + "@dhis2-ui/loader" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/notice-box@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/notice-box/-/notice-box-9.11.3.tgz#f8948e5e7a6465a3e116b7d4e50f85b1c00a6b13" - integrity sha512-T+dq0eSj9FJXgOg6DHuYer2OpStZhhcNf2MoaRo69kySUnpgOz+iThbVPRQW/lfwLdccdl8mpHykrQ0PVDySyw== +"@dhis2-ui/notice-box@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/notice-box/-/notice-box-10.1.4.tgz#9cd8fb65fbd3f5ae925baa5c2c6744c363934706" + integrity sha512-sD9Nv5IpHL3d2wXVS6UiTg7hDE6CNjkJK0Tm/iwFwCdvz9jEmxpm3BmOZeFsYsibDnqLhSGN3ob0xqhDVhgQUw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/organisation-unit-tree@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/organisation-unit-tree/-/organisation-unit-tree-9.11.3.tgz#ed5119794508e147866fc12c4918c819e765e6bb" - integrity sha512-aXh2ZmCbT6ppTHA9PjwCkU+4ASZnxDID6F1FE4Ok5IlCOqnwnK5QsJggEH9iEChUFjOs/gzRtizOHwORICVM6g== +"@dhis2-ui/organisation-unit-tree@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/organisation-unit-tree/-/organisation-unit-tree-10.1.4.tgz#28b49df8964459277a7897e400aff4bab917454d" + integrity sha512-A4pA+/1OQgvUOZl+OxrR733ezQPCjaZii6ly0zxJ2E6pxZDnK9TD/zYhTrneKULgoV06g6H5kZ6J70l6axJPGw== dependencies: - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/checkbox" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/node" "9.11.3" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/checkbox" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/node" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/pagination@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/pagination/-/pagination-9.11.3.tgz#40080262f1ec19d5528b04a053b044a2a6b7202c" - integrity sha512-rduo7ZhLpB1LrXESJrzkr3wo3eiZdOXq4Bii8F4et1T+zn1RdU3dM9hyCf0TkFRIpHZyK077N/YujJkodZBlEw== +"@dhis2-ui/pagination@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/pagination/-/pagination-10.1.4.tgz#ecd6d17ca3b775e19a815e1c365d56ff992bb964" + integrity sha512-4qqmnWV9jfReZfmT+gcY4Y9wWDTUtiTEEpSX0mVCPqKpfrs9Oyvdca5oc1o05QLcupteTyk1bitUCk3+Z8e5vw== dependencies: - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/select" "9.11.3" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/select" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/popover@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/popover/-/popover-9.11.3.tgz#6f4dabb86595a628bb8099a579620116e74a21ab" - integrity sha512-eElXEBw0RTYbnMrc9E5wlAf62reGVC0yx76TGmBEb+2vc3735oJ38S/RqcbZJWW6yk9xmsK7f0svigWQDMxB9A== +"@dhis2-ui/popover@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/popover/-/popover-10.1.4.tgz#d802814861b42f21ed6c9966c954cd7b64a87607" + integrity sha512-yEERm/Tz5coa2VRcRVTsUKxbeiJVWycfk9w7GO8jpJZSduIfG375R7UgT5u0RmKqHxyyaMZxsNOLVLgIyCpjaA== dependencies: - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/popper" "9.11.3" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/popper" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/popper@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/popper/-/popper-9.11.3.tgz#64be0a253e2517c5cf6bda90fdd71f0940fb9797" - integrity sha512-cp94YmH6rTQEcRnl26SW8lQhYwwBwLLmW+LYyDP1j0s8YLPLUGfXCf5VyOykbncz580Xd8R/u9wVWDbDcI00fg== +"@dhis2-ui/popper@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/popper/-/popper-10.1.4.tgz#4ceb124243df3e3457bf4d1dafc05ffee9ff3725" + integrity sha512-MwkGq6N+Ge+kc3l6ETFKiudgp+FG65SLgIY1nQgxgvxxtN1YOEfai3iznN5IFGrB1dKooTA8PqeKug7d8Fn4+Q== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" "@popperjs/core" "^2.10.1" classnames "^2.3.1" prop-types "^15.7.2" react-popper "^2.2.5" resize-observer-polyfill "^1.5.1" -"@dhis2-ui/portal@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/portal/-/portal-9.11.3.tgz#2436ee9758d834536e42664ce534dec8758c9a4f" - integrity sha512-7EPXGoiW/2ZvDRigwTjAQ7o2jq4LAHQGqhjGudCdWxF/UstZUtcGGlG8sMQOGTOhVbuYduXVA0giLFq0iq2zjA== +"@dhis2-ui/portal@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/portal/-/portal-10.1.4.tgz#8c579d04771caebc19b18c864fda168641ede9b5" + integrity sha512-6jsmkcU9sdJgZy4O3H8Xke8KNQtT9WJzhb2JVpmMwW1bZIl7QIpWQzG+/VBb4yukp62cOFvJS2EnFgfD9WFmeA== dependencies: classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/radio@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/radio/-/radio-9.11.3.tgz#779b814ebc8e1b14e4a9197b99139f66a483a23c" - integrity sha512-vFWl5sTAicezSHKpncD3+SfxIC96zSm9EkN2klI+0ksN6G54oKpjrDkECbgd52XZblgdnSiaQ0L+ir0SdLbaXw== +"@dhis2-ui/radio@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/radio/-/radio-10.1.4.tgz#622b4e432a8fcafd60531102ab6096ab8d9449b5" + integrity sha512-jhhRDit58Nr+7Csq/2Rv/Y37Zxb4fqJmxBulN0BL6azC5X4SrR+nwfPdpM2E77OmTtCjiYK24+q55ySPmQ1jfg== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/required@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/required/-/required-9.11.3.tgz#a684c7e01bc148fcd12645605dfb234f0b401397" - integrity sha512-jOeWSLYbQlJMuqPw097gzYyrtTBwGHgNrOpOc9xmbC9rvln7fl8Tvdrks2V5MVz7v+P/caEpeZNNFQcFQF983A== +"@dhis2-ui/required@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/required/-/required-10.1.4.tgz#0e05b4d86279ab844be423f90fc67f3110d34926" + integrity sha512-311sLPkR+4l3nRVMONWvQFhOi5TQdzpwy2xEWZzJ89arCIsIPr4K00H2V2krrSoPKjZRHJpXlyXMJp9eiw5Izw== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/segmented-control@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/segmented-control/-/segmented-control-9.11.3.tgz#61604d58db21e8b3194b781399f41b016fd6fb98" - integrity sha512-korTtvS2vn7iuqGw4pMkcu3VQi9O4Mii1KEhbnn9tFpNCEeDS92BCWm4EucVXrHS+Ggi1Dp9gMpwBrFRfjflBQ== +"@dhis2-ui/segmented-control@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/segmented-control/-/segmented-control-10.1.4.tgz#3179c41bc019168949fd1f6d63aa4aa5aeb0ce7c" + integrity sha512-FF3FfmTsVtGFj1mC2mobtRboy9sNxKQ1BByRGtWi5Bl7pr9FudXl6tR726n54f0td70tcHnjUiTsMjWx+PRsWA== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/select@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/select/-/select-9.11.3.tgz#dd8b0343b2c6b59cf5d82a4cbcac44c6efce4bda" - integrity sha512-OMtQ98SA46Qgbc7HoRoSxh6m4gNyMbFlGznVV+JDTGZD0VNPF3gHJAl099wXlS45iOwDvqcqQ3O6c9U4hr5UgQ== - dependencies: - "@dhis2-ui/box" "9.11.3" - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/checkbox" "9.11.3" - "@dhis2-ui/chip" "9.11.3" - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/popper" "9.11.3" - "@dhis2-ui/status-icon" "9.11.3" - "@dhis2-ui/tooltip" "9.11.3" +"@dhis2-ui/select@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/select/-/select-10.1.4.tgz#e502e9fe97b08aeaefa8406da82167429720d7ba" + integrity sha512-JA9vZC8T8y4hObc9dTeLJr7VgmZfUYaYAQyNK3QfqOuQ80WLkC14vsTw2JtTdmftrGKHXf9mOVW1Dtcg60HwmQ== + dependencies: + "@dhis2-ui/box" "10.1.4" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/checkbox" "10.1.4" + "@dhis2-ui/chip" "10.1.4" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/popper" "10.1.4" + "@dhis2-ui/status-icon" "10.1.4" + "@dhis2-ui/tooltip" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/selector-bar@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/selector-bar/-/selector-bar-9.11.3.tgz#4038639457dadc09549063f53eddebcc7821909b" - integrity sha512-V1cW7BBzX7n/AIxZR9DH8/w6U4bebZw15udpDvg8qkHma540MWiQzG3D7P4HSKs1UXQ8ES5+IIDijNlNqa7C3A== - dependencies: - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/popper" "9.11.3" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" - "@testing-library/react" "^12.1.2" +"@dhis2-ui/selector-bar@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/selector-bar/-/selector-bar-10.1.4.tgz#d7a0f82611dae3e7e2fccebfc147a578089e5021" + integrity sha512-5oOq/irXi6ga4NcD5O9gEjyDphXnXx5PX9v9jEdSYhzyhQbtwhS4c/I2cdrIQwOtQ4hJPGV8bzSYiCiAZyFr5A== + dependencies: + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/popper" "10.1.4" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" + "@testing-library/react" "^16.0.1" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/sharing-dialog@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/sharing-dialog/-/sharing-dialog-9.11.3.tgz#e4640ed8703424f10b10c4497e7ffd178528b45e" - integrity sha512-hEEwtRAhlldTlAhIjnUof7tPzr0mkWExWDuO1UI/+7JAj1MBlNZ8Ubw78Cx7fgTGveInJkp5vMGFs4+NId0IaQ== - dependencies: - "@dhis2-ui/box" "9.11.3" - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/divider" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/menu" "9.11.3" - "@dhis2-ui/modal" "9.11.3" - "@dhis2-ui/notice-box" "9.11.3" - "@dhis2-ui/popper" "9.11.3" - "@dhis2-ui/select" "9.11.3" - "@dhis2-ui/tab" "9.11.3" - "@dhis2-ui/tooltip" "9.11.3" - "@dhis2-ui/user-avatar" "9.11.3" +"@dhis2-ui/sharing-dialog@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/sharing-dialog/-/sharing-dialog-10.1.4.tgz#4261c888dba364435ab034c7002a75da7b545f74" + integrity sha512-+Fi3CpieaY4NowivZYWIy79uAF/Enf8pAfwV4OZrzriojHklHnLnNeTPlWT0bfDyB3+wd8BB3h3On6e2afjKtw== + dependencies: + "@dhis2-ui/box" "10.1.4" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/divider" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/menu" "10.1.4" + "@dhis2-ui/modal" "10.1.4" + "@dhis2-ui/notice-box" "10.1.4" + "@dhis2-ui/popper" "10.1.4" + "@dhis2-ui/select" "10.1.4" + "@dhis2-ui/tab" "10.1.4" + "@dhis2-ui/tooltip" "10.1.4" + "@dhis2-ui/user-avatar" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" "@react-hook/size" "^2.1.2" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/status-icon@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/status-icon/-/status-icon-9.11.3.tgz#8bb0e4b9c735b0d6c7a24fd9ecd1abc9eb8d624d" - integrity sha512-3YfN6nTtd4JLePGSdxUUzCvJjzP5OSPTJ1Kwkr9aIq1Ax32/WC1Ux9eahn9VX5yje+7oSvgfjQ9SLKXzwFi6iA== +"@dhis2-ui/status-icon@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/status-icon/-/status-icon-10.1.4.tgz#592a87548648355d0227c599de8cfec132304744" + integrity sha512-669yMFeI1FBnZ7aTHSLnB6HEybzxm6q2+8ipWNfmbKfw1gCWOLz6rvtiIsNsqIPTUWffKlrJnYHbdU+J1vwBRA== dependencies: - "@dhis2-ui/loader" "9.11.3" + "@dhis2-ui/loader" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/switch@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/switch/-/switch-9.11.3.tgz#04620cb97a8e0c57f2c63d8ec954513d0029536b" - integrity sha512-fBhJfYomN5BDbj/IxLPunlIg3wc/c1gRLJhJnFrIP+FUHVHIpIgmjBn6uNr2uJhiJU45/D5N/oXouoDg2fBCYg== +"@dhis2-ui/switch@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/switch/-/switch-10.1.4.tgz#b310588a1c465b85b7ad40fb7e5f9c35b60d342e" + integrity sha512-rZcg7fesKjsrupSEVE3qmh+zJscRlcoA3F70kUtpXmChqYK7q9Ehc7ocgrZjyTIWcrWofRKj6ukPeVDmTo4+Lw== dependencies: - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/required" "9.11.3" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/required" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/tab@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/tab/-/tab-9.11.3.tgz#70891a14351553c25bdf33249d8e1fffe549b258" - integrity sha512-DoMjr9Udv0DZnSaFLQsAJ8teZvxCr5XoDWcsER7XYtA4eNUUMDUxMJE7F5SoCRi2PmMcd034eWyv2alAys9H9g== +"@dhis2-ui/tab@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/tab/-/tab-10.1.4.tgz#7004e568c202360a1077600615979a80d1ff42b9" + integrity sha512-JbxRXDnW5PsaiXAl9kRi7d4rV2uzQqQ25rBqF21mKdPv1UU/IA0UdZA1gAgF0+IE96WdEcMPN/bztrku3l2O0g== dependencies: - "@dhis2-ui/tooltip" "9.11.3" + "@dhis2-ui/tooltip" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/table@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/table/-/table-9.11.3.tgz#02678f8a8aba21936b35893a6ceaf490323a36a8" - integrity sha512-o6VET9zhI88wsV/2pCdPEWDG8+Sagp+EXKjU3BMhQzV5SGo6VM9wIiMikL+dXZVAqGak4sibS3cXVyApBLCaqg== +"@dhis2-ui/table@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/table/-/table-10.1.4.tgz#235f1b2539da3e17f592338b96e7873a9cbdc401" + integrity sha512-dKP+iJCiLqQZKRc5GSwibQJn8UPczAPQZ0ueCc+4nSJQ9+X1Weid35hBNp8Me5FrhesDCuS4zw01wRrlCwn5+w== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/tag@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/tag/-/tag-9.11.3.tgz#89dba3534604dfd17c063ac91d334b1a6a747f7a" - integrity sha512-0ulcUiuG3qlTUSUtsEWItCsFyK1PEeac0FDjlzMjohimqj5a41ai9UQImY5shySQP4UEzH2t4qGcCH7DN7kWuw== +"@dhis2-ui/tag@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/tag/-/tag-10.1.4.tgz#fd07d0200f023eeadb62e98d4832f71355388d45" + integrity sha512-5Oz2Ng9OhJ9j375kjNc6No8e3LmczbRWf6/BNsInKb0l+wv9PfebEw5U0YHtoMVbfmTZhQBWqKgP0jW/LstR0Q== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/text-area@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/text-area/-/text-area-9.11.3.tgz#b01453a0a311bfbfa8da161c3fc58430cf101ece" - integrity sha512-SXpvy9AvGJQPWQ6jtsYMPtOtLo3f9WNvoECEKoI9E/SziqrNDb9ZxCvlJJs19deKyd2Pq0V4MJHuJ9kLSYIDjQ== +"@dhis2-ui/text-area@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/text-area/-/text-area-10.1.4.tgz#ed7ff94770690d69f6bd0d45a9399e1a9fce04fe" + integrity sha512-dsFmps4kRM+Y2tNGe7ANRBE9Wus1kYilBG7u2v2wgcdP4G6l3VBNev6Jn0zLrPfskdp4qOpbjhulfsbiIL85Tw== dependencies: - "@dhis2-ui/box" "9.11.3" - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/status-icon" "9.11.3" + "@dhis2-ui/box" "10.1.4" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/status-icon" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-icons" "9.11.3" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-icons" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/tooltip@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/tooltip/-/tooltip-9.11.3.tgz#5ade358097266c4c6017a732d61547fc45edbc30" - integrity sha512-GUgU9pmcGok6LKDf6Lc9mVD0qKL8Qps5skOLZH9VFaN03sWsf9jGDNuIbVDsdHrehN0oc6Al4mm/657Wx0+Xww== +"@dhis2-ui/tooltip@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/tooltip/-/tooltip-10.1.4.tgz#3384b99ea1910844e66929c703270d424f4d41d6" + integrity sha512-b3VoZHq9XPfdGQ6Bxoh+kSaWtndCaGXrwoby19Y9CNTQ8Et0p/VU4e6plOsIdzbfuUz3iFZRLXtOZEJJ1Cae6A== dependencies: - "@dhis2-ui/popper" "9.11.3" - "@dhis2-ui/portal" "9.11.3" + "@dhis2-ui/popper" "10.1.4" + "@dhis2-ui/portal" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/transfer@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/transfer/-/transfer-9.11.3.tgz#fb6bc0d3a04d9ed1d0df5db0f8bd03e579eb739e" - integrity sha512-sHK+QUBVDW+QcCZ3Tk3tukk56We7hvpwXefMmG8zFV6WfHLxMc24CGtS6JswtWmRYBNFwpDI/F/BJubskKWkwA== +"@dhis2-ui/transfer@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/transfer/-/transfer-10.1.4.tgz#ee6f56f72e47fd9d0455d6c60cbff1ba699877bf" + integrity sha512-tRH27A315XYWL8PtvRYHmA4vMvGO6jAIW7kB9iWprbBDCknHPe8ynWhAEbPNq0imF1MXM3jLNoRJ2UsaUiXWkA== dependencies: - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/intersection-detector" "9.11.3" - "@dhis2-ui/loader" "9.11.3" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/intersection-detector" "10.1.4" + "@dhis2-ui/loader" "10.1.4" "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2-ui/user-avatar@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2-ui/user-avatar/-/user-avatar-9.11.3.tgz#ce00e47525736afa849596f398c3d3b901da410e" - integrity sha512-vFTf8kPnMEW9hfcNgQoCedq6OzTvo7+ObSYyTq9/ZmQZ/sr1aByMltuqIDUH8ykoOgD6+T2sLNSN8LKB6Zn0aw== +"@dhis2-ui/user-avatar@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2-ui/user-avatar/-/user-avatar-10.1.4.tgz#f91fa6a6c26d3d2075ba39ceebbd60de5e11fdd0" + integrity sha512-T5r6qBVM3yqo3lSze6tvxFxNkhzi7ctegnr00ZCoBNKoG/LlRKFYLVS9SYIQXu/ao/FwnX6UsFi+ir4/vtXV3g== dependencies: "@dhis2/prop-types" "^3.1.2" - "@dhis2/ui-constants" "9.11.3" + "@dhis2/ui-constants" "10.1.4" classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@^26.8.2": - version "26.8.2" - resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-26.8.2.tgz#a7da752009aae277f5e8f485c7c96853ca535543" - integrity sha512-mJNr3V0sL7XgK9KssX9dGoO5GiuCcIe8epOtvJ8LPFmbfurNYtCcRANXCtybVe3yKfZshxOgXEYvtVqtZJ8TCg== +"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#e398d08e696356908725c8f51f32c30e7cb002ec": + version "26.9.4" + resolved "git+https://github.com/d2-ci/analytics.git#e398d08e696356908725c8f51f32c30e7cb002ec" dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7" @@ -2315,7 +2314,7 @@ "@dnd-kit/utilities" "^3.2.1" "@react-hook/debounce" "^4.0.0" classnames "^2.3.1" - crypto-js "^4.1.1" + crypto-js "^4.2.0" d2-utilizr "^0.2.16" d3-color "^1.2.3" highcharts "^10.3.3" @@ -2524,7 +2523,16 @@ i18next "^10.3" moment "^2.24.0" -"@dhis2/multi-calendar-dates@^1.2.2", "@dhis2/multi-calendar-dates@^1.2.3": +"@dhis2/multi-calendar-dates@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-2.0.0.tgz#febf04f873670960804d38c9ebaa1cadf8050db3" + integrity sha512-pxu81kkkh70tB+CyAub41ulpNJPHyxDGwH2pdcc+NUqrKu4OTQr5ScdCBL2MndShrEKj9J6qj9zKVagvvymH5w== + dependencies: + "@dhis2/d2-i18n" "^1.1.3" + "@js-temporal/polyfill" "0.4.3" + classnames "^2.3.2" + +"@dhis2/multi-calendar-dates@^1.2.2": version "1.2.4" resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-1.2.4.tgz#bf90587c7a27b5ca7345ab63948ded0495e8f287" integrity sha512-OTK4fxLMgTSERU16dof0pBqGBjmq+zen3HB1d4odxMHxykxbUXTmrRkYB7afIXrDrUA/pan5t6UVwmhe/O1BWw== @@ -2549,91 +2557,91 @@ workbox-routing "^6.1.5" workbox-strategies "^6.1.5" -"@dhis2/ui-constants@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2/ui-constants/-/ui-constants-9.11.3.tgz#bc0d2ab460e3868ebbd1e7a9b4cc12c4e60053b5" - integrity sha512-vD6GbkhvO0q51lo0KqOHp6u8qoPZWn4GE8eDMAwyZ21Xx1YLTGPCXgoPquvC+VD1N/rJ4SxtBKLpLPKQ5YDx6g== +"@dhis2/ui-constants@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2/ui-constants/-/ui-constants-10.1.4.tgz#3d799233756ba19cf9ad88785422c224c8c7149a" + integrity sha512-7HOT2DyENTmpqY81vIC4hDPPlaVNYs2UfGqvQRokGblpEEwffX9Q8rSMltFkn6GDOo2Qp4RQhU1YUemj4DyMRA== dependencies: prop-types "^15.7.2" -"@dhis2/ui-forms@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2/ui-forms/-/ui-forms-9.11.3.tgz#a1e9217cf753d57ba3d89220296b2f55ccda49c6" - integrity sha512-8lLoCgYRLEhD/SLtq89lJ2E7bwHWRpM0JndIvdhR67Q3XYjOGPJyEsjN8k53fEGAxdzsqmneCt/KSiX/a+tb+A== - dependencies: - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/checkbox" "9.11.3" - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/file-input" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/radio" "9.11.3" - "@dhis2-ui/select" "9.11.3" - "@dhis2-ui/switch" "9.11.3" - "@dhis2-ui/text-area" "9.11.3" +"@dhis2/ui-forms@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2/ui-forms/-/ui-forms-10.1.4.tgz#3fc75376f9d866313c4fdcacc5daadc088c3cbd5" + integrity sha512-3iUo42xhQHFMpo8sha1CsYdUn3Z/i/wJFterVbfD4vLNJJVCxGphL4YQ1o3A3iayEhlRyXs2dXVsliQVE8keGg== + dependencies: + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/checkbox" "10.1.4" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/file-input" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/radio" "10.1.4" + "@dhis2-ui/select" "10.1.4" + "@dhis2-ui/switch" "10.1.4" + "@dhis2-ui/text-area" "10.1.4" "@dhis2/prop-types" "^3.1.2" classnames "^2.3.1" final-form "^4.20.2" prop-types "^15.7.2" react-final-form "^6.5.3" -"@dhis2/ui-icons@9.11.3": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2/ui-icons/-/ui-icons-9.11.3.tgz#4a403a768f1a9f84e2ed563a5900c0e571f64581" - integrity sha512-pvaxQePgqERy5GHqoV/8RaUjBuCU5FiC79CmsUbcrrI3W4+Pb+1u9ig2BBH/ntZJ/0iGzVC2EcH/OVRCGSNR1g== - -"@dhis2/ui@^9.11.3", "@dhis2/ui@^9.8.9": - version "9.11.3" - resolved "https://registry.yarnpkg.com/@dhis2/ui/-/ui-9.11.3.tgz#134dc47c17b4f226b20e302ef08f7a3d11e5ee4b" - integrity sha512-Wsyu9HGpqjLR2bzVtmsoequb1PSzPmEe7Crbx2hlrqbwT8E2sZtfHnvEYDDgqyk9r49VZMRx+5bQH7YR2Bqxbg== - dependencies: - "@dhis2-ui/alert" "9.11.3" - "@dhis2-ui/box" "9.11.3" - "@dhis2-ui/button" "9.11.3" - "@dhis2-ui/calendar" "9.11.3" - "@dhis2-ui/card" "9.11.3" - "@dhis2-ui/center" "9.11.3" - "@dhis2-ui/checkbox" "9.11.3" - "@dhis2-ui/chip" "9.11.3" - "@dhis2-ui/cover" "9.11.3" - "@dhis2-ui/css" "9.11.3" - "@dhis2-ui/divider" "9.11.3" - "@dhis2-ui/field" "9.11.3" - "@dhis2-ui/file-input" "9.11.3" - "@dhis2-ui/header-bar" "9.11.3" - "@dhis2-ui/help" "9.11.3" - "@dhis2-ui/input" "9.11.3" - "@dhis2-ui/intersection-detector" "9.11.3" - "@dhis2-ui/label" "9.11.3" - "@dhis2-ui/layer" "9.11.3" - "@dhis2-ui/legend" "9.11.3" - "@dhis2-ui/loader" "9.11.3" - "@dhis2-ui/logo" "9.11.3" - "@dhis2-ui/menu" "9.11.3" - "@dhis2-ui/modal" "9.11.3" - "@dhis2-ui/node" "9.11.3" - "@dhis2-ui/notice-box" "9.11.3" - "@dhis2-ui/organisation-unit-tree" "9.11.3" - "@dhis2-ui/pagination" "9.11.3" - "@dhis2-ui/popover" "9.11.3" - "@dhis2-ui/popper" "9.11.3" - "@dhis2-ui/portal" "9.11.3" - "@dhis2-ui/radio" "9.11.3" - "@dhis2-ui/required" "9.11.3" - "@dhis2-ui/segmented-control" "9.11.3" - "@dhis2-ui/select" "9.11.3" - "@dhis2-ui/selector-bar" "9.11.3" - "@dhis2-ui/sharing-dialog" "9.11.3" - "@dhis2-ui/switch" "9.11.3" - "@dhis2-ui/tab" "9.11.3" - "@dhis2-ui/table" "9.11.3" - "@dhis2-ui/tag" "9.11.3" - "@dhis2-ui/text-area" "9.11.3" - "@dhis2-ui/tooltip" "9.11.3" - "@dhis2-ui/transfer" "9.11.3" - "@dhis2-ui/user-avatar" "9.11.3" - "@dhis2/ui-constants" "9.11.3" - "@dhis2/ui-forms" "9.11.3" - "@dhis2/ui-icons" "9.11.3" +"@dhis2/ui-icons@10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2/ui-icons/-/ui-icons-10.1.4.tgz#b8a794e60e4a588b28d4922de19b30d491aa2f6f" + integrity sha512-XP+Yy7SUlUZbfggSIEBBerH5kKg0dP5UmyJwLYPgv4fR1NMNrbcL6KEftEL/YWDkGN5jLQm1U9ZqnbV/RL38xA== + +"@dhis2/ui@^10.1.4", "@dhis2/ui@^9.8.9": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@dhis2/ui/-/ui-10.1.4.tgz#e4e284a79b6ef626e886db99ae1706baa4314c92" + integrity sha512-ZpXHRKFKQFXqXp/Nb1WYvTAfrDS8OUEUB7WTujQ1vvVIGlW7vAfQ8qaGEZy8V7WqEgc3WoyqLaOcyyX3U9Psuw== + dependencies: + "@dhis2-ui/alert" "10.1.4" + "@dhis2-ui/box" "10.1.4" + "@dhis2-ui/button" "10.1.4" + "@dhis2-ui/calendar" "10.1.4" + "@dhis2-ui/card" "10.1.4" + "@dhis2-ui/center" "10.1.4" + "@dhis2-ui/checkbox" "10.1.4" + "@dhis2-ui/chip" "10.1.4" + "@dhis2-ui/cover" "10.1.4" + "@dhis2-ui/css" "10.1.4" + "@dhis2-ui/divider" "10.1.4" + "@dhis2-ui/field" "10.1.4" + "@dhis2-ui/file-input" "10.1.4" + "@dhis2-ui/header-bar" "10.1.4" + "@dhis2-ui/help" "10.1.4" + "@dhis2-ui/input" "10.1.4" + "@dhis2-ui/intersection-detector" "10.1.4" + "@dhis2-ui/label" "10.1.4" + "@dhis2-ui/layer" "10.1.4" + "@dhis2-ui/legend" "10.1.4" + "@dhis2-ui/loader" "10.1.4" + "@dhis2-ui/logo" "10.1.4" + "@dhis2-ui/menu" "10.1.4" + "@dhis2-ui/modal" "10.1.4" + "@dhis2-ui/node" "10.1.4" + "@dhis2-ui/notice-box" "10.1.4" + "@dhis2-ui/organisation-unit-tree" "10.1.4" + "@dhis2-ui/pagination" "10.1.4" + "@dhis2-ui/popover" "10.1.4" + "@dhis2-ui/popper" "10.1.4" + "@dhis2-ui/portal" "10.1.4" + "@dhis2-ui/radio" "10.1.4" + "@dhis2-ui/required" "10.1.4" + "@dhis2-ui/segmented-control" "10.1.4" + "@dhis2-ui/select" "10.1.4" + "@dhis2-ui/selector-bar" "10.1.4" + "@dhis2-ui/sharing-dialog" "10.1.4" + "@dhis2-ui/switch" "10.1.4" + "@dhis2-ui/tab" "10.1.4" + "@dhis2-ui/table" "10.1.4" + "@dhis2-ui/tag" "10.1.4" + "@dhis2-ui/text-area" "10.1.4" + "@dhis2-ui/tooltip" "10.1.4" + "@dhis2-ui/transfer" "10.1.4" + "@dhis2-ui/user-avatar" "10.1.4" + "@dhis2/ui-constants" "10.1.4" + "@dhis2/ui-forms" "10.1.4" + "@dhis2/ui-icons" "10.1.4" prop-types "^15.7.2" "@dnd-kit/accessibility@^3.0.0": @@ -4038,21 +4046,20 @@ lz-string "^1.4.4" pretty-format "^27.0.2" -"@testing-library/jest-dom@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.1.2.tgz#3e7422904349223cb1e04968adada63f65f40d5b" - integrity sha512-NP9jl1Q2qDDtx+cqogowtQtmgD2OVs37iMSIsTv5eN5ETRkf26Kj6ugVwA93/gZzzFWQAsgkKkcftDe91BJCkQ== +"@testing-library/jest-dom@^6.6.3": + version "6.6.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" + integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== dependencies: - "@adobe/css-tools" "^4.3.0" - "@babel/runtime" "^7.9.2" + "@adobe/css-tools" "^4.4.0" aria-query "^5.0.0" chalk "^3.0.0" css.escape "^1.5.1" - dom-accessibility-api "^0.5.6" - lodash "^4.17.15" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" redent "^3.0.0" -"@testing-library/react@^12", "@testing-library/react@^12.1.2": +"@testing-library/react@^12": version "12.1.5" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== @@ -4061,6 +4068,13 @@ "@testing-library/dom" "^8.0.0" "@types/react-dom" "<18.0.0" +"@testing-library/react@^16.0.1": + version "16.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875" + integrity sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg== + dependencies: + "@babel/runtime" "^7.12.5" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -6613,7 +6627,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-js@^4.1.1: +crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== @@ -6843,6 +6857,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +cypress-real-events@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.13.0.tgz#6b7cd32dcac172db1493608f97a2576c7d0bd5af" + integrity sha512-LoejtK+dyZ1jaT8wGT5oASTPfsNV8/ClRp99ruN60oPj8cBJYod80iJDyNwfPAu4GCxTXOhhAv9FO65Hpwt6Hg== + cypress@^13.13.1: version "13.13.1" resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.1.tgz#860c1142a6e58ea412a764f0ce6ad07567721129" @@ -7301,11 +7320,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -16011,7 +16035,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16110,7 +16143,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17794,8 +17834,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17822,6 +17861,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"