@openmrs/esm-report-builder 0.1.1-pre.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/.tx/config +12 -0
  5. package/LICENSE +401 -0
  6. package/README.md +336 -0
  7. package/__mocks__/react-i18next.js +12 -0
  8. package/dist/162.js +1 -0
  9. package/dist/162.js.map +1 -0
  10. package/dist/184.js +2 -0
  11. package/dist/184.js.LICENSE.txt +14 -0
  12. package/dist/184.js.map +1 -0
  13. package/dist/242.js +2 -0
  14. package/dist/242.js.LICENSE.txt +21 -0
  15. package/dist/242.js.map +1 -0
  16. package/dist/253.js +1 -0
  17. package/dist/253.js.map +1 -0
  18. package/dist/300.js +1 -0
  19. package/dist/311.js +1 -0
  20. package/dist/311.js.map +1 -0
  21. package/dist/469.js +2 -0
  22. package/dist/469.js.LICENSE.txt +45 -0
  23. package/dist/469.js.map +1 -0
  24. package/dist/478.js +2 -0
  25. package/dist/478.js.LICENSE.txt +9 -0
  26. package/dist/478.js.map +1 -0
  27. package/dist/539.js +1 -0
  28. package/dist/539.js.map +1 -0
  29. package/dist/540.js +2 -0
  30. package/dist/540.js.LICENSE.txt +9 -0
  31. package/dist/540.js.map +1 -0
  32. package/dist/669.js +1 -0
  33. package/dist/669.js.map +1 -0
  34. package/dist/710.js +1 -0
  35. package/dist/710.js.map +1 -0
  36. package/dist/729.js +1 -0
  37. package/dist/729.js.map +1 -0
  38. package/dist/838.js +2 -0
  39. package/dist/838.js.LICENSE.txt +9 -0
  40. package/dist/838.js.map +1 -0
  41. package/dist/917.js +1 -0
  42. package/dist/917.js.map +1 -0
  43. package/dist/961.js +2 -0
  44. package/dist/961.js.LICENSE.txt +19 -0
  45. package/dist/961.js.map +1 -0
  46. package/dist/main.js +1 -0
  47. package/dist/main.js.map +1 -0
  48. package/dist/openmrs-esm-report-builder.js +1 -0
  49. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +530 -0
  50. package/dist/openmrs-esm-report-builder.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/e2e/core/env.ts +5 -0
  53. package/e2e/core/global-setup.ts +34 -0
  54. package/e2e/core/test.ts +21 -0
  55. package/e2e/pages/app-shell.page.ts +34 -0
  56. package/e2e/pages/etl-sources.page.ts +35 -0
  57. package/e2e/pages/report-sections.page.ts +20 -0
  58. package/e2e/pages/section-preview.modal.ts +29 -0
  59. package/e2e/specs/etl-sources.spec.ts +51 -0
  60. package/e2e/specs/navigation.spec.ts +13 -0
  61. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  62. package/e2e/specs/section-preview.spec.ts +94 -0
  63. package/e2e/support/github/Dockerfile +22 -0
  64. package/e2e/support/github/Dockerfile.dockerignore +13 -0
  65. package/e2e/support/github/docker-compose.yml +31 -0
  66. package/e2e/support/github/run-e2e-docker-env.sh +131 -0
  67. package/e2e/support/github/spa-assemble-config.json +9 -0
  68. package/e2e/tsconfig.json +11 -0
  69. package/example.env +5 -0
  70. package/jest.config.js +28 -0
  71. package/linelist_report_builder_design_spec.md +816 -0
  72. package/package.json +111 -0
  73. package/playwright.config.ts +28 -0
  74. package/routes.json +22 -0
  75. package/src/__debug__/all-designs.test.tsx +30 -0
  76. package/src/admin-card-link.extension.module.scss +55 -0
  77. package/src/admin-card-link.extension.tsx +24 -0
  78. package/src/components/admin/admin-page.component.tsx +75 -0
  79. package/src/components/admin/age-categories-page.component.tsx +109 -0
  80. package/src/components/admin/age-groups-page.component.tsx +146 -0
  81. package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
  82. package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
  83. package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
  84. package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
  85. package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
  86. package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
  87. package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
  88. package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
  89. package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
  90. package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
  91. package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
  92. package/src/components/admin/etl-sources-page.component.tsx +355 -0
  93. package/src/components/admin/etl-tasks-page.component.scss +370 -0
  94. package/src/components/admin/etl-tasks-page.component.tsx +768 -0
  95. package/src/components/admin/report-categories-page.component.tsx +267 -0
  96. package/src/components/admin/report-library-page.component.tsx +551 -0
  97. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  98. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  99. package/src/components/ai-support/ai.service.ts +21 -0
  100. package/src/components/ai-support/ai.types.ts +12 -0
  101. package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
  102. package/src/components/app-shell/report-builder-shell.scss +18 -0
  103. package/src/components/dashboards/dashboard-config.schema.ts +97 -0
  104. package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
  105. package/src/components/dashboards/dashboards-admin.scss +62 -0
  106. package/src/components/dashboards/dashboards-page.component.tsx +153 -0
  107. package/src/components/dashboards/dashboards-table.component.tsx +130 -0
  108. package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
  109. package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
  110. package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
  111. package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
  112. package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
  113. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  114. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  115. package/src/components/data-themes/data-themes-page.component.tsx +139 -0
  116. package/src/components/data-themes/data-themes-table.component.tsx +98 -0
  117. package/src/components/data-themes/icon-registry.ts +35 -0
  118. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  119. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  120. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  121. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  122. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  123. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  124. package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
  125. package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
  126. package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
  127. package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
  128. package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
  129. package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
  130. package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
  131. package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
  132. package/src/components/data-visualizer/components/header/header.scss +71 -0
  133. package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
  134. package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
  135. package/src/components/data-visualizer/components/panel/panel.scss +25 -0
  136. package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
  137. package/src/components/data-visualizer/constants.ts +445 -0
  138. package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
  139. package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
  140. package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
  141. package/src/components/data-visualizer/data-visualizer.scss +449 -0
  142. package/src/components/data-visualizer/declarations.d.ts +131 -0
  143. package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
  144. package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
  145. package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
  146. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
  147. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
  148. package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
  149. package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
  150. package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
  151. package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
  152. package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
  153. package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
  154. package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
  155. package/src/components/data-visualizer/types.ts +206 -0
  156. package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
  157. package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
  158. package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
  159. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
  160. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
  161. package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
  162. package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
  163. package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
  164. package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
  165. package/src/components/etl-monitor/builder/index.ts +5 -0
  166. package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
  167. package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
  168. package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
  169. package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
  170. package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
  171. package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
  172. package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
  173. package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
  174. package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
  175. package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
  176. package/src/components/etl-monitor/builder/steps/index.ts +5 -0
  177. package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
  178. package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
  179. package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
  180. package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
  181. package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
  182. package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
  183. package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
  184. package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
  185. package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
  186. package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
  187. package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
  188. package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
  189. package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
  190. package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
  191. package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
  192. package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
  193. package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
  194. package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
  195. package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
  196. package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
  197. package/src/components/etl-monitor/renderers/components/index.ts +6 -0
  198. package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
  199. package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
  200. package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
  201. package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
  202. package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
  203. package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
  204. package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
  205. package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
  206. package/src/components/etl-monitor/renderers/index.ts +11 -0
  207. package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
  208. package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
  209. package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
  210. package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
  211. package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
  212. package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
  213. package/src/components/import-export/artifact-groups.ts +153 -0
  214. package/src/components/import-export/export-report-modal.component.tsx +265 -0
  215. package/src/components/import-export/import-export-page.component.tsx +610 -0
  216. package/src/components/import-export/import-export.styles.scss +825 -0
  217. package/src/components/import-export/import-package-modal.component.tsx +401 -0
  218. package/src/components/import-export/index.ts +3 -0
  219. package/src/components/indicators/README.md +24 -0
  220. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  221. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  222. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  223. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  224. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  225. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  226. package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
  227. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
  228. package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
  229. package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
  230. package/src/components/indicators/custom-condition-input.component.tsx +310 -0
  231. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  232. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  233. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  234. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  235. package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
  236. package/src/components/indicators/indicator-search-select.component.tsx +297 -0
  237. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  238. package/src/components/indicators/indicators-page.component.tsx +673 -0
  239. package/src/components/indicators/indicators-page.scss +121 -0
  240. package/src/components/indicators/indicators-table.component.tsx +188 -0
  241. package/src/components/indicators/preview-table.component.tsx +31 -0
  242. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  243. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
  244. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  245. package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
  246. package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
  247. package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
  248. package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
  249. package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
  250. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  251. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  252. package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
  253. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  254. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  255. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  256. package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
  257. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
  258. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  259. package/src/components/indicators/sql-preview.component.tsx +19 -0
  260. package/src/components/indicators/tag-input.component.tsx +177 -0
  261. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  262. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  263. package/src/components/indicators/types/composite-indicator.types.ts +27 -0
  264. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  265. package/src/components/indicators/types/custom-indicator.types.ts +161 -0
  266. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  267. package/src/components/indicators/types/indicator-types.ts +57 -0
  268. package/src/components/indicators/types/sql-builders.ts +43 -0
  269. package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
  270. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
  271. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  272. package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
  273. package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
  274. package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
  275. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  276. package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
  277. package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
  278. package/src/components/indicators/utils/population-sql.compiler.test.ts +999 -0
  279. package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
  280. package/src/components/landing/landing-action-item.component.tsx +50 -0
  281. package/src/components/landing/landing-action-list.component.tsx +12 -0
  282. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  283. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  284. package/src/components/landing/landing-hero.component.tsx +18 -0
  285. package/src/components/landing/landing-section-card.component.tsx +33 -0
  286. package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
  287. package/src/components/landing/report-builder-landing-page.scss +131 -0
  288. package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
  289. package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
  290. package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
  291. package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
  292. package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
  293. package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
  294. package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
  295. package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
  296. package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
  297. package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
  298. package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
  299. package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
  300. package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
  301. package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
  302. package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
  303. package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
  304. package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
  305. package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
  306. package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
  307. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
  308. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
  309. package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
  310. package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
  311. package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
  312. package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
  313. package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
  314. package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
  315. package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
  316. package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
  317. package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
  318. package/src/components/linelist/design/panels/column-category-selector.scss +329 -0
  319. package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
  320. package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
  321. package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
  322. package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
  323. package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
  324. package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
  325. package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
  326. package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
  327. package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
  328. package/src/components/linelist/design/panels/json-preview.scss +85 -0
  329. package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
  330. package/src/components/linelist/design/panels/query-config-panel.scss +342 -0
  331. package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
  332. package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
  333. package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
  334. package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
  335. package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
  336. package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
  337. package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
  338. package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
  339. package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
  340. package/src/components/linelist/linelist-dashboard.page.scss +240 -0
  341. package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
  342. package/src/components/linelist/linelist-editor.page.scss +28 -0
  343. package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
  344. package/src/components/linelist/linelist-run-report.page.scss +136 -0
  345. package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
  346. package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
  347. package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
  348. package/src/components/report/dashboard/reports-table.component.tsx +186 -0
  349. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  350. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  351. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  352. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  353. package/src/components/report/definition/report-definition.types.ts +66 -0
  354. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  355. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  356. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  357. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  358. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  359. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  360. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  361. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  362. package/src/components/report/design/report-design-editor.component.tsx +1170 -0
  363. package/src/components/report/design/report-design.types.ts +48 -0
  364. package/src/components/report/design/report-design.utils.ts +86 -0
  365. package/src/components/report/panels/report-detail.component.tsx +71 -0
  366. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  367. package/src/components/report/report-dashboard.page.component.tsx +554 -0
  368. package/src/components/report/report-editor.page.component.tsx +590 -0
  369. package/src/components/report/report.routes.tsx +32 -0
  370. package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
  371. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  372. package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
  373. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  374. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  375. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
  376. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  377. package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
  378. package/src/components/report-sections/report-sections-page.component.tsx +325 -0
  379. package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
  380. package/src/components/report-sections/section-types.ts +55 -0
  381. package/src/components/report-sections/section-utils.ts +50 -0
  382. package/src/components/report-sections/sections-page.scss +79 -0
  383. package/src/components/run-reports/run-reports-page.component.tsx +8 -0
  384. package/src/components/security/access-denied-page.component.tsx +69 -0
  385. package/src/components/security/access-denied-page.scss +39 -0
  386. package/src/components/security/privilege-route.component.test.tsx +79 -0
  387. package/src/components/security/privilege-route.component.tsx +39 -0
  388. package/src/components/shared/compile-setup-modal.component.tsx +182 -0
  389. package/src/components/shared/concept-selector.component.tsx +245 -0
  390. package/src/components/shared/concept-selector.scss +121 -0
  391. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  392. package/src/components/shared/header/header.component.tsx +85 -0
  393. package/src/components/shared/header/header.scss +150 -0
  394. package/src/components/shared/header/illustration.component.tsx +34 -0
  395. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  396. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  397. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  398. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  399. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  400. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  401. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  402. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  403. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  404. package/src/config-schema.ts +43 -0
  405. package/src/constants/privileges.test.ts +53 -0
  406. package/src/constants/privileges.ts +118 -0
  407. package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
  408. package/src/features/dashboards/DashboardsPage.tsx +164 -0
  409. package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
  410. package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
  411. package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
  412. package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
  413. package/src/features/dashboards/components/DashboardSection.tsx +46 -0
  414. package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
  415. package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
  416. package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
  417. package/src/features/dashboards/dashboards.scss +196 -0
  418. package/src/features/dashboards/hooks/useDashboard.ts +61 -0
  419. package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
  420. package/src/features/dashboards/hooks/useDashboards.ts +56 -0
  421. package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
  422. package/src/features/dashboards/index.ts +11 -0
  423. package/src/features/dashboards/utils/dashboard-config.util.test.ts +255 -0
  424. package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
  425. package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
  426. package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
  427. package/src/features/report-visualizer/components/Export/index.ts +2 -0
  428. package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
  429. package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
  430. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
  431. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
  432. package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
  433. package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
  434. package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
  435. package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
  436. package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
  437. package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
  438. package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
  439. package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
  440. package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
  441. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
  442. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
  443. package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
  444. package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
  445. package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
  446. package/src/features/report-visualizer/components/TableView/index.ts +2 -0
  447. package/src/features/report-visualizer/hooks/index.ts +4 -0
  448. package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
  449. package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
  450. package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
  451. package/src/features/report-visualizer/hooks/useReports.ts +135 -0
  452. package/src/features/report-visualizer/index.ts +6 -0
  453. package/src/features/report-visualizer/report-visualizer.scss +593 -0
  454. package/src/features/report-visualizer/types/index.ts +1 -0
  455. package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
  456. package/src/features/report-visualizer/utils/status-colors.ts +117 -0
  457. package/src/globals.d.ts +14 -0
  458. package/src/hooks/address-template.ts +86 -0
  459. package/src/hooks/concept.ts +112 -0
  460. package/src/hooks/indicator/index.ts +1 -0
  461. package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
  462. package/src/hooks/openmrs-reference-data.ts +432 -0
  463. package/src/hooks/theme/index.ts +4 -0
  464. package/src/hooks/theme/useDataTheme.ts +61 -0
  465. package/src/hooks/theme/useDataThemes.ts +28 -0
  466. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  467. package/src/hooks/theme/useMambaTables.ts +95 -0
  468. package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
  469. package/src/hooks/use-report-builder-privileges.ts +58 -0
  470. package/src/index.ts +29 -0
  471. package/src/resources/address-template/address-template.api.ts +180 -0
  472. package/src/resources/agegroup/agegroups.api.ts +58 -0
  473. package/src/resources/concept/concept.api.ts +116 -0
  474. package/src/resources/concepts/concept-mapper.ts +21 -0
  475. package/src/resources/concepts/concept-types.ts +31 -0
  476. package/src/resources/concepts/concepts.resource.ts +44 -0
  477. package/src/resources/concepts/useConceptSearch.ts +41 -0
  478. package/src/resources/dashboard/dashboard.api.ts +107 -0
  479. package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
  480. package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
  481. package/src/resources/etl-source/etl-source.api.ts +65 -0
  482. package/src/resources/etl-task/etl-task.api.ts +96 -0
  483. package/src/resources/etl-task/etl-task.types.ts +31 -0
  484. package/src/resources/indicator/indicators.api.ts +125 -0
  485. package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
  486. package/src/resources/linelist/linelist-reports.api.ts +616 -0
  487. package/src/resources/location/location.api.ts +135 -0
  488. package/src/resources/openmrs-api.ts +40 -0
  489. package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
  490. package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
  491. package/src/resources/preview/sql-preview.api.ts +54 -0
  492. package/src/resources/report/reports.api.ts +140 -0
  493. package/src/resources/report-category/report-category.api.ts +41 -0
  494. package/src/resources/report-import-export/import-export-api.ts +164 -0
  495. package/src/resources/report-library/report-library.api.ts +63 -0
  496. package/src/resources/report-section/report-sections.api.ts +73 -0
  497. package/src/resources/report-section/section-preview.api.ts +25 -0
  498. package/src/resources/theme/data-theme.api.ts +88 -0
  499. package/src/resources/theme/etl-schema.api.ts +101 -0
  500. package/src/resources/theme/etl-table-meta.api.ts +45 -0
  501. package/src/root.component.tsx +20 -0
  502. package/src/routes/report-builder.routes.tsx +160 -0
  503. package/src/routes/report-builder.scss +56 -0
  504. package/src/routes.json +28 -0
  505. package/src/sample-template.ts +121 -0
  506. package/src/template-utils.ts +110 -0
  507. package/src/types/carbon-react-inert.d.ts +13 -0
  508. package/src/types/condition-operators.ts +63 -0
  509. package/src/types/dashboard/dashboard.types.ts +137 -0
  510. package/src/types/dashboard/index.ts +4 -0
  511. package/src/types/etl/etl-types.ts +238 -0
  512. package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
  513. package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
  514. package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
  515. package/src/types/etl-monitor/index.ts +6 -0
  516. package/src/types/linelist/compile-config.ts +455 -0
  517. package/src/types/linelist-types.ts +2059 -0
  518. package/src/types/theme/data-theme.types.ts +88 -0
  519. package/src/utils/api-error.utils.test.ts +151 -0
  520. package/src/utils/api-error.utils.ts +131 -0
  521. package/src/utils/config-enhancer.ts +62 -0
  522. package/src/utils/etl-browser/etl-browser.util.test.ts +145 -0
  523. package/src/utils/etl-browser/export-csv.util.ts +43 -0
  524. package/src/utils/etl-browser/group-tables.util.ts +84 -0
  525. package/src/utils/etl-browser/index.ts +4 -0
  526. package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
  527. package/src/utils/etl-browser/table-sql.util.ts +20 -0
  528. package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
  529. package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
  530. package/src/utils/etl-monitor/index.ts +10 -0
  531. package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
  532. package/src/utils/etl-monitor/normalization-examples.ts +0 -0
  533. package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
  534. package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
  535. package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
  536. package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
  537. package/src/utils/etl-task/etl-task-card.util.test.ts +239 -0
  538. package/src/utils/etl-task/etl-task-card.util.ts +276 -0
  539. package/src/utils/etl-task/index.ts +1 -0
  540. package/src/utils/html-entities.utils.ts +34 -0
  541. package/src/utils/parameter-resolution.ts +218 -0
  542. package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
  543. package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
  544. package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
  545. package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
  546. package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
  547. package/tools/scss-stub.js +12 -0
  548. package/tools/setup-tests.ts +1 -0
  549. package/translations/en.json +65 -0
  550. package/tsconfig.json +43 -0
  551. package/webpack.config.js +30 -0
