[Feature]: regenerate-metadata CLI (refresh Metadata sheet from DHIS2) - #409
Conversation
Per-template flag (default false) that adds a "Code" column after "Metadata version" in the generated Metadata sheet, populated with item.code. Additive: existing Metadata columns and the =_<id> defined names are unchanged when off. Enables looking up option codes by name from the Metadata sheet (e.g. for building composite identifiers). Mirrors the generateMetadata wiring: entity -> TemplateWebRepository -> DownloadTemplateUseCase -> SheetBuilder. Adds the first SheetBuilder unit test covering the column on/off behaviour.
Regenerate only the Metadata sheet of a custom template from fresh DHIS2 metadata (with the Code column), leaving every other sheet — custom form, dropdowns, VBA — untouched, so it can be refreshed without rebuilding the whole template. - SheetBuilder.generateMetadataOnly(): loads the input workbook, clears the Metadata sheet IN PLACE (not delete, so its codeName/VBA binding survives) and refills it via fillMetadataSheet with includeMetadataCodes on. - Workbook.clear() and a cross-environment base64 loader/writer (fromBase64Data / writeToBase64) that work in both Node and the browser. - RegenerateTemplateMetadataUseCase wired in CompositionRoot; reusable from the web app. - src/scripts/regenerate-metadata.ts CLI (cmd-ts), same pattern as import-multiple-files.ts. - Unit test covering the refresh + preservation of other sheets.
Bakes in the ts-node --transpile-only --project src/scripts/tsconfig.json flags so the script runs with: yarn regenerate-metadata --dhis2-url ... --input ... --output ... --form-id ...
Expose the three SheetBuilder flags that affect Metadata-sheet output as CLI flags on regenerate-metadata, instead of hardcoding them in the use case: --include-codes (Code column), --use-codes (codes instead of names), --org-unit-short-name (short vs regular OU name). All default off. Other SheetBuilder params only affect full generate(), not metadata-only.
anagperal
left a comment
There was a problem hiding this comment.
thanks @gqcorneby ! Onle a few comments
| console.debug(`Regenerating Metadata for ${args.input} (form ${args.formId})`); | ||
|
|
||
| const outputBase64 = await compositionRoot.templates.regenerateMetadata(api, { | ||
| type: args.formType as DataFormType, |
There was a problem hiding this comment.
Instead of as you can declare:
formType: option({
type: oneOf(dataFormTypes),
long: "form-type",
defaultValue: () => dataFormTypeMap.trackerPrograms,
description: "Data form type: trackerPrograms | programs | dataSets (default: trackerPrograms)",
})
Use cmd-ts oneOf(dataFormTypes) so --form-type is validated at parse time and the DataFormType cast at the call site disappears. Also document why the use case imports Settings and SheetBuilder from webapp/logic; fixing the layering needs a wider refactor.
The Code column is written for org units too, but the fixture passed an empty organisationUnits list so that branch was never exercised. Addresses review feedback on #408.
Buffer does not exist in the browser bundle, so the helper threw "Buffer is not defined" for any caller outside Node. It had no callers until now, which is why this went unnoticed.
Shared by more than one use case now, so keep a single definition.
The import page's drop area is now a component, so other screens can reuse it instead of copying the render prop and its styles.
Identifies the template and its data form from the file alone, so a caller does not have to ask the user which template it is. AnalyzeTemplateUseCase resolves the same way but then reads every data value, which is unnecessary when only the template is wanted.
Adds a Regenerate template metadata row to the Maintenance list. The dropped file identifies the template, the generation flags are editable, and the regenerated workbook is downloaded; nothing is persisted. Until now this was only possible through the regenerate-metadata CLI, which needs a checkout and DHIS2 credentials on the command line. The Maintenance section moves out of SettingsFields into its own component to host the new row.
|
Thanks @anagperal! Addressed the comments. The scope of the PR increased a bit 😅 After discussing with Miquel, we'll expose this regenerate in settings page as a maintenance function.
|
…rate-metadata # Conflicts: # i18n/en.pot # i18n/es.po # i18n/fr.po # i18n/pt.po # i18n/ru.po # src/test/sheetBuilder.spec.ts # src/webapp/logic/sheetBuilder.ts
BundleMonNo change in files bundle size Groups updated (1)
Final result: ✅ View report in BundleMon website ➡️ |
anagperal
left a comment
There was a problem hiding this comment.
thanks @gqcorneby ! a lot of work done here! Just a few comments
| const loading = useLoading(); | ||
| const classes = useStyles(); | ||
|
|
||
| const [resolution, setResolution] = useState<ResolutionState>({ type: "empty" }); |
There was a problem hiding this comment.
This component has also the logic presentation here, move it to a custom hook to have only the render logic
| private excelRepository: ExcelRepository | ||
| ) {} | ||
|
|
||
| public async execute(file: File): Promise<ResolvedTemplate> { |
There was a problem hiding this comment.
This method shares some code with AnalyzeTemplateUseCase. Maybe you can extract it to a helper
There was a problem hiding this comment.
I ruled out reusing AnalyzeTemplateUseCase itself but I missed the shared code. Thanks Ana!
AnalyzeTemplateUseCase and ResolveTemplateFromFileUseCase opened with the same load-template and resolve-data-form steps. Kept as two helpers rather than one so the resolve use case can wrap only the lookup in its friendly error, leaving the data form errors distinct. Neither helper handles errors, so both use cases surface the same messages as before. Addresses review feedback on #409.
The dialog is now render-only; the drop handling, flags, regeneration and dismissal guard live in useRegenerateMetadata, following the shape of useMaintenanceCleanup. Addresses review feedback on #409.
Moving strings between files reorders the extraction; msgmerge also re-fuzzed one entry onto an unrelated translation, cleared here so it falls back to English.

📌 References
📝 Implementation
Add a
regenerate-metadatause case + CLI script that refreshes only the Metadata sheet of an existing template from fresh DHIS2 metadata, leaving every other sheet (custom form, dropdowns, VBA) untouched.SheetBuilder.generateMetadataOnlyclears and refills the Metadata sheet in place (not deleted) so the sheet'scodeName/ VBA binding survives.CLI flags:
--dhis2-url,--auth,--input,--output,--form-id,--form-type,--language, plus the metadata-generation toggles--include-codes,--use-codes,--org-unit-short-name(all default off).yarn regenerate-metadataalias; README documentation.Bumps
@eyeseetea/d2-apito1.21.0.Extracted MaintenanceSection from Settings page and added regenerate option there
🔥 Notes for the reviewer
📹 Screenshots/Screen capture
For testing, i used
PHSM - Policy Tracker.xlsm
this file where these metadata rows are removed
2026-07-29.12-27-11.0.02.-.1.11.mp4
📑 Others