From da9fbadbbfdfa571a07ba4a8ad1fbede6d99544d Mon Sep 17 00:00:00 2001 From: Mozafar Date: Wed, 6 Mar 2024 11:13:10 +0000 Subject: [PATCH] feat(DHIS2-16133): migrate events and tracker entities to new tracker API (#1951) BREAKING CHANGE: migrate to new API for tracker import/export. This expects a format incompatible with previous versions. * fix: loader keeps showing when opening export in new page we were assigning setting the state to an event in the new window. That reference doesn't work one outside the React app in the new window, so the loader never hides. Not sure if this pattern worked at some point, but even if it did, it was propbably disabled as a security issue * feat: migrate TEI and Event export to new tracker API * feat: move Event import to new tracker endpoints * feat: move TEI import to new tracker endpoints * refactor: change TEI references to Tracked entity * fix(migration-events): change start and endDate to occuuredBefore and occuredAfter * fix(migration-trackedentity): change lastUpdateStartDate to updatedAfter also change lastUpdateEndDate to updatedBefore, and lastUpdatedDuration to updatedWithin * fix(migration-trackedentity): change programStateDate to enrollmentEnrolledAfter and same for endDate to enrollmentEnrolledBefore * fix(migration-trackedentity): change followupStatus to followup and ensure that the parameter is not selected if ALL is chosen * fix: lint issues * fix: update tests snapshots * refactor: apply code review comments --- src/components/Inputs/AtomicMode.js | 2 +- src/components/Inputs/EndDate.js | 12 ++- src/components/Inputs/FollowUpStatus.js | 6 +- src/components/Inputs/LastUpdatedDuration.js | 2 +- src/components/Inputs/LastUpdatedEndDate.js | 2 +- src/components/Inputs/LastUpdatedStartDate.js | 2 +- src/components/Inputs/ProgramEndDate.js | 2 +- src/components/Inputs/ProgramStartDate.js | 2 +- src/components/Inputs/StartDate.js | 11 ++- src/components/Inputs/TEITypeFilter.js | 1 - .../JobSummary/SingleSummary/SingleSummary.js | 76 +++++++++++++-- src/components/JobSummary/Summary/Summary.js | 1 + .../__snapshots__/Summary.test.js.snap | 94 +------------------ src/components/Sidebar/Sidebar.js | 4 +- src/hooks/useTasks.js | 36 ++++++- src/pages/DataExport/form-helper.js | 3 +- src/pages/EventExport/EventExport.js | 15 +-- src/pages/EventExport/form-helper.js | 13 +-- src/pages/EventImport/EventImport.js | 8 +- src/pages/EventImport/form-helper.js | 15 +-- src/pages/Home/pages.js | 4 +- .../MetadataDependencyExport/form-helper.js | 3 +- src/pages/MetadataExport/form-helper.js | 3 +- src/pages/TEIExport/TEIExport.js | 27 +++--- src/pages/TEIExport/form-helper.js | 84 +++++++++-------- src/pages/TEIImport/TEIImport.js | 14 ++- src/pages/TEIImport/form-helper.js | 12 +-- src/utils/helper.js | 16 ++-- src/utils/tasks.js | 2 +- 29 files changed, 242 insertions(+), 230 deletions(-) diff --git a/src/components/Inputs/AtomicMode.js b/src/components/Inputs/AtomicMode.js index e0165a2f2..468061c8b 100644 --- a/src/components/Inputs/AtomicMode.js +++ b/src/components/Inputs/AtomicMode.js @@ -4,7 +4,7 @@ import { RadioGroupField } from '../index.js' const atomicModeOptions = [ { value: 'ALL', label: i18n.t('Do not import') }, - { value: 'NONE', label: i18n.t('Import') }, + { value: 'OBJECT', label: i18n.t('Import') }, ] const defaultAtomicModeOption = atomicModeOptions[0].value diff --git a/src/components/Inputs/EndDate.js b/src/components/Inputs/EndDate.js index 164f3b097..0ddd4656c 100644 --- a/src/components/Inputs/EndDate.js +++ b/src/components/Inputs/EndDate.js @@ -1,5 +1,6 @@ import i18n from '@dhis2/d2-i18n' import { hasValue, composeValidators } from '@dhis2/ui' +import PropTypes from 'prop-types' import React from 'react' import { DATE_VALIDATOR } from '../DatePicker/DatePickerField.js' import { DatePickerField } from '../index.js' @@ -9,13 +10,18 @@ const DATATEST = 'input-end-date' const LABEL = i18n.t('End date') const VALIDATOR = composeValidators(hasValue, DATE_VALIDATOR) -const EndDate = () => ( +const EndDate = ({ name, label }) => ( ) +EndDate.propTypes = { + label: PropTypes.string, + name: PropTypes.string, +} + export { EndDate } diff --git a/src/components/Inputs/FollowUpStatus.js b/src/components/Inputs/FollowUpStatus.js index d09234ab0..1fc2383d0 100644 --- a/src/components/Inputs/FollowUpStatus.js +++ b/src/components/Inputs/FollowUpStatus.js @@ -5,12 +5,12 @@ import { RadioGroupField } from '../index.js' const followUpStatusOptions = [ { value: 'ALL', label: i18n.t('All') }, - { value: 'TRUE', label: i18n.t('Marked for follow-up') }, - { value: 'FALSE', label: i18n.t('Not marked for follow-up') }, + { value: 'true', label: i18n.t('Marked for follow-up') }, + { value: 'false', label: i18n.t('Not marked for follow-up') }, ] const defaultFollowUpStatusOption = followUpStatusOptions[0].value -const NAME = 'followUpStatus' +const NAME = 'followup' const DATATEST = 'input-follow-up-status' const LABEL = i18n.t('Include only entities with follow-up status') diff --git a/src/components/Inputs/LastUpdatedDuration.js b/src/components/Inputs/LastUpdatedDuration.js index 08b24dd51..21fc074c3 100644 --- a/src/components/Inputs/LastUpdatedDuration.js +++ b/src/components/Inputs/LastUpdatedDuration.js @@ -5,7 +5,7 @@ import React from 'react' import { DURATION_VALIDATOR } from '../Duration/DurationField.js' import { DurationField } from '../index.js' -const NAME = 'lastUpdatedDuration' +const NAME = 'updatedWithin' const DATATEST = 'input-last-updated-duration' const LABEL = i18n.t('Last updated duration') const VALIDATOR = composeValidators(hasValue, DURATION_VALIDATOR) diff --git a/src/components/Inputs/LastUpdatedEndDate.js b/src/components/Inputs/LastUpdatedEndDate.js index f765fc732..0bd558b66 100644 --- a/src/components/Inputs/LastUpdatedEndDate.js +++ b/src/components/Inputs/LastUpdatedEndDate.js @@ -5,7 +5,7 @@ import React from 'react' import { OPTIONAL_DATE_VALIDATOR } from '../DatePicker/DatePickerField.js' import { DatePickerField } from '../index.js' -const NAME = 'lastUpdatedEndDate' +const NAME = 'updatedBefore' const DATATEST = 'input-last-updated-end-date' const LABEL = i18n.t('Last updated end date') const VALIDATOR = composeValidators(OPTIONAL_DATE_VALIDATOR) diff --git a/src/components/Inputs/LastUpdatedStartDate.js b/src/components/Inputs/LastUpdatedStartDate.js index 9a99ae208..d38735480 100644 --- a/src/components/Inputs/LastUpdatedStartDate.js +++ b/src/components/Inputs/LastUpdatedStartDate.js @@ -5,7 +5,7 @@ import React from 'react' import { OPTIONAL_DATE_VALIDATOR } from '../DatePicker/DatePickerField.js' import { DatePickerField } from '../index.js' -const NAME = 'lastUpdatedStartDate' +const NAME = 'updatedAfter' const DATATEST = 'input-last-updated-start-date' const LABEL = i18n.t('Last updated start date') const VALIDATOR = composeValidators(OPTIONAL_DATE_VALIDATOR) diff --git a/src/components/Inputs/ProgramEndDate.js b/src/components/Inputs/ProgramEndDate.js index e2f65738d..f1dcd137a 100644 --- a/src/components/Inputs/ProgramEndDate.js +++ b/src/components/Inputs/ProgramEndDate.js @@ -5,7 +5,7 @@ import React from 'react' import { OPTIONAL_DATE_VALIDATOR } from '../DatePicker/DatePickerField.js' import { DatePickerField } from '../index.js' -const NAME = 'programEndDate' +const NAME = 'enrollmentEnrolledBefore' const DATATEST = 'input-program-end-date' const LABEL = i18n.t('End date') const VALIDATOR = composeValidators(OPTIONAL_DATE_VALIDATOR) diff --git a/src/components/Inputs/ProgramStartDate.js b/src/components/Inputs/ProgramStartDate.js index 35e035fb8..7757048c4 100644 --- a/src/components/Inputs/ProgramStartDate.js +++ b/src/components/Inputs/ProgramStartDate.js @@ -5,7 +5,7 @@ import React from 'react' import { OPTIONAL_DATE_VALIDATOR } from '../DatePicker/DatePickerField.js' import { DatePickerField } from '../index.js' -const NAME = 'programStartDate' +const NAME = 'enrollmentEnrolledAfter' const DATATEST = 'input-program-start-date' const LABEL = i18n.t('Start date') const VALIDATOR = composeValidators(OPTIONAL_DATE_VALIDATOR) diff --git a/src/components/Inputs/StartDate.js b/src/components/Inputs/StartDate.js index 8b77968f8..23c41904e 100644 --- a/src/components/Inputs/StartDate.js +++ b/src/components/Inputs/StartDate.js @@ -1,5 +1,6 @@ import i18n from '@dhis2/d2-i18n' import { hasValue, composeValidators } from '@dhis2/ui' +import PropTypes from 'prop-types' import React from 'react' import { DATE_VALIDATOR } from '../DatePicker/DatePickerField.js' import { DatePickerField } from '../index.js' @@ -9,13 +10,17 @@ const DATATEST = 'input-start-date' const LABEL = i18n.t('Start date') const VALIDATOR = composeValidators(hasValue, DATE_VALIDATOR) -const StartDate = () => ( +const StartDate = ({ name, label }) => ( ) +StartDate.propTypes = { + label: PropTypes.string, + name: PropTypes.string, +} export { StartDate } diff --git a/src/components/Inputs/TEITypeFilter.js b/src/components/Inputs/TEITypeFilter.js index f440f0478..822e9956f 100644 --- a/src/components/Inputs/TEITypeFilter.js +++ b/src/components/Inputs/TEITypeFilter.js @@ -3,7 +3,6 @@ import React from 'react' import { RadioGroupField } from '../index.js' const teiTypeFilterOptions = [ - { value: 'NONE', label: i18n.t('None') }, { value: 'PROGRAM', label: i18n.t('Program') }, { value: 'TE', label: i18n.t('Tracked entity type') }, ] diff --git a/src/components/JobSummary/SingleSummary/SingleSummary.js b/src/components/JobSummary/SingleSummary/SingleSummary.js index f54ae56cd..001bf54d1 100644 --- a/src/components/JobSummary/SingleSummary/SingleSummary.js +++ b/src/components/JobSummary/SingleSummary/SingleSummary.js @@ -20,6 +20,7 @@ const SingleSummary = ({ status, description, conflicts, + validationReport, id, }) => (
@@ -29,7 +30,7 @@ const SingleSummary = ({ name="summary" > <> - {status && ( + {status && description && ( - {importCount.imported} - {importCount.deleted} - {importCount.ignored} - {importCount.updated} - {importCount.total} + + {importCount?.imported ?? '0'} + + {importCount?.deleted} + {importCount?.ignored} + {importCount?.updated} + {importCount?.total} + {!!validationReport?.errorReports?.length && ( + + + + + {i18n.t('UID')} + + {i18n.t('Error Code')} + + {i18n.t('Message')} + + {i18n.t('Tracker Type')} + + {/* {i18n.t('')} */} + + + + {validationReport.errorReports.map((c, i) => ( + + {c.uid} + + + {c.warningCode ?? c.errorCode} + + + {c.message} + {c.trackerType} + + ))} + +
+
+ )} {conflicts && ( { importCount={importCount} status={summary.status} description={summary.description} + validationReport={summary.validationReport} conflicts={ summary.conflicts && (summary.conflicts.length || null) && diff --git a/src/components/JobSummary/__test__/__snapshots__/Summary.test.js.snap b/src/components/JobSummary/__test__/__snapshots__/Summary.test.js.snap index 08e40e253..92bad399b 100644 --- a/src/components/JobSummary/__test__/__snapshots__/Summary.test.js.snap +++ b/src/components/JobSummary/__test__/__snapshots__/Summary.test.js.snap @@ -172,51 +172,6 @@ exports[`different job type summaries matches snapshot - EVENT_IMPORT 1`] = ` > Summary - - - - - - - - - - - - -
- Status - - Description -
- ERROR - -
Summary -
- - - - - - - - - - - -
- Status - - Description -
- OK - -
+ > + 0 +
, } const teiImportPage = { - name: i18n.t('TEI import'), + name: i18n.t('Tracked entity import'), code: 'tei-import', path: '/import/tei', icon: , @@ -88,7 +88,7 @@ const metadataExportPage = { } const teiExportPage = { - name: i18n.t('TEI export'), + name: i18n.t('Tracked entity export'), code: 'tei-export', path: '/export/tei', icon: , diff --git a/src/hooks/useTasks.js b/src/hooks/useTasks.js index 7dfcc606c..7b52c2794 100644 --- a/src/hooks/useTasks.js +++ b/src/hooks/useTasks.js @@ -16,6 +16,20 @@ const jobSummaryQuery = { }, } +const trackerEventQuery = { + events: { + resource: 'tracker/jobs/', + id: ({ taskId }) => `${taskId}`, + }, +} + +const trackerSummaryQuery = { + summary: { + resource: 'tracker/jobs/', + id: ({ taskId }) => `${taskId}/report`, + }, +} + const defaultTasks = { data: {}, event: {}, @@ -39,13 +53,20 @@ const createFetchEvents = } const newTask = { ...task } - const { events, error } = await engine.query(jobEventQuery, { + const query = + task.importType === 'TRACKER_IMPORT_JOB' + ? trackerEventQuery + : jobEventQuery + + const response = await engine.query(query, { variables: { type: task.importType, taskId: task.id, }, }) + const { events, error } = response + if (error) { console.error('fetchEvents error: ', error) return @@ -90,13 +111,24 @@ const createFetchEvents = const createFetchSummary = (engine, setTasks) => async (type, id, task) => { const newTask = { ...task } - const { summary, error } = await engine.query(jobSummaryQuery, { + // we could still keep one query here (the jobs query), but tracker provides a facade to these + // and even though this branches the logic unnecessarily, we should stick to + // trackers' endpoint for tracker imports and they could abstract some job-related details + // more details here: https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/tracker.html#webapi_nti_import_summary + const query = + task.importType === 'TRACKER_IMPORT_JOB' + ? trackerSummaryQuery + : jobSummaryQuery + + const response = await engine.query(query, { variables: { type: task.importType, taskId: task.id, }, }) + const { summary, error } = response + if (error) { console.error('fetchSummary error: ', error) return diff --git a/src/pages/DataExport/form-helper.js b/src/pages/DataExport/form-helper.js index 03c350735..216b7793d 100644 --- a/src/pages/DataExport/form-helper.js +++ b/src/pages/DataExport/form-helper.js @@ -56,7 +56,8 @@ const onExport = (baseUrl, setExportEnabled) => async (values) => { const filename = `${endpoint}.${fileExtension}` const downloadUrlParams = valuesToParams(values, filename) const url = `${apiBaseUrl}${endpoint}?${downloadUrlParams}` - locationAssign(url, setExportEnabled) + locationAssign(url) + setExportEnabled(true) // log for debugging purposes console.log('data-export:', { url, params: downloadUrlParams }) diff --git a/src/pages/EventExport/EventExport.js b/src/pages/EventExport/EventExport.js index e88f1fef5..eb1d8e860 100644 --- a/src/pages/EventExport/EventExport.js +++ b/src/pages/EventExport/EventExport.js @@ -14,7 +14,6 @@ import { OrgUnitTree, ProgramPicker, Format, - formatOptions, defaultFormatOption, Compression, defaultCompressionOption, @@ -32,6 +31,7 @@ import { defaultOrgUnitIdSchemeOption, IdScheme, defaultIdSchemeOption, + formatNoXmlOptions, } from '../../components/Inputs/index.js' import { jsDateToISO8601 } from '../../utils/helper.js' import { onExport, validate } from './form-helper.js' @@ -41,7 +41,7 @@ const { Form } = ReactFinalForm // PAGE INFO export const PAGE_NAME = i18n.t('Event export') export const PAGE_DESCRIPTION = i18n.t( - 'Export event data for programs, stages and tracked entities to JSON, CSV, or DXF2 format.' + 'Export event data for programs, stages and tracked entities to JSON or CSV format.' ) const PAGE_ICON = @@ -58,13 +58,14 @@ const initialValues = { programStage: undefined, format: defaultFormatOption, compression: defaultCompressionOption, - startDate: jsDateToISO8601(threeMonthsBeforeToday), - endDate: jsDateToISO8601(today), + occurredAfter: jsDateToISO8601(threeMonthsBeforeToday), + occurredBefore: jsDateToISO8601(today), includeDeleted: false, dataElementIdScheme: defaultDataElementIdSchemeOption, orgUnitIdScheme: defaultOrgUnitIdSchemeOption, idScheme: defaultIdSchemeOption, inclusion: defaultInclusionOption, + skipPaging: true, } const EventExport = () => { @@ -100,10 +101,10 @@ const EventExport = () => { - - + + - + diff --git a/src/pages/EventExport/form-helper.js b/src/pages/EventExport/form-helper.js index bdecb8ff4..898d104c4 100644 --- a/src/pages/EventExport/form-helper.js +++ b/src/pages/EventExport/form-helper.js @@ -14,8 +14,8 @@ const onExport = (baseUrl, setExportEnabled) => (values) => { programStage, format, compression, - startDate, - endDate, + occurredAfter, + occurredBefore, includeDeleted, dataElementIdScheme, orgUnitIdScheme, @@ -24,7 +24,7 @@ const onExport = (baseUrl, setExportEnabled) => (values) => { } = values // generate URL and redirect - const apiBaseUrl = `${baseUrl}/api/` + const apiBaseUrl = `${baseUrl}/api/tracker/` const endpoint = `events` const endpointExtension = compression ? `${format}.${compression}` : format const filename = `${endpoint}.${endpointExtension}` @@ -38,8 +38,8 @@ const onExport = (baseUrl, setExportEnabled) => (values) => { `orgUnitIdScheme=${orgUnitIdScheme}`, `idScheme=${idScheme}`, `attachment=${filename}`, - `startDate=${startDate}`, - `endDate=${endDate}`, + `occurredAfter=${occurredAfter}`, + `occurredBefore=${occurredBefore}`, `ouMode=${inclusion}`, `format=${format}`, programStage != ALL_VALUE ? `programStage=${programStage}` : '', @@ -47,7 +47,8 @@ const onExport = (baseUrl, setExportEnabled) => (values) => { .filter((s) => s != '') .join('&') const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}` - locationAssign(url, setExportEnabled) + locationAssign(url) + setExportEnabled(true) // log for debugging purposes console.log('event-export:', { url, params: downloadUrlParams }) diff --git a/src/pages/EventImport/EventImport.js b/src/pages/EventImport/EventImport.js index 4f9e867f9..ef0c66139 100644 --- a/src/pages/EventImport/EventImport.js +++ b/src/pages/EventImport/EventImport.js @@ -14,7 +14,6 @@ import { import { FileUpload, Format, - formatOptions, defaultFormatOption, DataElementIdScheme, defaultDataElementIdSchemeOption, @@ -26,6 +25,7 @@ import { defaultOrgUnitIdSchemeOption, ImportButtonStrip, FormAlerts, + formatNoXmlOptions, } from '../../components/Inputs/index.js' import { TaskContext, getNewestTask } from '../../contexts/index.js' import { getPrevJobDetails } from '../../utils/helper.js' @@ -34,7 +34,7 @@ import { onImport } from './form-helper.js' // PAGE INFO export const PAGE_NAME = i18n.t('Event import') export const PAGE_DESCRIPTION = i18n.t( - 'Import event data for programs, stages and tracked entities to JSON, CSV, or DXF2 format.' + 'Import event data for programs, stages and tracked entities from JSON or CSV format.' ) const PAGE_ICON = @@ -95,14 +95,14 @@ const EventImport = () => { ', } )} /> diff --git a/src/pages/EventImport/form-helper.js b/src/pages/EventImport/form-helper.js index af0ce5664..5dd6c3a1f 100644 --- a/src/pages/EventImport/form-helper.js +++ b/src/pages/EventImport/form-helper.js @@ -12,32 +12,27 @@ const onImport = format, dataElementIdScheme, orgUnitIdScheme, - eventIdScheme, idScheme, } = values // send xhr - const apiBaseUrl = `${baseUrl}/api/` - const endpoint = 'events.json' + const apiBaseUrl = `${baseUrl}/api/tracker` const params = [ - 'skipFirst=true', `async=${isAsync}`, - `dryRun=${dryRun}`, + `importMode=${dryRun ? 'validate' : 'commit'}`, `dataElementIdScheme=${dataElementIdScheme}`, `orgUnitIdScheme=${orgUnitIdScheme}`, - `eventIdScheme=${eventIdScheme}`, `idScheme=${idScheme}`, - `payloadFormat=${format}`, ].join('&') - const url = `${apiBaseUrl}${endpoint}?${params}` + const url = `${apiBaseUrl}?${params}` try { await uploadFile({ url, file: files[0], format: format, - type: 'EVENT_IMPORT', - isAsync: isAsync, + type: 'TRACKER_IMPORT_JOB', + isAsync, setProgress, addEntry: (id, entry) => addTask('event', id, { ...entry, jobDetails: values }), diff --git a/src/pages/Home/pages.js b/src/pages/Home/pages.js index fbacd067a..dd60f360d 100644 --- a/src/pages/Home/pages.js +++ b/src/pages/Home/pages.js @@ -81,7 +81,7 @@ const exportPages = capitalizePages([ { name: TEI_EXPORT_PAGE_NAME, description: TEI_EXPORT_DESCRIPTION, - linkText: i18n.t('Export tracked entity instances'), + linkText: i18n.t('Export tracked entities'), to: '/export/tei', }, ]) @@ -120,7 +120,7 @@ const importPages = capitalizePages([ { name: TEI_IMPORT_PAGE_NAME, description: TEI_IMPORT_DESCRIPTION, - linkText: i18n.t('Import tracked entity instances'), + linkText: i18n.t('Import tracked entities'), to: '/import/tei', }, ]) diff --git a/src/pages/MetadataDependencyExport/form-helper.js b/src/pages/MetadataDependencyExport/form-helper.js index ee5e77dca..ca7dc2526 100644 --- a/src/pages/MetadataDependencyExport/form-helper.js +++ b/src/pages/MetadataDependencyExport/form-helper.js @@ -11,7 +11,8 @@ const onExport = (baseUrl, setExportEnabled) => (values) => { const endpointExtension = compression ? `${format}.${compression}` : format const downloadUrlParams = `skipSharing=${skipSharing}&download=true` const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}` - locationAssign(url, setExportEnabled) + locationAssign(url) + setExportEnabled(true) // log for debugging purposes console.log('metadata-dependency-export:', { diff --git a/src/pages/MetadataExport/form-helper.js b/src/pages/MetadataExport/form-helper.js index 879f3889a..63a1cbe0b 100644 --- a/src/pages/MetadataExport/form-helper.js +++ b/src/pages/MetadataExport/form-helper.js @@ -12,7 +12,8 @@ const onExport = (baseUrl, setExportEnabled) => (values) => { const schemaParams = checkedSchemas.map((name) => `${name}=true`).join('&') const downloadUrlParams = `skipSharing=${skipSharing}&download=true&${schemaParams}` const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}` - locationAssign(url, setExportEnabled) + locationAssign(url) + setExportEnabled(true) // log for debugging purposes console.log('metadata-export:', { url, params: downloadUrlParams }) diff --git a/src/pages/TEIExport/TEIExport.js b/src/pages/TEIExport/TEIExport.js index c870c13b9..2acefc9df 100644 --- a/src/pages/TEIExport/TEIExport.js +++ b/src/pages/TEIExport/TEIExport.js @@ -12,7 +12,6 @@ import { } from '../../components/index.js' import { Format, - formatOptions, defaultFormatOption, OrgUnitMode, defaultOrgUnitSelectionModeOption, @@ -36,7 +35,6 @@ import { AssignedUserMode, defaultAssignedUserModeOption, IncludeDeleted, - IncludeAllAttributes, DataElementIdScheme, defaultDataElementIdSchemeOption, EventIdScheme, @@ -47,15 +45,16 @@ import { defaultOrgUnitIdSchemeOption, ExportButton, FormAlerts, + formatNoXmlOptions, } from '../../components/Inputs/index.js' import { onExport, validate } from './form-helper.js' const { Form } = ReactFinalForm // PAGE INFO -export const PAGE_NAME = i18n.t('Tracked entity instances export') +export const PAGE_NAME = i18n.t('Tracked entities export') export const PAGE_DESCRIPTION = i18n.t( - 'Export tracked entity instances in JSON, CSV, or DXF2 format.' + 'Export tracked entities in JSON or CSV format.' ) const PAGE_ICON = @@ -69,18 +68,17 @@ const initialValues = { inclusion: defaultInclusionOption, teiTypeFilter: defaultTEITypeFilterOption, programStatus: defaultProgramStatusOption, - followUpStatus: defaultFollowUpStatusOption, - programStartDate: '', - programEndDate: '', + followup: defaultFollowUpStatusOption, + enrollmentEnrolledAfter: '', + enrollmentEnrolledBefore: '', compression: '', // disable compression until it is properly implemented in the backend lastUpdatedFilter: defaultLastUpdatedFilterOption, - lastUpdatedStartDate: '', - lastUpdatedEndDate: '', - lastUpdatedDuration: '', + updatedAfter: '', + updatedBefore: '', + updatedWithin: '', assignedUserModeFilter: false, assignedUserMode: defaultAssignedUserModeOption, includeDeleted: false, - includeAllAttributes: false, dataElementIdScheme: defaultDataElementIdSchemeOption, eventIdScheme: defaultEventIdSchemeOption, orgUnitIdScheme: defaultOrgUnitIdSchemeOption, @@ -136,7 +134,7 @@ const TEIExport = () => { - + @@ -147,7 +145,6 @@ const TEIExport = () => { - @@ -157,9 +154,7 @@ const TEIExport = () => { diff --git a/src/pages/TEIExport/form-helper.js b/src/pages/TEIExport/form-helper.js index 5f19cb647..8c9ba7e21 100644 --- a/src/pages/TEIExport/form-helper.js +++ b/src/pages/TEIExport/form-helper.js @@ -17,7 +17,6 @@ const valuesToParams = ( inclusion, format, includeDeleted, - includeAllAttributes, dataElementIdScheme, eventIdScheme, orgUnitIdScheme, @@ -26,13 +25,13 @@ const valuesToParams = ( assignedUserMode, teiTypeFilter, programStatus, - followUpStatus, - programStartDate, - programEndDate, + followup, + enrollmentEnrolledAfter, + enrollmentEnrolledBefore, lastUpdatedFilter, - lastUpdatedStartDate, - lastUpdatedEndDate, - lastUpdatedDuration, + updatedAfter, + updatedBefore, + updatedWithin, }, filename ) => { @@ -40,18 +39,18 @@ const valuesToParams = ( ouMode: ouMode, format: format, includeDeleted: includeDeleted.toString(), - includeAllAttributes: includeAllAttributes.toString(), dataElementIdScheme: dataElementIdScheme, eventIdScheme: eventIdScheme, orgUnitIdScheme: orgUnitIdScheme, idScheme: idScheme, attachment: filename, + skipPaging: true, } // include selected org.units only when manual selection is selected // ouMode is then stored in the `inclusion` field if (ouMode === OU_MODE_MANUAL_VALUE) { - minParams.ou = selectedOrgUnits.map((o) => pathToId(o)).join(';') + minParams.orgUnit = selectedOrgUnits.map((o) => pathToId(o)).join(';') minParams.ouMode = inclusion } @@ -71,14 +70,16 @@ const valuesToParams = ( minParams.programStatus = programStatus } - minParams.followUpStatus = followUpStatus + if (followup !== 'ALL') { + minParams.followup = followup + } - if (programStartDate) { - minParams.programStartDate = programStartDate + if (enrollmentEnrolledAfter) { + minParams.enrollmentEnrolledAfter = enrollmentEnrolledAfter } - if (programEndDate) { - minParams.programEndDate = programEndDate + if (enrollmentEnrolledBefore) { + minParams.enrollmentEnrolledBefore = enrollmentEnrolledBefore } } @@ -87,17 +88,17 @@ const valuesToParams = ( } if (lastUpdatedFilter == 'DATE') { - if (lastUpdatedStartDate) { - minParams.lastUpdatedStartDate = lastUpdatedStartDate + if (updatedAfter) { + minParams.updatedAfter = updatedAfter } - if (lastUpdatedEndDate) { - minParams.lastUpdatedEndDate = lastUpdatedEndDate + if (updatedBefore) { + minParams.updatedBefore = updatedBefore } } if (lastUpdatedFilter == 'DURATION') { - minParams.lastUpdatedDuration = lastUpdatedDuration + minParams.updatedWithin = updatedWithin } return Object.keys(minParams) @@ -111,12 +112,13 @@ const onExport = (baseUrl, setExportEnabled) => async (values) => { const { format } = values // generate URL and redirect - const apiBaseUrl = `${baseUrl}/api/` - const endpoint = `trackedEntityInstances` + const apiBaseUrl = `${baseUrl}/api/tracker/` + const endpoint = `trackedEntities` const filename = `${endpoint}.${format}` const downloadUrlParams = valuesToParams(values, filename) const url = `${apiBaseUrl}${endpoint}.${format}?${downloadUrlParams}` - locationAssign(url, setExportEnabled) + locationAssign(url) + setExportEnabled(true) // log for debugging purposes console.log('tei-export:', { url, params: downloadUrlParams }) @@ -127,40 +129,40 @@ const validate = (values) => { if ( values.teiTypeFilter == 'PROGRAM' && - values.programStartDate && - values.programEndDate + values.enrollmentEnrolledAfter && + values.enrollmentEnrolledBefore ) { - errors.programStartDate = DATE_BEFORE_VALIDATOR( - values.programStartDate, - values.programEndDate + errors.enrollmentEnrolledAfter = DATE_BEFORE_VALIDATOR( + values.enrollmentEnrolledAfter, + values.enrollmentEnrolledBefore ) - errors.programEndDate = DATE_AFTER_VALIDATOR( - values.programEndDate, - values.programStartDate + errors.enrollmentEnrolledBefore = DATE_AFTER_VALIDATOR( + values.enrollmentEnrolledBefore, + values.enrollmentEnrolledAfter ) } if ( values.lastUpdatedFilter == 'DATE' && - values.lastUpdatedStartDate && - values.lastUpdatedEndDate + values.updatedAfter && + values.updatedBefore ) { - errors.lastUpdatedStartDate = DATE_BEFORE_VALIDATOR( - values.lastUpdatedStartDate, - values.lastUpdatedEndDate + errors.updatedAfter = DATE_BEFORE_VALIDATOR( + values.updatedAfter, + values.updatedBefore ) - errors.lastUpdatedEndDate = DATE_AFTER_VALIDATOR( - values.lastUpdatedEndDate, - values.lastUpdatedStartDate + errors.updatedBefore = DATE_AFTER_VALIDATOR( + values.updatedBefore, + values.updatedAfter ) } if ( values.lastUpdatedFilter == 'DATE' && - !values.lastUpdatedStartDate && - !values.lastUpdatedEndDate + !values.updatedAfter && + !values.updatedBefore ) { - errors.lastUpdatedEndDate = i18n.t( + errors.updatedBefore = i18n.t( "At least one of the 'last updated' date fields must be specified" ) } diff --git a/src/pages/TEIImport/TEIImport.js b/src/pages/TEIImport/TEIImport.js index 3daed9cf9..aeb5c11d5 100644 --- a/src/pages/TEIImport/TEIImport.js +++ b/src/pages/TEIImport/TEIImport.js @@ -14,7 +14,6 @@ import { import { FileUpload, Format, - formatNoCsvOptions, defaultFormatOption, Identifier, defaultIdentifierOption, @@ -46,6 +45,7 @@ import { defaultIdSchemeOption, OrgUnitIdScheme, defaultOrgUnitIdSchemeOption, + formatNoXmlNoCsvOptions, } from '../../components/Inputs/index.js' import { TaskContext, getNewestTask } from '../../contexts/index.js' import { getPrevJobDetails, getInitialBoolValue } from '../../utils/helper.js' @@ -54,9 +54,9 @@ import { onImport } from './form-helper.js' const { Form } = ReactFinalForm // PAGE INFO -export const PAGE_NAME = i18n.t('Tracked entity instances import') +export const PAGE_NAME = i18n.t('Tracked entities import') export const PAGE_DESCRIPTION = i18n.t( - 'Import tracked entity instances using JSON or DXF2 format.' + 'Import tracked entities using JSON format.' ) const PAGE_ICON = @@ -81,9 +81,7 @@ const createInitialValues = (prevJobDetails) => ({ prevJobDetails.skipValidation, defaultSkipValidationOption ), - // disable async until it is fully implemented for this resource - // (expected 2.36) - isAsync: false, + isAsync: true, dataElementIdScheme: prevJobDetails.dataElementIdScheme || defaultDataElementIdSchemeOption, orgUnitIdScheme: @@ -136,14 +134,14 @@ const TEIImport = () => { ', } )} /> diff --git a/src/pages/TEIImport/form-helper.js b/src/pages/TEIImport/form-helper.js index d7386cbb0..ed3b60cba 100644 --- a/src/pages/TEIImport/form-helper.js +++ b/src/pages/TEIImport/form-helper.js @@ -26,8 +26,7 @@ const onImport = } = values // send xhr - const apiBaseUrl = `${baseUrl}/api/` - const endpoint = 'trackedEntityInstances.json' + const apiBaseUrl = `${baseUrl}/api/tracker/` const params = [ `importMode=${dryRun ? 'VALIDATE' : 'COMMIT'}`, `identifier=${identifier}`, @@ -49,18 +48,19 @@ const onImport = ] .filter((s) => s != '') .join('&') - const url = `${apiBaseUrl}${endpoint}?${params}` + const url = `${apiBaseUrl}?${params}` try { await uploadFile({ url, file: files[0], format: format, - type: 'TEI_IMPORT', + type: 'TRACKER_IMPORT_JOB', isAsync: isAsync, setProgress, - addEntry: (id, entry) => - addTask('tei', id, { ...entry, jobDetails: values }), + addEntry: (id, entry) => { + addTask('tei', id, { ...entry, jobDetails: values }) + }, }) return jobStartedMessage } catch (e) { diff --git a/src/utils/helper.js b/src/utils/helper.js index 5db84c3f2..853274c24 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -101,7 +101,8 @@ const uploadFile = ({ url, upload: file, type, - onResponse: ({ error, id, msg, typeReports }) => { + onResponse: (response) => { + const { error, id, msg, typeReports } = response let entry if (!isAsync) { // we are done @@ -149,12 +150,14 @@ const uploadFile = ({ created: new Date(), lastUpdated: new Date(), completed: false, - events: [msg], + events: [{ ...msg, date: new Date() }], // this is a workaround for the initial message date coming as invalid + summary: undefined, error: false, importType: type, } } + addEntry(entry.id, entry) if (error) { @@ -169,7 +172,7 @@ const uploadFile = ({ const response = JSON.parse(ev.target.response) message = response.message } catch (e2) { - message = genericErrorMessage + message = ev } console.error('sendFile error', message) reject(errF(message)) @@ -193,7 +196,7 @@ const downloadWindowHtml = ` ` // call stub function if available -const locationAssign = (url, setExportEnabled) => { +const locationAssign = (url) => { if (window.locationAssign) { window.locationAssign(url) } else { @@ -201,11 +204,6 @@ const locationAssign = (url, setExportEnabled) => { downloadWindow.document.title = downloadWindowTitle downloadWindow.document.body.innerHTML = downloadWindowHtml // does not work in Chrome - - const enableExport = () => setExportEnabled(true) - downloadWindow.onbeforeunload = enableExport - downloadWindow.onabort = enableExport - downloadWindow.onerror = enableExport } } diff --git a/src/utils/tasks.js b/src/utils/tasks.js index b68ca447f..1179012a4 100644 --- a/src/utils/tasks.js +++ b/src/utils/tasks.js @@ -43,7 +43,7 @@ const categoryTypes = [ key: 'tei', importType: 'TEI_IMPORT', icon: , - label: i18n.t('TEI'), + label: i18n.t('Tracked entity'), }, ]