@@ -0,0 +1,1170 @@
1
+ import React from 'react';
2
+ import {
3
+ Button,
4
+ InlineNotification,
5
+ Tabs,
6
+ TabList,
7
+ Tab,
8
+ TextInput,
9
+ NumberInput,
10
+ Select,
11
+ SelectItem,
12
+ Checkbox,
13
+ Tag,
14
+ } from '@carbon/react';
15
+ import {
16
+ Add,
17
+ ArrowUp,
18
+ ArrowDown,
19
+ TrashCan,
20
+ Draggable,
21
+ ArrowRight,
22
+ ArrowLeft,
23
+ Renew,
24
+ View,
25
+ Download,
26
+ Copy,
27
+ Launch,
28
+ } from '@carbon/icons-react';
29
+ import { useTranslation } from 'react-i18next';
30
+
31
+ import styles from '../../../routes/report-builder.scss';
32
+
33
+ import type { ReportDefinitionDraft } from '../definition/report-definition.types';
34
+ import type { DesignGroup, DesignRow, ReportDesignDraft } from './report-design.types';
35
+ import {
36
+ createEmptyDesignRow,
37
+ createEmptyReportDesignDraft,
38
+ } from './report-design.utils';
39
+
40
+ export type DesignSectionSource = {
41
+ sectionUuid: string;
42
+ title: string;
43
+ indicators: Array<{
44
+ id: string;
45
+ code: string;
46
+ name: string;
47
+ type: string;
48
+ }>;
49
+ };
50
+
51
+ type Props = {
52
+ value?: ReportDesignDraft | null;
53
+ onChange?: (next: ReportDesignDraft) => void;
54
+ definitionDraft?: ReportDefinitionDraft | null;
55
+ sectionSources?: DesignSectionSource[];
56
+ sectionNameLookup?: Record<string, string>;
57
+ };
58
+
59
+ type DragState = {
60
+ rowId: string;
61
+ fromGroupId: string;
62
+ } | null;
63
+
64
+ type StructureItem =
65
+ | { kind: 'group'; group: DesignGroup }
66
+ | { kind: 'row'; group: DesignGroup; row: DesignRow; rowIndex: number };
67
+
68
+ const MAX_INDENT = 10;
69
+
70
+ function getMinIndentForRowType(type?: string | null) {
71
+ return type === 'section-label' ? 0 : 1;
72
+ }
73
+
74
+ function clampIndentForRowType(indent: number | undefined, type?: string | null) {
75
+ const min = getMinIndentForRowType(type);
76
+ const safe = Number.isNaN(Number(indent)) ? min : Number(indent ?? min);
77
+ return Math.max(min, Math.min(MAX_INDENT, safe));
78
+ }
79
+
80
+ /**
81
+ * Rebuild hierarchical relationships (parentId, position, children) based on indent levels.
82
+ * This function analyzes rows and their indent values to establish parent-child connections.
83
+ */
84
+ function rebuildHierarchicalStructure(rows: DesignRow[]): DesignRow[] {
85
+ const result = [...rows];
86
+ const parentStack: Array<{ rowId: string; indent: number }> = [];
87
+
88
+ for (let i = 0; i < result.length; i++) {
89
+ const row = result[i];
90
+ const currentIndent = row.indent ?? 0;
91
+
92
+ // Pop from stack until we find the parent (lower indent level)
93
+ while (parentStack.length > 0 && parentStack[parentStack.length - 1].indent >= currentIndent) {
94
+ parentStack.pop();
95
+ }
96
+
97
+ // Set parentId if we have a parent
98
+ if (parentStack.length > 0) {
99
+ row.parentId = parentStack[parentStack.length - 1].rowId;
100
+ row.position = parentStack.filter(p => p.rowId === row.parentId).length;
101
+ } else {
102
+ row.parentId = undefined;
103
+ row.position = 0;
104
+ }
105
+
106
+ // If this could be a parent (not a spacer/label with minimal content), add to stack
107
+ if (row.type === 'group-label' || row.type === 'section-label' || (row.type === 'indicator' && currentIndent < 10)) {
108
+ parentStack.push({ rowId: row.id, indent: currentIndent });
109
+ }
110
+ }
111
+
112
+ // Build children arrays for parent rows
113
+ const childrenMap = new Map<string, string[]>();
114
+ for (const row of result) {
115
+ if (row.parentId) {
116
+ if (!childrenMap.has(row.parentId)) {
117
+ childrenMap.set(row.parentId, []);
118
+ }
119
+ childrenMap.get(row.parentId)!.push(row.id);
120
+ }
121
+ }
122
+
123
+ // Assign children arrays to parent rows
124
+ for (const row of result) {
125
+ row.children = childrenMap.get(row.id) || [];
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ function applyRowTypeDefaults(row: DesignRow, nextType: string): DesignRow {
132
+ if (nextType === 'section-label') {
133
+ return {
134
+ ...row,
135
+ type: nextType as any,
136
+ indent: 0,
137
+ span: 'all' as any,
138
+ emphasis: 'section' as any,
139
+ showTotal: false,
140
+ showDisaggregation: false,
141
+ };
142
+ }
143
+
144
+ if (nextType === 'group-label') {
145
+ return {
146
+ ...row,
147
+ type: nextType as any,
148
+ indent: clampIndentForRowType(row.indent, nextType),
149
+ span: 'label-only' as any,
150
+ emphasis: 'group' as any,
151
+ showTotal: false,
152
+ showDisaggregation: false,
153
+ };
154
+ }
155
+
156
+ if (nextType === 'indicator') {
157
+ return {
158
+ ...row,
159
+ type: nextType as any,
160
+ indent: clampIndentForRowType(row.indent, nextType),
161
+ span: 'label-only' as any,
162
+ emphasis: 'normal' as any,
163
+ showTotal: true,
164
+ showDisaggregation: true,
165
+ };
166
+ }
167
+
168
+ return {
169
+ ...row,
170
+ type: nextType as any,
171
+ indent: clampIndentForRowType(row.indent, nextType),
172
+ span: 'label-only' as any,
173
+ emphasis: 'normal' as any,
174
+ };
175
+ }
176
+
177
+ function buildDesignFromSectionSources(
178
+ sectionSources: DesignSectionSource[] = [],
179
+ previous?: ReportDesignDraft | null,
180
+ ): ReportDesignDraft {
181
+ return {
182
+ version: 1,
183
+ template: 'section-tabular',
184
+ arrayName: previous?.arrayName ?? 'results',
185
+ defaultValue: previous?.defaultValue ?? 0,
186
+ dimensions: previous?.dimensions ?? {},
187
+ groups: sectionSources.map((section) => {
188
+ const rows = [
189
+ {
190
+ id: `${section.sectionUuid}__section_label`,
191
+ type: 'section-label' as any,
192
+ label: section.title,
193
+ indent: 0,
194
+ span: 'all' as any,
195
+ emphasis: 'section' as any,
196
+ showTotal: false,
197
+ showDisaggregation: false,
198
+ },
199
+ ...section.indicators.map((i) => ({
200
+ id: i.id,
201
+ type: 'indicator' as const,
202
+ code: i.code,
203
+ label: `${i.code}. ${i.name}`,
204
+ indent: 1,
205
+ keyPattern: '{code}_{age}_{sex}',
206
+ dims: {},
207
+ showTotal: true,
208
+ showDisaggregation: true,
209
+ span: 'label-only' as any,
210
+ emphasis: 'normal' as any,
211
+ })),
212
+ ];
213
+ return {
214
+ id: section.sectionUuid,
215
+ title: section.title,
216
+ rows: rebuildHierarchicalStructure(rows),
217
+ };
218
+ }),
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Smart merge function that preserves user customizations when updating from section sources.
224
+ * This is the preferred method over buildDesignFromSectionSources() for incremental design work.
225
+ */
226
+ function buildDesignFromSectionSourcesWithMerge(
227
+ sectionSources: DesignSectionSource[] = [],
228
+ previous?: ReportDesignDraft | null,
229
+ ): ReportDesignDraft {
230
+ if (!previous || !previous.groups.length) {
231
+ // If no previous design exists, fall back to the regular build
232
+ return buildDesignFromSectionSources(sectionSources, previous);
233
+ }
234
+
235
+ // Create a map of existing groups and rows for quick lookup
236
+ const existingGroupsMap = new Map<string, DesignGroup>();
237
+ const existingRowsMap = new Map<string, { group: DesignGroup; row: DesignRow }>();
238
+
239
+ previous.groups.forEach((group) => {
240
+ existingGroupsMap.set(group.id, group);
241
+ group.rows.forEach((row) => {
242
+ existingRowsMap.set(row.id, { group, row });
243
+ });
244
+ });
245
+
246
+ // Create a set of new section UUIDs
247
+ const newSectionUuids = new Set(sectionSources.map((s) => s.sectionUuid));
248
+
249
+ // Build new groups, merging with existing where possible
250
+ const mergedGroups: DesignGroup[] = [];
251
+
252
+ for (const sectionSource of sectionSources) {
253
+ const existingGroup = existingGroupsMap.get(sectionSource.sectionUuid);
254
+
255
+ if (existingGroup) {
256
+ // Group exists - merge rows
257
+ const mergedRows: DesignRow[] = [];
258
+
259
+ // Create a map of existing rows in this group
260
+ const existingRowsInGroup = new Map<string, DesignRow>();
261
+ existingGroup.rows.forEach((row) => {
262
+ existingRowsInGroup.set(row.id, row);
263
+ });
264
+
265
+ // Create a set of new indicator IDs
266
+ const newIndicatorIds = new Set(sectionSource.indicators.map((i) => i.id));
267
+
268
+ // Always preserve the section label row if it exists
269
+ const sectionLabelRowId = `${sectionSource.sectionUuid}__section_label`;
270
+ const existingSectionLabel = existingRowsInGroup.get(sectionLabelRowId);
271
+ if (existingSectionLabel) {
272
+ mergedRows.push({ ...existingSectionLabel }); // Preserve all customizations
273
+ } else {
274
+ // Create new section label row
275
+ mergedRows.push({
276
+ id: sectionLabelRowId,
277
+ type: 'section-label' as any,
278
+ label: sectionSource.title,
279
+ indent: 0,
280
+ span: 'all' as any,
281
+ emphasis: 'section' as any,
282
+ showTotal: false,
283
+ showDisaggregation: false,
284
+ });
285
+ }
286
+
287
+ // Merge indicator rows
288
+ for (const indicator of sectionSource.indicators) {
289
+ const existingRow = existingRowsInGroup.get(indicator.id);
290
+
291
+ if (existingRow) {
292
+ // Row exists - preserve all customizations
293
+ mergedRows.push({ ...existingRow });
294
+ } else {
295
+ // New indicator - create default row
296
+ mergedRows.push({
297
+ id: indicator.id,
298
+ type: 'indicator' as const,
299
+ code: indicator.code,
300
+ label: `${indicator.code}. ${indicator.name}`,
301
+ indent: 1,
302
+ keyPattern: '{code}_{age}_{sex}',
303
+ dims: {},
304
+ showTotal: true,
305
+ showDisaggregation: true,
306
+ span: 'label-only' as any,
307
+ emphasis: 'normal' as any,
308
+ });
309
+ }
310
+ }
311
+
312
+ // Preserve any additional custom rows that aren't section labels or standard indicators
313
+ // (e.g., group labels, custom rows the user added)
314
+ for (const [rowId, row] of existingRowsInGroup) {
315
+ if (
316
+ rowId !== sectionLabelRowId &&
317
+ !newIndicatorIds.has(rowId) &&
318
+ row.type !== 'section-label' &&
319
+ row.type !== 'indicator'
320
+ ) {
321
+ // This is a custom row - preserve it
322
+ mergedRows.push({ ...row });
323
+ }
324
+ }
325
+
326
+ // Rebuild hierarchical relationships for merged rows
327
+ const rowsWithStructure = rebuildHierarchicalStructure(mergedRows);
328
+
329
+ // Update group title but preserve other properties
330
+ mergedGroups.push({
331
+ ...existingGroup,
332
+ title: sectionSource.title,
333
+ rows: rowsWithStructure,
334
+ });
335
+ } else {
336
+ // New group - build from section source
337
+ const newGroupRows = [
338
+ {
339
+ id: `${sectionSource.sectionUuid}__section_label`,
340
+ type: 'section-label' as any,
341
+ label: sectionSource.title,
342
+ indent: 0,
343
+ span: 'all' as any,
344
+ emphasis: 'section' as any,
345
+ showTotal: false,
346
+ showDisaggregation: false,
347
+ },
348
+ ...sectionSource.indicators.map((i) => ({
349
+ id: i.id,
350
+ type: 'indicator' as const,
351
+ code: i.code,
352
+ label: `${i.code}. ${i.name}`,
353
+ indent: 1,
354
+ keyPattern: '{code}_{age}_{sex}',
355
+ dims: {},
356
+ showTotal: true,
357
+ showDisaggregation: true,
358
+ span: 'label-only' as any,
359
+ emphasis: 'normal' as any,
360
+ })),
361
+ ];
362
+
363
+ mergedGroups.push({
364
+ id: sectionSource.sectionUuid,
365
+ title: sectionSource.title,
366
+ rows: rebuildHierarchicalStructure(newGroupRows),
367
+ });
368
+ }
369
+ }
370
+
371
+ // Preserve any additional custom groups that aren't in the new section sources
372
+ // (e.g., groups the user manually created)
373
+ for (const [groupId, group] of existingGroupsMap) {
374
+ if (!newSectionUuids.has(groupId)) {
375
+ // This is a custom group - preserve it with rebuilt structure
376
+ mergedGroups.push({
377
+ ...group,
378
+ rows: rebuildHierarchicalStructure(group.rows.map((row) => ({ ...row })))
379
+ });
380
+ }
381
+ }
382
+
383
+ return {
384
+ version: previous.version,
385
+ template: previous.template,
386
+ arrayName: previous.arrayName,
387
+ defaultValue: previous.defaultValue,
388
+ dimensions: previous.dimensions || {},
389
+ groups: mergedGroups,
390
+ };
391
+ }
392
+
393
+ function buildMappingPreview(groups: DesignGroup[]) {
394
+ const columns = ['Age <5 | Male', 'Age <5 | Female', '5-14 | Male', '5-14 | Female'];
395
+
396
+ const rows = groups.flatMap((group) =>
397
+ group.rows.map((r) => ({
398
+ id: r.id,
399
+ type: r.type,
400
+ code: (r as any).code,
401
+ label: r.label || r.code || 'Untitled row',
402
+ values:
403
+ r.type === ('section-label' as any) || r.type === ('group-label' as any)
404
+ ? []
405
+ : columns.map(() => 0),
406
+ indent: r.indent ?? 0,
407
+ groupId: group.id,
408
+ })),
409
+ );
410
+
411
+ return { columns, rows };
412
+ }
413
+
414
+ const ReportDesignEditor: React.FC<Props> = ({
415
+ value,
416
+ onChange,
417
+ definitionDraft,
418
+ sectionSources = [],
419
+ }) => {
420
+ const { t } = useTranslation();
421
+
422
+ const [selectedGroupId, setSelectedGroupId] = React.useState<string | null>(null);
423
+ const [selectedRowId, setSelectedRowId] = React.useState<string | null>(null);
424
+ const [dragState, setDragState] = React.useState<DragState>(null);
425
+ const [dragOverRowId, setDragOverRowId] = React.useState<string | null>(null);
426
+ const [propTab, setPropTab] = React.useState<'details' | 'disaggregation' | 'mapping' | 'api'>('details');
427
+
428
+ const draft = React.useMemo<ReportDesignDraft>(
429
+ () => value ?? createEmptyReportDesignDraft(),
430
+ [value],
431
+ );
432
+
433
+ const setDraft = React.useCallback(
434
+ (next: ReportDesignDraft) => {
435
+ onChange?.(next);
436
+ },
437
+ [onChange],
438
+ );
439
+
440
+ const groups = React.useMemo(() => draft.groups ?? [], [draft.groups]);
441
+
442
+ React.useEffect(() => {
443
+ if (!selectedGroupId && groups.length > 0) {
444
+ setSelectedGroupId(groups[0].id);
445
+ }
446
+ }, [groups, selectedGroupId]);
447
+
448
+ const selectedRow = React.useMemo(
449
+ () =>
450
+ groups
451
+ .flatMap((g) => g.rows)
452
+ .find((r) => r.id === selectedRowId) ?? null,
453
+ [groups, selectedRowId],
454
+ );
455
+
456
+ const updateGroups = React.useCallback(
457
+ (nextGroups: DesignGroup[]) => {
458
+ setDraft({
459
+ ...draft,
460
+ groups: nextGroups,
461
+ });
462
+ },
463
+ [draft, setDraft],
464
+ );
465
+
466
+ const updateRow = React.useCallback(
467
+ (groupId: string, rowId: string, patch: Partial<DesignRow>) => {
468
+ updateGroups(
469
+ groups.map((g) => {
470
+ if (g.id !== groupId) return g;
471
+
472
+ const rows = g.rows.map((r) => {
473
+ if (r.id !== rowId) return r;
474
+ const next = { ...r, ...patch };
475
+ return {
476
+ ...next,
477
+ indent: clampIndentForRowType(next.indent, next.type),
478
+ };
479
+ });
480
+
481
+ // Rebuild hierarchical structure after update
482
+ return {
483
+ ...g,
484
+ rows: rebuildHierarchicalStructure(rows),
485
+ };
486
+ }),
487
+ );
488
+ },
489
+ [groups, updateGroups],
490
+ );
491
+
492
+ const addRowToGroup = React.useCallback(
493
+ (groupId: string) => {
494
+ const row = createEmptyDesignRow();
495
+ row.indent = 1;
496
+
497
+ updateGroups(
498
+ groups.map((g) =>
499
+ g.id === groupId
500
+ ? { ...g, rows: [...g.rows, row] }
501
+ : g,
502
+ ),
503
+ );
504
+ setSelectedGroupId(groupId);
505
+ setSelectedRowId(row.id);
506
+ },
507
+ [groups, updateGroups],
508
+ );
509
+
510
+ const removeRow = React.useCallback(
511
+ (groupId: string, rowId: string) => {
512
+ updateGroups(
513
+ groups.map((g) =>
514
+ g.id === groupId
515
+ ? { ...g, rows: g.rows.filter((r) => r.id !== rowId) }
516
+ : g,
517
+ ),
518
+ );
519
+
520
+ if (selectedRowId === rowId) {
521
+ setSelectedRowId(null);
522
+ }
523
+ },
524
+ [groups, selectedRowId, updateGroups],
525
+ );
526
+
527
+ const moveRow = React.useCallback(
528
+ (groupId: string, rowId: string, dir: -1 | 1) => {
529
+ const group = groups.find((g) => g.id === groupId);
530
+ if (!group) return;
531
+
532
+ const rows = group.rows.slice();
533
+ const idx = rows.findIndex((r) => r.id === rowId);
534
+ if (idx < 0) return;
535
+
536
+ const j = idx + dir;
537
+ if (j < 0 || j >= rows.length) return;
538
+
539
+ const tmp = rows[idx];
540
+ rows[idx] = rows[j];
541
+ rows[j] = tmp;
542
+
543
+ // Rebuild hierarchical structure after move
544
+ const rowsWithStructure = rebuildHierarchicalStructure(rows);
545
+
546
+ updateGroups(
547
+ groups.map((g) =>
548
+ g.id === groupId ? { ...g, rows: rowsWithStructure } : g,
549
+ ),
550
+ );
551
+ },
552
+ [groups, updateGroups],
553
+ );
554
+
555
+ const indentRow = React.useCallback(
556
+ (groupId: string, rowId: string, dir: -1 | 1) => {
557
+ const group = groups.find((g) => g.id === groupId);
558
+ const row = group?.rows.find((r) => r.id === rowId);
559
+ if (!group || !row) return;
560
+
561
+ const minIndent = getMinIndentForRowType(row.type);
562
+ const nextIndent = Math.max(minIndent, Math.min(MAX_INDENT, (row.indent ?? minIndent) + dir));
563
+
564
+ // Update the row indent
565
+ const rows = group.rows.map((r) =>
566
+ r.id === rowId ? { ...r, indent: nextIndent } : r
567
+ );
568
+
569
+ // Rebuild hierarchical structure after indent change
570
+ const rowsWithStructure = rebuildHierarchicalStructure(rows);
571
+
572
+ updateGroups(
573
+ groups.map((g) =>
574
+ g.id === groupId ? { ...g, rows: rowsWithStructure } : g,
575
+ ),
576
+ );
577
+ },
578
+ [groups, updateGroups],
579
+ );
580
+
581
+ const generateFromDefinition = React.useCallback(() => {
582
+ const next = buildDesignFromSectionSources(sectionSources, draft);
583
+ setDraft(next);
584
+ setSelectedGroupId(next.groups[0]?.id ?? null);
585
+ setSelectedRowId(next.groups[0]?.rows[0]?.id ?? null);
586
+ }, [draft, sectionSources, setDraft]);
587
+
588
+ const refreshFromDefinition = React.useCallback(() => {
589
+ const next = buildDesignFromSectionSourcesWithMerge(sectionSources, draft);
590
+ setDraft(next);
591
+ // Keep current selection if possible, otherwise select first
592
+ const stillSelected = next.groups.find((g) => g.id === selectedGroupId);
593
+ if (stillSelected) {
594
+ const rowStillExists = stillSelected.rows.find((r) => r.id === selectedRowId);
595
+ if (rowStillExists) {
596
+ // Both group and row still exist, keep selection
597
+ return;
598
+ }
599
+ // Group exists but row doesn't, select first row in group
600
+ setSelectedRowId(stillSelected.rows[0]?.id ?? null);
601
+ } else {
602
+ // Group doesn't exist, select first group and row
603
+ setSelectedGroupId(next.groups[0]?.id ?? null);
604
+ setSelectedRowId(next.groups[0]?.rows[0]?.id ?? null);
605
+ }
606
+ }, [draft, sectionSources, setDraft, selectedGroupId, selectedRowId]);
607
+
608
+ const handleDragStart = React.useCallback((rowId: string, fromGroupId: string) => {
609
+ setDragState({ rowId, fromGroupId });
610
+ }, []);
611
+
612
+ const handleDropOnRow = React.useCallback(
613
+ (targetGroupId: string, targetRowId: string) => {
614
+ if (!dragState) return;
615
+ if (dragState.fromGroupId !== targetGroupId) return;
616
+ if (dragState.rowId === targetRowId) return;
617
+
618
+ const group = groups.find((g) => g.id === targetGroupId);
619
+ if (!group) return;
620
+
621
+ const rows = group.rows.slice();
622
+ const fromIdx = rows.findIndex((r) => r.id === dragState.rowId);
623
+ const toIdx = rows.findIndex((r) => r.id === targetRowId);
624
+
625
+ if (fromIdx < 0 || toIdx < 0) return;
626
+
627
+ const moving = rows[fromIdx];
628
+ rows.splice(fromIdx, 1);
629
+ rows.splice(toIdx, 0, moving);
630
+
631
+ // Rebuild hierarchical structure after drop
632
+ const rowsWithStructure = rebuildHierarchicalStructure(rows);
633
+
634
+ updateGroups(
635
+ groups.map((g) =>
636
+ g.id === targetGroupId ? { ...g, rows: rowsWithStructure } : g,
637
+ ),
638
+ );
639
+
640
+ setDragState(null);
641
+ setDragOverRowId(null);
642
+ },
643
+ [dragState, groups, updateGroups],
644
+ );
645
+
646
+ const structureItems = React.useMemo<StructureItem[]>(() => {
647
+ return groups.flatMap((group) => [
648
+ { kind: 'group' as const, group },
649
+ ...group.rows.map((row, rowIndex) => ({
650
+ kind: 'row' as const,
651
+ group,
652
+ row,
653
+ rowIndex,
654
+ })),
655
+ ]);
656
+ }, [groups]);
657
+
658
+ const mappingPreview = React.useMemo(() => buildMappingPreview(groups), [groups]);
659
+
660
+ return (
661
+ <div className={styles.designWorkspace}>
662
+ <div style={{ marginBottom: '1rem' }}>
663
+ <h3 className={styles.workspaceTitle}>{t('reportDesign', 'Report Design')}</h3>
664
+ <p className={styles.workspaceHint}>
665
+ {t(
666
+ 'reportDesignHint',
667
+ 'Design the report structure, indicator presentation, JSON template, and mapping preview.',
668
+ )}
669
+ </p>
670
+ </div>
671
+
672
+ {!definitionDraft ? (
673
+ <InlineNotification
674
+ kind="warning"
675
+ lowContrast
676
+ title="Definition required"
677
+ subtitle="Build the report definition first so the design can align to it."
678
+ />
679
+ ) : null}
680
+
681
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
682
+ <Button
683
+ size="sm"
684
+ kind="primary"
685
+ onClick={generateFromDefinition}
686
+ disabled={!sectionSources.length}
687
+ >
688
+ Generate From Definition
689
+ </Button>
690
+ <Button
691
+ size="sm"
692
+ kind="secondary"
693
+ onClick={refreshFromDefinition}
694
+ disabled={!sectionSources.length}
695
+ renderIcon={Renew}
696
+ >
697
+ Refresh from Definition
698
+ </Button>
699
+ </div>
700
+
701
+ <div
702
+ style={{
703
+ border: '1px solid var(--cds-border-subtle, #d0d0d0)',
704
+ borderRadius: 12,
705
+ padding: '1rem',
706
+ background: '#fff',
707
+ }}
708
+ >
709
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
710
+ <div style={{ display: 'grid', gridTemplateRows: 'auto auto', gap: '1rem' }}>
711
+ <div
712
+ style={{
713
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
714
+ borderRadius: 10,
715
+ padding: '1rem',
716
+ background: 'var(--cds-layer, #f4f4f4)',
717
+ }}
718
+ >
719
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
720
+ <div style={{ fontSize: '1.1rem', fontWeight: 600 }}>Template Structure</div>
721
+ <Button kind="ghost" size="sm" renderIcon={Renew} onClick={generateFromDefinition}>
722
+ Refresh JSON
723
+ </Button>
724
+ </div>
725
+
726
+ <div
727
+ style={{
728
+ minHeight: '12rem',
729
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
730
+ borderRadius: 8,
731
+ background: '#fff',
732
+ padding: '0.5rem',
733
+ overflow: 'auto',
734
+ }}
735
+ >
736
+ {structureItems.length === 0 ? (
737
+ <div style={{ opacity: 0.7 }}>No sections/rows yet. Generate from definition.</div>
738
+ ) : (
739
+ structureItems.map((item) => {
740
+ if (item.kind === 'group') {
741
+ return null;
742
+ }
743
+
744
+ const { group, row, rowIndex } = item;
745
+ const isSelected = row.id === selectedRowId;
746
+ const isDragOver = row.id === dragOverRowId;
747
+
748
+ const isSectionLabel = row.type === ('section-label' as any);
749
+ const isGroupLabel = row.type === ('group-label' as any);
750
+
751
+ return (
752
+ <div
753
+ key={row.id}
754
+ draggable
755
+ onDragStart={() => handleDragStart(row.id, group.id)}
756
+ onDragEnd={() => {
757
+ setDragState(null);
758
+ setDragOverRowId(null);
759
+ }}
760
+ onDragOver={(e) => {
761
+ e.preventDefault();
762
+ setDragOverRowId(row.id);
763
+ }}
764
+ onDrop={(e) => {
765
+ e.preventDefault();
766
+ handleDropOnRow(group.id, row.id);
767
+ }}
768
+ onClick={() => {
769
+ setSelectedGroupId(group.id);
770
+ setSelectedRowId(row.id);
771
+ }}
772
+ style={{
773
+ display: 'flex',
774
+ alignItems: 'center',
775
+ gap: '0.5rem',
776
+ padding: '0.65rem 0.75rem',
777
+ marginBottom: '0.35rem',
778
+ marginLeft: `${(row.indent ?? 0) * 18}px`,
779
+ borderRadius: 6,
780
+ border: isSelected
781
+ ? '2px solid var(--cds-border-interactive, #0f62fe)'
782
+ : isDragOver
783
+ ? '2px dashed var(--cds-border-interactive, #0f62fe)'
784
+ : '1px solid transparent',
785
+ background: isSelected ? 'var(--cds-layer-selected, #e8f1ff)' : 'transparent',
786
+ cursor: 'pointer',
787
+ }}
788
+ >
789
+ <Draggable size={16} />
790
+ <span
791
+ style={{
792
+ fontWeight: isSectionLabel ? 700 : isGroupLabel ? 600 : 500,
793
+ }}
794
+ >
795
+ {row.label || row.code || 'Untitled row'}
796
+ </span>
797
+
798
+ <Tag
799
+ size="sm"
800
+ type={
801
+ isSectionLabel
802
+ ? 'purple'
803
+ : isGroupLabel
804
+ ? 'cyan'
805
+ : 'gray'
806
+ }
807
+ >
808
+ {isSectionLabel
809
+ ? 'section'
810
+ : isGroupLabel
811
+ ? 'group'
812
+ : row.code || row.type}
813
+ </Tag>
814
+
815
+ {isSectionLabel ? (
816
+ <Button
817
+ size="sm"
818
+ kind="ghost"
819
+ renderIcon={Add}
820
+ onClick={(e) => {
821
+ e.stopPropagation();
822
+ addRowToGroup(group.id);
823
+ }}
824
+ >
825
+ Add Node
826
+ </Button>
827
+ ) : null}
828
+
829
+ <div style={{ marginLeft: 'auto', display: 'flex', gap: '0.25rem' }}>
830
+ <Button
831
+ kind="ghost"
832
+ size="sm"
833
+ hasIconOnly
834
+ iconDescription="Move up"
835
+ renderIcon={ArrowUp}
836
+ disabled={rowIndex === 0}
837
+ onClick={(e) => {
838
+ e.stopPropagation();
839
+ moveRow(group.id, row.id, -1);
840
+ }}
841
+ />
842
+ <Button
843
+ kind="ghost"
844
+ size="sm"
845
+ hasIconOnly
846
+ iconDescription="Move down"
847
+ renderIcon={ArrowDown}
848
+ disabled={rowIndex === group.rows.length - 1}
849
+ onClick={(e) => {
850
+ e.stopPropagation();
851
+ moveRow(group.id, row.id, 1);
852
+ }}
853
+ />
854
+ <Button
855
+ kind="ghost"
856
+ size="sm"
857
+ hasIconOnly
858
+ iconDescription="Indent"
859
+ renderIcon={ArrowRight}
860
+ disabled={(row.indent ?? getMinIndentForRowType(row.type)) >= MAX_INDENT}
861
+ onClick={(e) => {
862
+ e.stopPropagation();
863
+ indentRow(group.id, row.id, 1);
864
+ }}
865
+ />
866
+ <Button
867
+ kind="ghost"
868
+ size="sm"
869
+ hasIconOnly
870
+ iconDescription="Outdent"
871
+ renderIcon={ArrowLeft}
872
+ disabled={(row.indent ?? getMinIndentForRowType(row.type)) <= getMinIndentForRowType(row.type)}
873
+ onClick={(e) => {
874
+ e.stopPropagation();
875
+ indentRow(group.id, row.id, -1);
876
+ }}
877
+ />
878
+ <Button
879
+ kind="ghost"
880
+ size="sm"
881
+ hasIconOnly
882
+ iconDescription="Remove"
883
+ renderIcon={TrashCan}
884
+ onClick={(e) => {
885
+ e.stopPropagation();
886
+ removeRow(group.id, row.id);
887
+ }}
888
+ />
889
+ </div>
890
+ </div>
891
+ );
892
+ })
893
+ )}
894
+ </div>
895
+ </div>
896
+
897
+ <div
898
+ style={{
899
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
900
+ borderRadius: 10,
901
+ padding: '1rem',
902
+ background: '#fff',
903
+ }}
904
+ >
905
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
906
+ <div style={{ fontSize: '1.1rem', fontWeight: 600 }}>JSON Preview</div>
907
+ <div style={{ display: 'flex', gap: '0.35rem' }}>
908
+ <Button kind="ghost" hasIconOnly size="sm" iconDescription="Refresh" renderIcon={Renew} />
909
+ <Button kind="ghost" hasIconOnly size="sm" iconDescription="Copy" renderIcon={Copy} />
910
+ <Button kind="ghost" hasIconOnly size="sm" iconDescription="Download" renderIcon={Download} />
911
+ <Button kind="ghost" hasIconOnly size="sm" iconDescription="Expand" renderIcon={Launch} />
912
+ </div>
913
+ </div>
914
+
915
+ <pre
916
+ style={{
917
+ margin: 0,
918
+ minHeight: '16rem',
919
+ maxHeight: '22rem',
920
+ overflow: 'auto',
921
+ whiteSpace: 'pre-wrap',
922
+ wordBreak: 'break-word',
923
+ fontSize: '0.85rem',
924
+ background: '#f8f8f8',
925
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
926
+ borderRadius: 8,
927
+ padding: '0.75rem',
928
+ }}
929
+ >
930
+ {JSON.stringify(draft, null, 2)}
931
+ </pre>
932
+ </div>
933
+ </div>
934
+
935
+ <div style={{ display: 'grid', gridTemplateRows: 'auto auto', gap: '1rem' }}>
936
+ <div
937
+ style={{
938
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
939
+ borderRadius: 10,
940
+ padding: '1rem',
941
+ background: 'var(--cds-layer, #f4f4f4)',
942
+ }}
943
+ >
944
+ <div style={{ fontSize: '1.1rem', fontWeight: 600 }}>Indicator Properties</div>
945
+ <div style={{ opacity: 0.8, marginBottom: '0.75rem' }}>
946
+ Select an indicator/row to edit properties
947
+ </div>
948
+
949
+ <Tabs
950
+ selectedIndex={
951
+ propTab === 'details' ? 0 : propTab === 'disaggregation' ? 1 : propTab === 'mapping' ? 2 : 3
952
+ }
953
+ onChange={({ selectedIndex }) =>
954
+ setPropTab(selectedIndex === 0 ? 'details' : selectedIndex === 1 ? 'disaggregation' : selectedIndex === 2 ? 'mapping' : 'api')
955
+ }
956
+ >
957
+ <TabList aria-label="Indicator properties tabs">
958
+ <Tab>Details</Tab>
959
+ <Tab>Disaggregation</Tab>
960
+ <Tab>Mapping</Tab>
961
+ <Tab>API</Tab>
962
+ </TabList>
963
+ </Tabs>
964
+
965
+ <div style={{ marginTop: '1rem' }}>
966
+ {!selectedRow || !selectedGroupId ? (
967
+ <div style={{ opacity: 0.7 }}>Select a node to edit.</div>
968
+ ) : propTab === 'details' ? (
969
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
970
+ <TextInput
971
+ id="design-row-label"
972
+ labelText="Label"
973
+ value={selectedRow.label}
974
+ onChange={(e) =>
975
+ updateRow(selectedGroupId, selectedRow.id, { label: (e.target as HTMLInputElement).value })
976
+ }
977
+ />
978
+ <TextInput
979
+ id="design-row-code"
980
+ labelText="Code"
981
+ value={selectedRow.code ?? ''}
982
+ onChange={(e) =>
983
+ updateRow(selectedGroupId, selectedRow.id, { code: (e.target as HTMLInputElement).value })
984
+ }
985
+ />
986
+ <Select
987
+ id="design-row-type"
988
+ labelText="Row type"
989
+ value={selectedRow.type}
990
+ onChange={(e) => {
991
+ const nextType = (e.target as HTMLSelectElement).value;
992
+ const nextRow = applyRowTypeDefaults(selectedRow, nextType);
993
+ updateRow(selectedGroupId, selectedRow.id, nextRow);
994
+ }}
995
+ >
996
+ <SelectItem value="section-label" text="Section Label" />
997
+ <SelectItem value="group-label" text="Group Label" />
998
+ <SelectItem value="indicator" text="Indicator" />
999
+ <SelectItem value="label" text="Label" />
1000
+ <SelectItem value="spacer" text="Spacer" />
1001
+ </Select>
1002
+ <NumberInput
1003
+ id="design-row-indent"
1004
+ label="Indent"
1005
+ min={getMinIndentForRowType(selectedRow.type)}
1006
+ max={MAX_INDENT}
1007
+ value={selectedRow.indent ?? getMinIndentForRowType(selectedRow.type)}
1008
+ onChange={(e) => {
1009
+ const next = Number((e.target as HTMLInputElement).value || getMinIndentForRowType(selectedRow.type));
1010
+ updateRow(selectedGroupId, selectedRow.id, {
1011
+ indent: clampIndentForRowType(next, selectedRow.type),
1012
+ });
1013
+ }}
1014
+ />
1015
+ </div>
1016
+ ) : propTab === 'disaggregation' ? (
1017
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
1018
+ <Checkbox
1019
+ id="design-row-total"
1020
+ labelText="Show total"
1021
+ checked={Boolean(selectedRow.showTotal)}
1022
+ onChange={(checked) =>
1023
+ updateRow(selectedGroupId, selectedRow.id, { showTotal: Boolean(checked) })
1024
+ }
1025
+ />
1026
+ <Checkbox
1027
+ id="design-row-disagg"
1028
+ labelText="Show disaggregation"
1029
+ checked={Boolean(selectedRow.showDisaggregation)}
1030
+ onChange={(checked) =>
1031
+ updateRow(selectedGroupId, selectedRow.id, { showDisaggregation: Boolean(checked) })
1032
+ }
1033
+ />
1034
+ </div>
1035
+ ) : propTab === 'mapping' ? (
1036
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
1037
+ <TextInput
1038
+ id="design-row-key-pattern"
1039
+ labelText="Key pattern"
1040
+ value={selectedRow.keyPattern ?? ''}
1041
+ onChange={(e) =>
1042
+ updateRow(selectedGroupId, selectedRow.id, { keyPattern: (e.target as HTMLInputElement).value })
1043
+ }
1044
+ />
1045
+ <div style={{ opacity: 0.8, fontSize: '0.875rem' }}>
1046
+ Example: {'{code}_{age}_{sex}'}
1047
+ </div>
1048
+ </div>
1049
+ ) : (
1050
+ <div style={{ opacity: 0.8, fontSize: '0.875rem' }}>
1051
+ Node API preview and compile-time bindings will appear here.
1052
+ </div>
1053
+ )}
1054
+ </div>
1055
+ </div>
1056
+
1057
+ <div
1058
+ style={{
1059
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
1060
+ borderRadius: 10,
1061
+ padding: '1rem',
1062
+ background: 'var(--cds-layer, #f4f4f4)',
1063
+ }}
1064
+ >
1065
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
1066
+ <div style={{ fontSize: '1.1rem', fontWeight: 600 }}>Mapping Preview</div>
1067
+ <View size={18} />
1068
+ </div>
1069
+
1070
+ <div
1071
+ style={{
1072
+ border: '1px solid var(--cds-border-subtle, #e0e0e0)',
1073
+ borderRadius: 8,
1074
+ overflow: 'hidden',
1075
+ background: '#fff',
1076
+ }}
1077
+ >
1078
+ <div
1079
+ style={{
1080
+ display: 'grid',
1081
+ gridTemplateColumns: `minmax(220px, 1.3fr) repeat(${mappingPreview.columns.length}, minmax(120px, 1fr))`,
1082
+ borderBottom: '1px solid var(--cds-border-subtle, #e0e0e0)',
1083
+ fontWeight: 600,
1084
+ background: '#fafafa',
1085
+ }}
1086
+ >
1087
+ <div style={{ padding: '0.75rem' }} />
1088
+ {mappingPreview.columns.map((c) => (
1089
+ <div key={c} style={{ padding: '0.75rem' }}>
1090
+ {c}
1091
+ </div>
1092
+ ))}
1093
+ </div>
1094
+
1095
+ {mappingPreview.rows.length === 0 ? (
1096
+ <div style={{ padding: '1rem', opacity: 0.7 }}>No rows to preview.</div>
1097
+ ) : (
1098
+ mappingPreview.rows.map((row: any) =>
1099
+ row.type === 'section-label' ? (
1100
+ <div
1101
+ key={row.id}
1102
+ style={{
1103
+ borderTop: '1px solid var(--cds-border-subtle, #f0f0f0)',
1104
+ background: 'var(--cds-layer-accent, #f4f4f4)',
1105
+ fontWeight: 700,
1106
+ padding: '0.85rem 0.75rem',
1107
+ }}
1108
+ >
1109
+ {row.label}
1110
+ </div>
1111
+ ) : row.type === 'group-label' ? (
1112
+ <div
1113
+ key={row.id}
1114
+ style={{
1115
+ display: 'grid',
1116
+ gridTemplateColumns: `minmax(220px, 1.3fr) repeat(${mappingPreview.columns.length}, minmax(120px, 1fr))`,
1117
+ borderTop: '1px solid var(--cds-border-subtle, #f0f0f0)',
1118
+ background: '#fafafa',
1119
+ }}
1120
+ >
1121
+ <div
1122
+ style={{
1123
+ padding: '0.75rem',
1124
+ fontWeight: 600,
1125
+ paddingLeft: `${12 + row.indent * 18}px`,
1126
+ }}
1127
+ >
1128
+ {row.code ? `${row.code} ${row.label}` : row.label}
1129
+ </div>
1130
+ {mappingPreview.columns.map((_: any, idx: number) => (
1131
+ <div key={idx} style={{ padding: '0.75rem' }} />
1132
+ ))}
1133
+ </div>
1134
+ ) : (
1135
+ <div
1136
+ key={row.id}
1137
+ style={{
1138
+ display: 'grid',
1139
+ gridTemplateColumns: `minmax(220px, 1.3fr) repeat(${mappingPreview.columns.length}, minmax(120px, 1fr))`,
1140
+ borderTop: '1px solid var(--cds-border-subtle, #f0f0f0)',
1141
+ }}
1142
+ >
1143
+ <div
1144
+ style={{
1145
+ padding: '0.75rem',
1146
+ fontWeight: 600,
1147
+ paddingLeft: `${12 + row.indent * 18}px`,
1148
+ }}
1149
+ >
1150
+ {row.label}
1151
+ </div>
1152
+ {row.values.map((v: any, idx: number) => (
1153
+ <div key={idx} style={{ padding: '0.75rem' }}>
1154
+ {v}
1155
+ </div>
1156
+ ))}
1157
+ </div>
1158
+ ),
1159
+ )
1160
+ )}
1161
+ </div>
1162
+ </div>
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ </div>
1167
+ );
1168
+ };
1169
+
1170
+ export default ReportDesignEditor;