From 3f8febfda313e64f254840ade5db8adb4d9eecf9 Mon Sep 17 00:00:00 2001 From: Experiments DB Dev Date: Mon, 6 Jul 2026 15:35:19 -0400 Subject: [PATCH] feat(frontend): listExportableParams enumerates export options --- frontend/src/lib/dataExport.js | 32 +++++++++++++++++++++++++ frontend/tests/dataExport.test.js | 39 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/frontend/src/lib/dataExport.js b/frontend/src/lib/dataExport.js index 6bcbcf2..3158cbb 100644 --- a/frontend/src/lib/dataExport.js +++ b/frontend/src/lib/dataExport.js @@ -21,3 +21,35 @@ export function extractValue(status, param) { return null; } } + +// Build the grouped, ordered list of exportable parameters. +// Session metrics (total, success_rate, then each dynamic count category found +// in the data) come first; then every field defined in the daily template, +// including inactive ones (historical data may exist). +export function listExportableParams(dailyTemplate, statuses) { + const params = [ + { id: '__total__', label: 'Total attempts', group: 'metrics', kind: 'total' }, + { id: '__success_rate__', label: 'Success rate', group: 'metrics', kind: 'success_rate' }, + ]; + + const cats = new Set(); + for (const s of statuses ?? []) { + const counts = s?.analysis_summary?.counts; + if (counts && typeof counts === 'object') { + for (const k of Object.keys(counts)) cats.add(k); + } + } + for (const cat of [...cats].sort()) { + params.push({ id: `__cat__${cat}`, label: cat, group: 'metrics', kind: 'category', category: cat }); + } + + for (const f of dailyTemplate ?? []) { + if (f.builtin) { + params.push({ id: `b:${f.fieldId}`, label: f.label, group: 'daily', kind: 'builtin', statusKey: f.key }); + } else { + params.push({ id: `c:${f.fieldId}`, label: f.label, group: 'daily', kind: 'custom', fieldId: f.fieldId }); + } + } + + return params; +} diff --git a/frontend/tests/dataExport.test.js b/frontend/tests/dataExport.test.js index 02b3084..e97f7f4 100644 --- a/frontend/tests/dataExport.test.js +++ b/frontend/tests/dataExport.test.js @@ -1,4 +1,5 @@ import { extractValue } from '../src/lib/dataExport'; +import { listExportableParams } from '../src/lib/dataExport'; const status = { vitals: 'ok', @@ -31,3 +32,41 @@ describe('extractValue', () => { expect(extractValue({}, { kind: 'builtin', statusKey: 'notes' })).toBeNull(); }); }); + +const dailyTemplate = [ + { fieldId: 'b-vitals', key: 'vitals', label: 'Vitals', builtin: true, active: true }, + { fieldId: 'c-weight', key: 'weight', label: 'Weight (g)', builtin: false, active: false }, +]; +const statuses = [ + { analysis_summary: { counts: { Success: 1, Failure: 0 } } }, + { analysis_summary: { counts: { Success: 2, Other: 1 } } }, + { analysis_summary: null }, +]; + +describe('listExportableParams', () => { + const params = listExportableParams(dailyTemplate, statuses); + it('starts with the two fixed session metrics', () => { + expect(params.slice(0, 2)).toEqual([ + expect.objectContaining({ id: '__total__', label: 'Total attempts', group: 'metrics', kind: 'total' }), + expect.objectContaining({ id: '__success_rate__', label: 'Success rate', group: 'metrics', kind: 'success_rate' }), + ]); + }); + it('adds one metrics entry per dynamic count category, sorted', () => { + const cats = params.filter((p) => p.kind === 'category').map((p) => p.label); + expect(cats).toEqual(['Failure', 'Other', 'Success']); + }); + it('includes every template field (incl. inactive) in the daily group', () => { + const daily = params.filter((p) => p.group === 'daily'); + expect(daily).toEqual([ + expect.objectContaining({ label: 'Vitals', kind: 'builtin', statusKey: 'vitals' }), + expect.objectContaining({ label: 'Weight (g)', kind: 'custom', fieldId: 'c-weight' }), + ]); + }); + it('gives every param a unique id', () => { + const ids = params.map((p) => p.id); + expect(new Set(ids).size).toBe(ids.length); + }); + it('tolerates empty/missing inputs', () => { + expect(listExportableParams(undefined, undefined).length).toBe(2); // just the two fixed metrics + }); +});