-
Notifications
You must be signed in to change notification settings - Fork 16
Subject table download #1210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Subject table download #1210
Changes from 28 commits
1847144
51cc8ff
fe309bc
27a15a2
2dc7552
883f178
b421716
4c0d622
57b70a9
12ae11e
6aa18a8
199a4a0
bb98e7a
96f6f4a
893ba0c
7999b71
7126328
b6c29b1
8c753ff
ddfe864
5940ce0
299fc8d
b6c5bfa
b607bd8
fbfbf94
4d6789a
5ccf057
a0fa764
1766ace
8a685b8
c011c01
cb92937
057d54c
a245fe5
83b1679
f06e4d4
72f323b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { act } from '@testing-library/react'; | ||
| import { afterEach, vi } from 'vitest'; | ||
| import * as zustand from 'zustand'; | ||
| import type { StateCreator, StoreApi, UseBoundStore } from 'zustand'; | ||
|
|
||
| const { create: zCreate, createStore: zCreateStore } = await vi.importActual<typeof zustand>('zustand'); | ||
|
|
||
| // a variable to hold reset functions for all stores declared in the app | ||
| const STORE_RESET_FUNCTIONS = new Set<() => void>(); | ||
|
|
||
| function createUncurried<T>(stateCreator: StateCreator<T>): UseBoundStore<StoreApi<T>> { | ||
| const store = zCreate(stateCreator); | ||
| const initialState = store.getInitialState(); | ||
| STORE_RESET_FUNCTIONS.add(() => { | ||
| store.setState(initialState, true); | ||
| }); | ||
| return store; | ||
| } | ||
|
|
||
| function createStoreUncurried<T>(stateCreator: StateCreator<T>): StoreApi<T> { | ||
| const store = zCreateStore(stateCreator); | ||
| const initialState = store.getInitialState(); | ||
| STORE_RESET_FUNCTIONS.add(() => { | ||
| store.setState(initialState, true); | ||
| }); | ||
| return store; | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| act(() => { | ||
| STORE_RESET_FUNCTIONS.forEach((resetFn) => { | ||
| resetFn(); | ||
| }); | ||
| STORE_RESET_FUNCTIONS.clear(); | ||
| }); | ||
| }); | ||
|
|
||
| export function create<T>(stateCreator: StateCreator<T>): UseBoundStore<StoreApi<T>> { | ||
| if (typeof stateCreator === 'function') { | ||
| return createUncurried(stateCreator); | ||
| } | ||
| return createUncurried as unknown as UseBoundStore<StoreApi<T>>; | ||
| } | ||
|
|
||
| export function createStore<T>(stateCreator: StateCreator<T>): StoreApi<T> { | ||
| if (typeof stateCreator === 'function') { | ||
| return createStoreUncurried(stateCreator); | ||
| } | ||
| return createStoreUncurried as unknown as StoreApi<T>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| import React from 'react'; | ||
|
|
||
| import { act } from '@testing-library/react'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { useInstrumentVisualization } from '../useInstrumentVisualization'; | ||
|
|
||
| const mockUseInstrument = vi.hoisted(() => | ||
| vi.fn(() => ({ | ||
| internal: { | ||
| name: 'test' | ||
| } | ||
| })) | ||
| ); | ||
david-roper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const mockRecords = [ | ||
| { | ||
| __date__: new Date(), | ||
| __time__: new Date().getTime(), | ||
| someValue: 'abc' | ||
| } | ||
| ]; | ||
|
|
||
| vi.mock('@/hooks/useInstrument', () => ({ | ||
| useInstrument: mockUseInstrument | ||
| })); | ||
|
|
||
| const mockStore = { | ||
| useAppStore: vi.fn(() => ({ | ||
| store: { | ||
| currentGroup: 'testGroup', | ||
| currentUser: 'testUser' | ||
| } | ||
| })) | ||
| }; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const mockBasicIsoString = '2025-04-30'; | ||
david-roper marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const mockUseDownload = vi.fn(); | ||
david-roper marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const mockNotification = { | ||
| useNotificationsStore: vi.fn() | ||
| }; | ||
| const mockTranslation = { | ||
| useTranslation: vi.fn() | ||
| }; | ||
|
|
||
| const mockInfoQuery = { | ||
| useInstrumentInfoQuery: vi.fn() | ||
| }; | ||
david-roper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const mockInstrumentRecords = { | ||
| useInstrumentRecords: vi.fn() | ||
| }; | ||
|
|
||
| vi.mock('@/store', () => ({ | ||
| useAppStore: () => mockStore | ||
| })); | ||
|
|
||
| vi.mock('@douglasneuroinformatics/libui/hooks', () => ({ | ||
| useDownload: () => mockUseDownload, | ||
| useNotificationsStore: () => mockNotification, | ||
| useTranslation: () => mockTranslation | ||
| })); | ||
david-roper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| vi.mock('react', async (importOriginal) => { | ||
| const actual = await importOriginal<typeof React>(); | ||
| return { | ||
| ...actual, | ||
| useEffect: vi.fn(), | ||
david-roper marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| useMemo: vi.fn(), | ||
| useState: vi.fn(() => [mockRecords, 'setRecords']) | ||
| }; | ||
| }); | ||
david-roper marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| vi.mock('@/hooks/useInstrumentInfoQuery', () => ({ | ||
| useInstrumentInfoQuery: () => mockInfoQuery | ||
| })); | ||
|
|
||
| vi.mock('@/hooks/useInstrumentRecords', () => ({ | ||
| useInstrumentRecords: () => mockInstrumentRecords | ||
| })); | ||
|
|
||
| vi.mock('@douglasneuroinformatics/libjs', () => ({ | ||
| toBasicISOString: vi.fn(() => mockBasicIsoString) | ||
david-roper marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| })); | ||
|
|
||
| describe('useInstrumentVisualization tests', () => { | ||
david-roper marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| afterEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe('CSV', () => { | ||
| it('Should download', () => { | ||
| vi.spyOn(React, 'useEffect').mockImplementation((fn) => fn()); | ||
| const { dl, records } = useInstrumentVisualization({ | ||
| params: { subjectId: 'testId' } | ||
| }); | ||
| act(() => dl('CSV')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockUseDownload).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockUseDownload.mock.calls[0]; | ||
| expect(filename).toContain('.csv'); | ||
| const csvContents = getContentFn(); | ||
| expect(csvContents).toMatch('subjectId,Date,someValue\r\ntestId,2025-04-30,abc'); | ||
| }); | ||
| }); | ||
| describe('TSV', () => { | ||
| it('Should download', () => { | ||
| vi.spyOn(React, 'useEffect').mockImplementation((fn) => fn()); | ||
| const { dl, records } = useInstrumentVisualization({ | ||
| params: { subjectId: 'testId' } | ||
| }); | ||
| act(() => dl('TSV')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockUseDownload).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockUseDownload.mock.calls[0]; | ||
| expect(filename).toContain('.tsv'); | ||
| const tsvContents = getContentFn(); | ||
| expect(tsvContents).toMatch('subjectId\tDate\tsomeValue\r\ntestId\t2025-04-30\tabc'); | ||
| }); | ||
| }); | ||
| describe('CSV Long', () => { | ||
| it('Should download', () => { | ||
| vi.spyOn(React, 'useEffect').mockImplementation((fn) => fn()); | ||
| const { dl, records } = useInstrumentVisualization({ | ||
| params: { subjectId: 'testId' } | ||
| }); | ||
| act(() => dl('CSV Long')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockUseDownload).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockUseDownload.mock.calls[0]; | ||
| expect(filename).toContain('.csv'); | ||
| const csvLongContents = getContentFn(); | ||
| expect(csvLongContents).toMatch('Date,SubjectID,Value,Variable\r\n2025-04-30,testId,abc,someValue'); | ||
| }); | ||
| }); | ||
| describe('TSV Long', () => { | ||
| it('Should download', () => { | ||
| vi.spyOn(React, 'useEffect').mockImplementation((fn) => fn()); | ||
| const { dl, records } = useInstrumentVisualization({ | ||
| params: { subjectId: 'testId' } | ||
| }); | ||
| act(() => dl('TSV Long')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockUseDownload).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockUseDownload.mock.calls[0]; | ||
| expect(filename).toMatch('.tsv'); | ||
| const tsvLongContents = getContentFn(); | ||
| expect(tsvLongContents).toMatch('Date\tSubjectID\tValue\tVariable\r\n2025-04-30\ttestId\tabc\tsomeValue'); | ||
| }); | ||
| }); | ||
| describe('JSON', () => { | ||
| it('Should download', async () => { | ||
| vi.spyOn(React, 'useEffect').mockImplementation((fn) => fn()); | ||
| const { dl, records } = useInstrumentVisualization({ | ||
| params: { subjectId: 'testId' } | ||
| }); | ||
| act(() => dl('JSON')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockUseDownload).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockUseDownload.mock.calls[0]; | ||
| expect(filename).toMatch('.json'); | ||
| const jsonContents = await getContentFn(); | ||
| expect(jsonContents).toContain('"someValue": "abc"'); | ||
| expect(jsonContents).toContain('"subjectID": "testId"'); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,10 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useMemo, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { toBasicISOString } from '@douglasneuroinformatics/libjs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useDownload, useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { AnyUnilingualScalarInstrument, InstrumentKind } from '@opendatacapture/runtime-core'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { omit } from 'lodash-es'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { unparse } from 'papaparse'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useInstrument } from '@/hooks/useInstrument'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -50,7 +52,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dl = (option: 'JSON' | 'TSV') => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dl = (option: 'CSV' | 'CSV Long' | 'JSON' | 'TSV' | 'TSV Long') => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!instrument) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -63,24 +65,113 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instrument.internal.edition | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }_${new Date().toISOString()}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const exportRecords = records.map((record) => omit(record, ['__date__', '__time__'])); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const exportRecords = records.map((record) => omit(record, ['__time__'])); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const makeWideRows = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const columnNames = Object.keys(exportRecords[0]!); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
david-roper marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return exportRecords.map((item) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const obj: { [key: string]: any } = { subjectId: params.subjectId }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const key of columnNames) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const val = item[key]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (key === '__date__') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj.Date = toBasicISOString(val as Date); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj[key] = typeof val === 'object' ? JSON.stringify(val) : val; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return obj; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const makeLongRows = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const longRecord: { [key: string]: any }[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exportRecords.forEach((item) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let date: Date; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.entries(item).forEach(([objKey, objVal]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (objKey === '__date__') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| date = objVal as Date; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Array.isArray(objVal)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| objVal.forEach((arrayItem) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.entries(arrayItem as object).forEach(([arrKey, arrItem]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| longRecord.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Date: toBasicISOString(date), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SubjectID: params.subjectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Variable: `${objKey}-${arrKey}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
david-roper marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Value: arrItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| longRecord.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Date: toBasicISOString(date), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SubjectID: params.subjectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Value: objVal, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Variable: objKey | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return longRecord; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+86
to
+122
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: date depends on object iteration order; can be undefined. Don’t rely on encountering 'date' first. Read it once, then iterate the rest. Apply: - const makeLongRows = () => {
- const longRecord: { [key: string]: any }[] = [];
-
- exportRecords.forEach((item) => {
- let date: Date;
-
- Object.entries(item).forEach(([objKey, objVal]) => {
- if (objKey === '__date__') {
- date = objVal as Date;
- return;
- }
- if (Array.isArray(objVal)) {
- objVal.forEach((arrayItem) => {
- Object.entries(arrayItem as object).forEach(([arrKey, arrItem]) => {
- longRecord.push({
- Date: toBasicISOString(date),
- SubjectID: params.subjectId,
- Variable: `${objKey}-${arrKey}`,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
- Value: arrItem
- });
- });
- });
- } else {
- longRecord.push({
- Date: toBasicISOString(date),
- SubjectID: params.subjectId,
- Value: objVal,
- Variable: objKey
- });
- }
- });
- });
-
- return longRecord;
- };
+ const makeLongRows = () => {
+ return exportRecords.flatMap((item) => {
+ const date = (item as any).__date__ as Date;
+ const base = { Date: toBasicISOString(date), SubjectID: params.subjectId };
+ return Object.entries(item)
+ .filter(([k]) => k !== '__date__')
+ .flatMap(([objKey, objVal]) => {
+ if (Array.isArray(objVal)) {
+ return (objVal as any[]).flatMap((arrayItem) =>
+ Object.entries(arrayItem as object).map(([arrKey, arrItem]) => ({
+ ...base,
+ Variable: `${objKey}-${arrKey}`,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
+ Value: arrItem
+ }))
+ );
+ }
+ return [
+ {
+ ...base,
+ Variable: objKey,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
+ Value: objVal
+ }
+ ];
+ });
+ });
+ };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parseHelper = (rows: unknown[], delimiter: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return unparse(rows, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delimiter: delimiter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| escapeChar: '"', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quoteChar: '"', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quotes: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skipEmptyLines: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (option) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'JSON': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'CSV': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void download(`${baseFilename}.csv`, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = makeWideRows(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const csv = parseHelper(rows, ','); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return csv; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'CSV Long': { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void download(`${baseFilename}.csv`, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = makeLongRows(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const csv = parseHelper(rows, ','); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return csv; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'JSON': { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exportRecords.map((item) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
david-roper marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| item.subjectID = params.subjectId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void download(`${baseFilename}.json`, () => Promise.resolve(JSON.stringify(exportRecords, null, 2))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'TSV': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void download(`${baseFilename}.tsv`, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const columnNames = Object.keys(exportRecords[0]!).join('\t'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = exportRecords | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((item) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.values(item) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((val) => JSON.stringify(val)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join('\t') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join('\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return columnNames + '\n' + rows; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = makeWideRows(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tsv = parseHelper(rows, '\t'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tsv; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'TSV Long': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void download(`${baseFilename}.tsv`, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = makeLongRows(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tsv = parseHelper(rows, '\t'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tsv; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.