From f5e61788c570581a49dc56797a59684af9366d9b Mon Sep 17 00:00:00 2001 From: Ignacio Foche Perez Date: Thu, 19 Nov 2020 12:47:22 +0100 Subject: [PATCH 01/11] version bumped --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b97c38b3..f2152d7b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "d2-reports", "description": "DHIS2 Reports", - "version": "0.0.2", + "version": "0.0.3", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".", From 22690eec85eaf39800a1ca6143c5ddee7afcedd6 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 11 Dec 2020 14:09:31 +0100 Subject: [PATCH 02/11] Remove now unused SECTION_ORDER attribute --- src/data/Dhis2ConfigRepository.ts | 10 +--------- src/data/Dhis2DataValueRepository.ts | 2 -- src/domain/entities/Config.ts | 1 - 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/data/Dhis2ConfigRepository.ts b/src/data/Dhis2ConfigRepository.ts index 23e50d26..2f3c88dc 100644 --- a/src/data/Dhis2ConfigRepository.ts +++ b/src/data/Dhis2ConfigRepository.ts @@ -8,7 +8,6 @@ import { User } from "../domain/entities/User"; const base = { dataSets: { namePrefix: "NHWA", nameExcluded: /old$/ }, sqlViewName: "NHWA Data Comments", - sectionOrderAttributeCode: "SECTION_ORDER", constantCode: "NHWA_COMMENTS", }; @@ -16,11 +15,9 @@ export class Dhis2ConfigRepository implements ConfigRepository { constructor(private api: D2Api) {} async get(): Promise { - const { dataSets, constants, sqlViews, attributes } = await this.getMetadata(); + const { dataSets, constants, sqlViews } = await this.getMetadata(); const filteredDataSets = getFilteredDataSets(dataSets); - const attributeCode = base.sectionOrderAttributeCode; const getDataValuesSqlView = getFirst(sqlViews, `Missing sqlView: ${base.sqlViewName}`); - const sectionOrderAttribute = getFirst(attributes, `Missing attribute: ${attributeCode}`); const constant = getFirst(constants, `Missing constant: ${base.constantCode}`); const currentUser = await this.getCurrentUser(); const pairedDataElements = getPairedMapping(filteredDataSets); @@ -32,7 +29,6 @@ export class Dhis2ConfigRepository implements ConfigRepository { dataSets: keyById(filteredDataSets), currentUser, getDataValuesSqlView, - sectionOrderAttribute, pairedDataElementsByDataSet: pairedDataElements, sections: keyById(sections), sectionsByDataSet, @@ -60,10 +56,6 @@ export class Dhis2ConfigRepository implements ConfigRepository { fields: { id: true }, filter: { name: { eq: base.sqlViewName } }, }, - attributes: { - fields: { id: true }, - filter: { code: { eq: base.sectionOrderAttributeCode } }, - }, }); return metadata$.getData(); diff --git a/src/data/Dhis2DataValueRepository.ts b/src/data/Dhis2DataValueRepository.ts index c6542011..c94fc4d0 100644 --- a/src/data/Dhis2DataValueRepository.ts +++ b/src/data/Dhis2DataValueRepository.ts @@ -17,7 +17,6 @@ interface Variables { periods: string; orderByColumn: SqlField; orderByDirection: "asc" | "desc"; - sectionOrderAttributeId: Id; commentPairs: string; } @@ -75,7 +74,6 @@ export class Dhis2DataValueRepository implements DataValueRepository { orderByColumn: fieldMapping[sorting.field], orderByDirection: sorting.direction, commentPairs, - sectionOrderAttributeId: config.sectionOrderAttribute.id, }, paging ) diff --git a/src/domain/entities/Config.ts b/src/domain/entities/Config.ts index cba5f7ad..74d17a20 100644 --- a/src/domain/entities/Config.ts +++ b/src/domain/entities/Config.ts @@ -4,7 +4,6 @@ import { User } from "./User"; export interface Config { dataSets: Record; sections: Record; - sectionOrderAttribute: Ref; currentUser: User; getDataValuesSqlView: Ref; pairedDataElementsByDataSet: { From 2b484847f7ad43ddf6a46f8b842d0261aabb02a6 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 11 Dec 2020 14:51:14 +0100 Subject: [PATCH 03/11] Detault to .. if href has wrong value * --- src/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 2f38a5b2..53168029 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,7 +15,8 @@ async function getBaseUrl() { return baseUrl.replace(/\/*$/, ""); } else { const { data: manifest } = await axios.get("manifest.webapp"); - return manifest.activities.dhis.href; + const href = manifest.activities.dhis.href; + return href === "*" ? ".." : href; } } From d1d644909a71aea510ed8e34dd7389a6c1b47a8d Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 11 Dec 2020 15:14:19 +0100 Subject: [PATCH 04/11] Fix css for 2.34.3 --- src/webapp/components/app/App.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/webapp/components/app/App.css b/src/webapp/components/app/App.css index 4f0ed8f1..1f9e2f59 100644 --- a/src/webapp/components/app/App.css +++ b/src/webapp/components/app/App.css @@ -11,3 +11,7 @@ body { li { line-height: 1.75; } + +table th, table td { + border: none !important +} From 842d4e1c385cdcd678b3bf3d6417c36f28d88d6e Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 5 Mar 2021 09:55:39 +0100 Subject: [PATCH 05/11] Add generic useBooleanState hook --- src/webapp/utils/use-boolean.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/webapp/utils/use-boolean.ts diff --git a/src/webapp/utils/use-boolean.ts b/src/webapp/utils/use-boolean.ts new file mode 100644 index 00000000..35b5cf1a --- /dev/null +++ b/src/webapp/utils/use-boolean.ts @@ -0,0 +1,27 @@ +import React from "react"; + +type Callback = () => void; + +type UseBooleanReturn = [boolean, UseBooleanActions]; + +interface UseBooleanActions { + set: (newValue: boolean) => void; + toggle: Callback; + enable: Callback; + disable: Callback; +} + +export function useBooleanState(initialValue: boolean): UseBooleanReturn { + const [value, setValue] = React.useState(initialValue); + + const actions = React.useMemo(() => { + return { + set: (newValue: boolean) => setValue(newValue), + enable: () => setValue(true), + disable: () => setValue(false), + toggle: () => setValue(value_ => !value_), + }; + }, [setValue]); + + return [value, actions]; +} From 4edb6def086d612d16511ac2ee4e9c39c0d70d29 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 5 Mar 2021 09:56:52 +0100 Subject: [PATCH 06/11] Move orgunit tree to filter box, as a popable dialog --- .gitignore | 1 + i18n/en.pot | 19 ++++- i18n/es.po | 17 ++++- src/compositionRoot.ts | 6 ++ src/data/Dhis2OrgUnitsRepository.ts | 22 ++++++ src/domain/entities/Config.ts | 6 ++ src/domain/entities/OrgUnit.ts | 18 +++-- src/domain/repositories/OrgUnitsRepository.ts | 5 ++ src/domain/usecases/GetOrgUnitsUseCase.ts | 10 +++ .../data-values-list/DataValuesFilters.tsx | 23 ++++-- .../data-values-list/DataValuesList.tsx | 75 +++++++++---------- .../data-values-list/FiltersBox.tsx | 46 ++++++++++++ .../data-values-list/OrgUnitsFilter.tsx | 14 ++-- .../data-values-list/OrgUnitsFilterButton.tsx | 50 +++++++++++++ 14 files changed, 248 insertions(+), 64 deletions(-) create mode 100644 src/data/Dhis2OrgUnitsRepository.ts create mode 100644 src/domain/repositories/OrgUnitsRepository.ts create mode 100644 src/domain/usecases/GetOrgUnitsUseCase.ts create mode 100644 src/webapp/components/data-values-list/FiltersBox.tsx create mode 100644 src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx diff --git a/.gitignore b/.gitignore index 12866fba..31e1d722 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ cypress/videos/ # Custom bak +NOTES* diff --git a/i18n/en.pot b/i18n/en.pot index 6a0ae238..47a7c33b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2020-11-18T10:58:48.575Z\n" -"PO-Revision-Date: 2020-11-18T10:58:48.575Z\n" +"POT-Creation-Date: 2021-03-05T08:55:57.149Z\n" +"PO-Revision-Date: 2021-03-05T08:55:57.149Z\n" msgid "Periods" msgstr "" @@ -17,6 +17,9 @@ msgstr "" msgid "Sections" msgstr "" +msgid "NHWA Comments Report" +msgstr "" + msgid "Data set" msgstr "" @@ -47,6 +50,18 @@ msgstr "" msgid "Stored by" msgstr "" +msgid "Toggle filters" +msgstr "" + +msgid "Loading..." +msgstr "" + +msgid "Select parent organisation unit" +msgstr "" + +msgid "Close" +msgstr "" + msgid "" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index a4b10057..3b523e14 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-18T10:58:48.575Z\n" +"POT-Creation-Date: 2021-03-05T08:55:57.149Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -17,6 +17,9 @@ msgstr "" msgid "Sections" msgstr "" +msgid "NHWA Comments Report" +msgstr "" + msgid "Data set" msgstr "" @@ -47,6 +50,18 @@ msgstr "" msgid "Stored by" msgstr "" +msgid "Toggle filters" +msgstr "" + +msgid "Loading..." +msgstr "" + +msgid "Select parent organisation unit" +msgstr "" + +msgid "Close" +msgstr "" + msgid "" msgstr "" diff --git a/src/compositionRoot.ts b/src/compositionRoot.ts index 761a5eeb..fa359ea0 100644 --- a/src/compositionRoot.ts +++ b/src/compositionRoot.ts @@ -4,16 +4,22 @@ import { GetDataValuesUseCase } from "./domain/usecases/GetDataValuesUseCase"; import { GetConfig } from "./domain/usecases/GetConfig"; import { Dhis2ConfigRepository } from "./data/Dhis2ConfigRepository"; import { SaveDataValuesUseCase } from "./domain/usecases/SaveDataValuesCsvUseCase"; +import { GetOrgUnitsUseCase } from "./domain/usecases/GetOrgUnitsUseCase"; +import { Dhis2OrgUnitsRepository } from "./data/Dhis2OrgUnitsRepository"; export function getCompositionRoot(api: D2Api) { const configRepository = new Dhis2ConfigRepository(api); const dataValueRepository = new Dhis2DataValueRepository(api); + const orgUnitsRepository = new Dhis2OrgUnitsRepository(api); return { dataValues: { get: new GetDataValuesUseCase(dataValueRepository), save: new SaveDataValuesUseCase(dataValueRepository), }, + orgUnits: { + get: new GetOrgUnitsUseCase(orgUnitsRepository), + }, config: { get: new GetConfig(configRepository), }, diff --git a/src/data/Dhis2OrgUnitsRepository.ts b/src/data/Dhis2OrgUnitsRepository.ts new file mode 100644 index 00000000..385813b2 --- /dev/null +++ b/src/data/Dhis2OrgUnitsRepository.ts @@ -0,0 +1,22 @@ +import { OrgUnitPath, OrgUnit, getOrgUnitIdsFromPaths } from "../domain/entities/OrgUnit"; +import { OrgUnitsRepository } from "../domain/repositories/OrgUnitsRepository"; +import { D2Api } from "../types/d2-api"; + +export class Dhis2OrgUnitsRepository implements OrgUnitsRepository { + constructor(private api: D2Api) {} + + async getFromPaths(paths: OrgUnitPath[]): Promise { + const ids = getOrgUnitIdsFromPaths(paths); + + const { organisationUnits } = await this.api.metadata + .get({ + organisationUnits: { + filter: { id: { in: ids } }, + fields: { id: true, path: true, name: true, level: true }, + }, + }) + .getData(); + + return organisationUnits; + } +} diff --git a/src/domain/entities/Config.ts b/src/domain/entities/Config.ts index 74d17a20..a4b57298 100644 --- a/src/domain/entities/Config.ts +++ b/src/domain/entities/Config.ts @@ -1,4 +1,6 @@ +import _ from "lodash"; import { Id, NamedRef, Ref } from "./Base"; +import { getPath } from "./OrgUnit"; import { User } from "./User"; export interface Config { @@ -14,3 +16,7 @@ export interface Config { }; years: string[]; } + +export function getMainUserPaths(config: Config) { + return _.compact([getPath(config.currentUser.orgUnits)]); +} diff --git a/src/domain/entities/OrgUnit.ts b/src/domain/entities/OrgUnit.ts index 43d9b2d2..f0435a5c 100644 --- a/src/domain/entities/OrgUnit.ts +++ b/src/domain/entities/OrgUnit.ts @@ -1,15 +1,17 @@ import _ from "lodash"; import { Id } from "./Base"; -type Path = string; +export type OrgUnitPath = string; export interface OrgUnit { - id: string; - path: Path; + id: Id; + path: OrgUnitPath; name: string; level: number; } +const pathSeparator = "/"; + export function getRoots(orgUnits: OrgUnit[]): OrgUnit[] { const minLevel = _.min(orgUnits.map(ou => ou.level)); return _(orgUnits) @@ -22,13 +24,17 @@ export function getRootIds(orgUnits: OrgUnit[]): Id[] { return getRoots(orgUnits).map(ou => ou.id); } -export function getPath(orgUnits: OrgUnit[]): Path | undefined { +export function getPath(orgUnits: OrgUnit[]): OrgUnitPath | undefined { return getRoots(orgUnits).map(ou => ou.path)[0]; } -export function getOrgUnitIdsFromPaths(orgUnitPathsSelected: Path[]): Id[] { +export function getOrgUnitIdsFromPaths(orgUnitPathsSelected: OrgUnitPath[]): Id[] { return _(orgUnitPathsSelected) - .map(path => _.last(path.split("/"))) + .map(path => _.last(path.split(pathSeparator))) .compact() .value(); } + +export function getOrgUnitParentPath(path: OrgUnitPath) { + return _(path).split(pathSeparator).initial().join(pathSeparator); +} diff --git a/src/domain/repositories/OrgUnitsRepository.ts b/src/domain/repositories/OrgUnitsRepository.ts new file mode 100644 index 00000000..c66bcb49 --- /dev/null +++ b/src/domain/repositories/OrgUnitsRepository.ts @@ -0,0 +1,5 @@ +import { OrgUnit, OrgUnitPath } from "../entities/OrgUnit"; + +export interface OrgUnitsRepository { + getFromPaths(paths: OrgUnitPath[]): Promise; +} diff --git a/src/domain/usecases/GetOrgUnitsUseCase.ts b/src/domain/usecases/GetOrgUnitsUseCase.ts new file mode 100644 index 00000000..2a8b556e --- /dev/null +++ b/src/domain/usecases/GetOrgUnitsUseCase.ts @@ -0,0 +1,10 @@ +import { OrgUnit, OrgUnitPath } from "../entities/OrgUnit"; +import { OrgUnitsRepository } from "../repositories/OrgUnitsRepository"; + +export class GetOrgUnitsUseCase { + constructor(private orgUnitsRepository: OrgUnitsRepository) {} + + execute(options: { paths: OrgUnitPath[] }): Promise { + return this.orgUnitsRepository.getFromPaths(options.paths); + } +} diff --git a/src/webapp/components/data-values-list/DataValuesFilters.tsx b/src/webapp/components/data-values-list/DataValuesFilters.tsx index d5155187..16b940bf 100644 --- a/src/webapp/components/data-values-list/DataValuesFilters.tsx +++ b/src/webapp/components/data-values-list/DataValuesFilters.tsx @@ -2,14 +2,18 @@ import React from "react"; import i18n from "../../../locales"; import MultipleDropdown from "../../components/dropdown/MultipleDropdown"; import { Id, NamedRef } from "../../../domain/entities/Base"; +import { useAppContext } from "../../contexts/app-context"; +import { getRootIds } from "../../../domain/entities/OrgUnit"; +import { OrgUnitsFilterButton } from "./OrgUnitsFilterButton"; -interface DataValuesFiltersProps { +export interface DataValuesFiltersProps { values: DataValuesFilter; options: FilterOptions; onChange(newFilters: DataValuesFilter): void; } export interface DataValuesFilter { + orgUnitPaths: Id[]; periods: string[]; dataSetIds: Id[]; sectionIds: Id[]; @@ -22,25 +26,36 @@ interface FilterOptions { } export const DataValuesFilters: React.FC = React.memo(props => { + const { config, api } = useAppContext(); const { values: filter, options: filterOptions, onChange } = props; const periodItems = useMemoOptionsFromStrings(filterOptions.periods); const dataSetItems = useMemoOptionsFromNamedRef(filterOptions.dataSets); const sectionItems = useMemoOptionsFromNamedRef(filterOptions.sections); + const rootIds = React.useMemo(() => getRootIds(config.currentUser.orgUnits), [config]); return (
+ onChange({ ...filter, orgUnitPaths: paths })} + /> + onChange({ ...filter, periods })} label={i18n.t("Periods")} /> + onChange({ ...filter, dataSetIds })} label={i18n.t("Data sets")} /> + ({ value: option.id, text: option.name })); }, [options]); } - -export const emptyDataValuesFilter: DataValuesFilter = { - periods: [], - dataSetIds: [], - sectionIds: [], -}; diff --git a/src/webapp/components/data-values-list/DataValuesList.tsx b/src/webapp/components/data-values-list/DataValuesList.tsx index 3d19e59c..77a4220c 100644 --- a/src/webapp/components/data-values-list/DataValuesList.tsx +++ b/src/webapp/components/data-values-list/DataValuesList.tsx @@ -14,27 +14,19 @@ import { ObjectsList } from "../objects-list/ObjectsList"; import { TableConfig, useObjectsTable } from "../objects-list/objects-list-hooks"; import { useAppContext } from "../../contexts/app-context"; import { DataValue } from "../../../domain/entities/DataValue"; -import { DataValuesFilters, DataValuesFilter, emptyDataValuesFilter } from "./DataValuesFilters"; -import { OrgUnitsFilter } from "./OrgUnitsFilter"; +import { DataValuesFilter } from "./DataValuesFilters"; import { useSnackbarOnError } from "../../utils/snackbar"; -import { - getRootIds, - getPath as getMainPath, - getOrgUnitIdsFromPaths, -} from "../../../domain/entities/OrgUnit"; -import { Config } from "../../../domain/entities/Config"; +import { Config, getMainUserPaths } from "../../../domain/entities/Config"; import { Sorting } from "../../../domain/entities/PaginatedObjects"; import { sortByName } from "../../../domain/entities/Base"; import { Typography, makeStyles } from "@material-ui/core"; import { DataValueViewModel, getDataValueViews } from "../../view-models/DataValueViewModel"; +import { getOrgUnitIdsFromPaths } from "../../../domain/entities/OrgUnit"; +import { FiltersBox } from "./FiltersBox"; export const DataValuesList: React.FC = React.memo(() => { - const { compositionRoot, config, api } = useAppContext(); - const [filters, setFilters] = React.useState(emptyDataValuesFilter); - const rootIds = React.useMemo(() => getRootIds(config.currentUser.orgUnits), [config]); - const [orgUnitPathsSelected, setOrgUnitPathsSelected] = React.useState(() => - getMainUserPaths(config) - ); + const { compositionRoot, config } = useAppContext(); + const [filters, setFilters] = React.useState(() => getEmptyDataValuesFilter(config)); const baseConfig = React.useMemo(getBaseListConfig, []); const [sorting, setSorting] = React.useState>(); @@ -44,13 +36,12 @@ export const DataValuesList: React.FC = React.memo(() => { config, paging: { page: paging.page, pageSize: paging.pageSize }, sorting: getSortingFromTableSorting(sorting), - orgUnitIds: getOrgUnitIdsFromPaths(orgUnitPathsSelected), - ...filters, + ...getUseCaseOptions(filters), }); setSorting(sorting); return { pager, objects: getDataValueViews(config, objects) }; }, - [config, compositionRoot, filters, orgUnitPathsSelected] + [config, compositionRoot, filters] ); const getRowsWithSnackbarOrError = useSnackbarOnError(getRows); @@ -58,28 +49,18 @@ export const DataValuesList: React.FC = React.memo(() => { const filterOptions = React.useMemo(() => getFilterOptions(config, filters), [config, filters]); const classes = useStyles(); - const sideComponents = ( - - ); - const downloadCsv: TableGlobalAction = { name: "downloadCsv", text: "Download CSV", icon: , onClick: async () => { if (!sorting) return; - // Create a new use case that does everything? + // FUTURE: create a single use case that performs the get+saveCSV const { objects: dataValues } = await compositionRoot.dataValues.get.execute({ config, paging: { page: 1, pageSize: 100000 }, sorting: getSortingFromTableSorting(sorting), - orgUnitIds: getOrgUnitIdsFromPaths(orgUnitPathsSelected), - ...filters, + ...getUseCaseOptions(filters), }); compositionRoot.dataValues.save.execute("data-values.csv", dataValues); }, @@ -88,23 +69,30 @@ export const DataValuesList: React.FC = React.memo(() => { return (
- NHWA Comments Report + {i18n.t("NHWA Comments Report")} - - {...tableProps} - sideComponents={sideComponents} - globalActions={[downloadCsv]} - > - + + {...tableProps} globalActions={[downloadCsv]}> +
); }); +function getUseCaseOptions(filter: DataValuesFilter) { + return { + ...filter, + orgUnitIds: getOrgUnitIdsFromPaths(filter.orgUnitPaths), + }; +} + const useStyles = makeStyles({ - wrapper: { - padding: 10, - }, + wrapper: { padding: 10 }, }); function getSortingFromTableSorting(sorting: TableSorting): Sorting { @@ -157,6 +145,11 @@ function getFilterOptions(config: Config, filters: DataValuesFilter) { }; } -function getMainUserPaths(config: Config) { - return _.compact([getMainPath(config.currentUser.orgUnits)]); +function getEmptyDataValuesFilter(config: Config): DataValuesFilter { + return { + orgUnitPaths: getMainUserPaths(config), + periods: [], + dataSetIds: [], + sectionIds: [], + }; } diff --git a/src/webapp/components/data-values-list/FiltersBox.tsx b/src/webapp/components/data-values-list/FiltersBox.tsx new file mode 100644 index 00000000..0b70275f --- /dev/null +++ b/src/webapp/components/data-values-list/FiltersBox.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import _ from "lodash"; +import { IconButton } from "material-ui"; +import { FilterList } from "@material-ui/icons"; +import { DataValuesFilters, DataValuesFiltersProps } from "./DataValuesFilters"; +import { useBooleanState } from "../../utils/use-boolean"; +import i18n from "../../../locales"; + +export interface FiltersBoxProps extends DataValuesFiltersProps { + showToggleButton: boolean; +} + +export const FiltersBox: React.FC = React.memo(props => { + const { showToggleButton = true, ...otherProps } = props; + const areFiltersApplied = !_(props.values).values().every(_.isEmpty); + const [isFilterBoxVisible, { toggle: toggleFilterBoxVisibility }] = useBooleanState(false); + const filterIconColor = areFiltersApplied ? "#ff9800" : undefined; + const filterButtonColor = isFilterBoxVisible ? { backgroundColor: "#cdcdcd" } : undefined; + const areFiltersVisible = !showToggleButton || isFilterBoxVisible; + const filtersStyle = areFiltersVisible ? styles.filters.visible : styles.filters.hidden; + + return ( + + {showToggleButton && ( + + + + )} + +
+ +
+
+ ); +}); + +const styles = { + filters: { + visible: {}, + hidden: { display: "none" }, + }, +}; diff --git a/src/webapp/components/data-values-list/OrgUnitsFilter.tsx b/src/webapp/components/data-values-list/OrgUnitsFilter.tsx index 9dc53621..c9dfc01b 100644 --- a/src/webapp/components/data-values-list/OrgUnitsFilter.tsx +++ b/src/webapp/components/data-values-list/OrgUnitsFilter.tsx @@ -3,19 +3,21 @@ import { D2Api } from "../../../types/d2-api"; import { OrgUnitsSelector } from "d2-ui-components"; import { makeStyles } from "@material-ui/core"; import { Id } from "../../../domain/entities/Base"; +import { getOrgUnitParentPath, OrgUnitPath } from "../../../domain/entities/OrgUnit"; -interface OrgUnitsFilterProps { +export interface OrgUnitsFilterProps { api: D2Api; rootIds: Id[]; - selected: Path[]; - setSelected(newPaths: Path[]): void; + selected: OrgUnitPath[]; + setSelected(newPaths: OrgUnitPath[]): void; } -type Path = string; +const orgUnitsSelectorControls = {}; export const OrgUnitsFilter: React.FC = React.memo(props => { const { api, rootIds, selected, setSelected } = props; const classes = useStyles(); + const initiallyExpanded = React.useMemo(() => selected.map(getOrgUnitParentPath), [selected]); return (
@@ -33,14 +35,12 @@ export const OrgUnitsFilter: React.FC = React.memo(props => selected={selected} singleSelection={true} selectOnClick={true} - initiallyExpanded={[]} + initiallyExpanded={initiallyExpanded} />
); }); -const orgUnitsSelectorControls = {}; - const useStyles = makeStyles({ orgUnitFilter: { order: -1, diff --git a/src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx b/src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx new file mode 100644 index 00000000..8bc80395 --- /dev/null +++ b/src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { OrgUnitsFilter, OrgUnitsFilterProps } from "./OrgUnitsFilter"; +import { ConfirmationDialog } from "d2-ui-components"; +import i18n from "../../../locales"; +import { TextField } from "material-ui"; +import { useBooleanState } from "../../utils/use-boolean"; +import { useAppContext } from "../../contexts/app-context"; + +export interface OrgUnitsFilterButtonProps extends OrgUnitsFilterProps {} + +export const OrgUnitsFilterButton: React.FC = React.memo(props => { + const { compositionRoot } = useAppContext(); + const [isDialogOpen, { enable: openDialog, disable: closeDialog }] = useBooleanState(false); + const loadingMessage = i18n.t("Loading..."); + const [selectedOrgUnits, setSelectedOrgUnits] = React.useState(loadingMessage); + + React.useEffect(() => { + setSelectedOrgUnits(loadingMessage); + compositionRoot.orgUnits.get + .execute({ paths: props.selected }) + .then(orgUnits => setSelectedOrgUnits(orgUnits.map(ou => ou.name).join(", "))) + .catch(() => setSelectedOrgUnits(props.selected.join(", "))); + }, [compositionRoot, props.selected, loadingMessage]); + + return ( + + + + + + + + + + ); +}); + +const styles = { + textField: { display: "inline-flex", marginTop: -24 }, +}; From 203ea8ade42d2f13137eed98a7b564b9edee3c9b Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 5 Mar 2021 10:14:08 +0100 Subject: [PATCH 07/11] Update caniuse package --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1d5f5074..675d831a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4196,9 +4196,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001087, caniuse-lite@^1.0.30001088: - version "1.0.30001093" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001093.tgz#833e80f64b1a0455cbceed2a4a3baf19e4abd312" - integrity sha512-0+ODNoOjtWD5eS9aaIpf4K0gQqZfILNY4WSNuYzeT1sXni+lMrrVjc0odEobJt6wrODofDZUX8XYi/5y7+xl8g== + version "1.0.30001196" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001196.tgz" + integrity sha512-CPvObjD3ovWrNBaXlAIGWmg2gQQuJ5YhuciUOjPRox6hIQttu8O+b51dx6VIpIY9ESd2d0Vac1RKpICdG4rGUg== capture-exit@^2.0.0: version "2.0.0" From a5af12f7c042b1b9b440bf03437404859901bed8 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 5 Mar 2021 10:14:35 +0100 Subject: [PATCH 08/11] Prettify App.css --- src/webapp/components/app/App.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/webapp/components/app/App.css b/src/webapp/components/app/App.css index 1f9e2f59..fde50dbe 100644 --- a/src/webapp/components/app/App.css +++ b/src/webapp/components/app/App.css @@ -12,6 +12,7 @@ li { line-height: 1.75; } -table th, table td { - border: none !important +table th, +table td { + border: none !important; } From 8f3265a9f2ae6d537a359afa67edcf3f839a08a9 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Mon, 8 Mar 2021 09:06:31 +0100 Subject: [PATCH 09/11] Update README --- README.md | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 38050cc8..f1aa0165 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ ## Introduction -_d2-report_ provides the infrastructure to create DHIS2 reports with a React frontend. - -Those reports developed an an standard webapp, and they can both be used as an standalone DHIS2 webapp or an standard HTML report (App: Reports). - -Target DHIS2: 2.34. +_d2-reports_ than can be used as an standalone DHIS2 webapp or an standard HTML report (App: Reports). DHIS2 versions tested: 2.34. ## Reports @@ -15,12 +11,10 @@ This report shows data values for data sets `NHWA Module ...`. There are two kin 1. Data values that have comments. 2. Data values related pairs (value/comment), which are rendered as a single row. The pairing criteria is: - - Comment data element: `NHWA_Comment of Abc`. + - Comment data element `NHWA_Comment of Abc`. - Value data element: `NHWA_Abc`. -The API endpoint `/dataValueSets` does not provide all the features we need, so we use a custom SQL View instead. It will be included in the metadata. - -We use the data element group to put data elements in the same sections together. Note that only data elements belonging to a data element group will be displayed. +The API endpoint `/dataValueSets` does not provide all the features we need, so we use a custom SQL View instead. ## Initial setup @@ -30,7 +24,7 @@ $ yarn install ## Development -Start development server at `http://localhost:8082` using `https://play.dhis2.org/2.34` as backend: +Start the development server at `http://localhost:8082` using `https://play.dhis2.org/2.34` as a backend: ``` $ PORT=8082 REACT_APP_DHIS2_BASE_URL="https://play.dhis2.org/2.34" yarn start @@ -38,16 +32,16 @@ $ PORT=8082 REACT_APP_DHIS2_BASE_URL="https://play.dhis2.org/2.34" yarn start ## Deploy -Create standard report: +Create an standard report: ``` -$ yarn build-report # Create dist/index.html -$ yarn build-metadata -u 'user:password' http://dhis2-server.org # Creates dist/metadata.json -$ yarn post-metadata -u 'user:password' http://dhis2-server.org # Posts dist/metadata.json +$ yarn build-report # Creates dist/index.html +$ yarn build-metadata -u 'user:pass' http://dhis2-server.org # Creates dist/metadata.json +$ yarn post-metadata -u 'user:pass' http://dhis2-server.org # Posts dist/metadata.json ``` -Create web-app zip (`dist/d2-reports.zip`): +Create an standalone DHIS2 webapp app: ``` -$ yarn build-webapp +$ yarn build-webapp # Creates dist/d2-reports.zip ``` From 494a038dc29835546cec91709b516897fec9069a Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Mon, 8 Mar 2021 09:20:07 +0100 Subject: [PATCH 10/11] Show org unit name as title on button --- src/webapp/components/data-values-list/OrgUnitsFilter.tsx | 5 ++++- .../components/data-values-list/OrgUnitsFilterButton.tsx | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webapp/components/data-values-list/OrgUnitsFilter.tsx b/src/webapp/components/data-values-list/OrgUnitsFilter.tsx index c9dfc01b..9b3e1c2f 100644 --- a/src/webapp/components/data-values-list/OrgUnitsFilter.tsx +++ b/src/webapp/components/data-values-list/OrgUnitsFilter.tsx @@ -1,4 +1,5 @@ import React from "react"; +import _ from "lodash"; import { D2Api } from "../../../types/d2-api"; import { OrgUnitsSelector } from "d2-ui-components"; import { makeStyles } from "@material-ui/core"; @@ -17,7 +18,9 @@ const orgUnitsSelectorControls = {}; export const OrgUnitsFilter: React.FC = React.memo(props => { const { api, rootIds, selected, setSelected } = props; const classes = useStyles(); - const initiallyExpanded = React.useMemo(() => selected.map(getOrgUnitParentPath), [selected]); + const initiallyExpanded = React.useMemo(() => _.compact(selected.map(getOrgUnitParentPath)), [ + selected, + ]); return (
diff --git a/src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx b/src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx index 8bc80395..3f26cbdc 100644 --- a/src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx +++ b/src/webapp/components/data-values-list/OrgUnitsFilterButton.tsx @@ -26,6 +26,7 @@ export const OrgUnitsFilterButton: React.FC = React.m Date: Wed, 10 Mar 2021 14:53:20 +0100 Subject: [PATCH 11/11] version bumped --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2152d7b..ceeca243 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "d2-reports", "description": "DHIS2 Reports", - "version": "0.0.3", + "version": "0.0.4", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".",