diff --git a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts index c86bf8190..bfa1e7478 100644 --- a/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts +++ b/apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts @@ -20,6 +20,8 @@ const mockStore = { const mockDownloadFn = vi.fn(); +const mockExcelDownloadFn = vi.hoisted(() => vi.fn()); + const mockInfoQuery = { data: [] }; @@ -53,6 +55,10 @@ vi.mock('@/hooks/useInstrumentInfoQuery', () => ({ useInstrumentInfoQuery: () => mockInfoQuery })); +vi.mock('@/utils/excel', () => ({ + downloadSubjectTableExcel: mockExcelDownloadFn +})); + vi.mock('@/hooks/useInstrumentRecords', () => ({ useInstrumentRecords: () => mockInstrumentRecords })); @@ -124,6 +130,51 @@ describe('useInstrumentVisualization', () => { ); }); }); + describe('Excel', () => { + it('Should download', () => { + const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); + const { dl, records } = result.current; + act(() => dl('Excel')); + expect(records).toBeDefined(); + expect(mockExcelDownloadFn).toHaveBeenCalledTimes(1); + const [filename, getContentFn] = mockExcelDownloadFn.mock.calls[0] ?? []; + expect(filename).toMatch('.xlsx'); + const excelContents = getContentFn; + + expect(excelContents).toEqual([ + { + GroupID: 'testGroupId', + subjectId: 'testId', + // eslint-disable-next-line perfectionist/sort-objects + Date: '2025-04-30', + someValue: 'abc' + } + ]); + }); + }); + describe('Excel Long', () => { + it('Should download', () => { + const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); + const { dl, records } = result.current; + act(() => dl('Excel Long')); + expect(records).toBeDefined(); + expect(mockExcelDownloadFn).toHaveBeenCalledTimes(1); + + const [filename, getContentFn] = mockExcelDownloadFn.mock.calls[0] ?? []; + expect(filename).toMatch('.xlsx'); + const excelContents = getContentFn; + + expect(excelContents).toEqual([ + { + Date: '2025-04-30', + GroupID: 'testGroupId', + SubjectID: 'testId', + Value: 'abc', + Variable: 'someValue' + } + ]); + }); + }); describe('JSON', () => { it('Should download', async () => { const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); diff --git a/apps/web/src/hooks/useInstrumentVisualization.ts b/apps/web/src/hooks/useInstrumentVisualization.ts index 14efbad75..48dad2e90 100644 --- a/apps/web/src/hooks/useInstrumentVisualization.ts +++ b/apps/web/src/hooks/useInstrumentVisualization.ts @@ -11,6 +11,7 @@ import { useInstrument } from '@/hooks/useInstrument'; import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useInstrumentRecords } from '@/hooks/useInstrumentRecords'; import { useAppStore } from '@/store'; +import { downloadSubjectTableExcel } from '@/utils/excel'; type InstrumentVisualizationRecord = { [key: string]: unknown; @@ -53,7 +54,7 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio } }); - const dl = (option: 'CSV' | 'CSV Long' | 'JSON' | 'TSV' | 'TSV Long') => { + const dl = (option: 'CSV' | 'CSV Long' | 'Excel' | 'Excel Long' | 'JSON' | 'TSV' | 'TSV Long') => { if (!instrument) { notifications.addNotification({ message: t('errors.noInstrumentSelected'), type: 'error' }); return; @@ -157,6 +158,16 @@ export function useInstrumentVisualization({ params }: UseInstrumentVisualizatio }); break; } + case 'Excel': { + const rows = makeWideRows(); + downloadSubjectTableExcel(`${baseFilename}.xlsx`, rows); + break; + } + case 'Excel Long': { + const rows = makeLongRows(); + downloadSubjectTableExcel(`${baseFilename}.xlsx`, rows); + break; + } case 'JSON': { exportRecords.map((item) => { item.subjectID = params.subjectId; diff --git a/apps/web/src/routes/_app/datahub/$subjectId/table.tsx b/apps/web/src/routes/_app/datahub/$subjectId/table.tsx index 82249c865..dac5dd1b7 100644 --- a/apps/web/src/routes/_app/datahub/$subjectId/table.tsx +++ b/apps/web/src/routes/_app/datahub/$subjectId/table.tsx @@ -38,7 +38,7 @@ const RouteComponent = () => { widthFull data-spotlight-type="export-data-dropdown" disabled={!instrumentId} - options={['TSV', 'TSV Long', 'JSON', 'CSV', 'CSV Long']} + options={['TSV', 'TSV Long', 'JSON', 'CSV', 'CSV Long', 'Excel', 'Excel Long']} title={t('core.download')} triggerClassName="min-w-32" onSelection={dl} diff --git a/apps/web/src/utils/__tests__/excel.test.ts b/apps/web/src/utils/__tests__/excel.test.ts index 6cbf9de37..79e699526 100644 --- a/apps/web/src/utils/__tests__/excel.test.ts +++ b/apps/web/src/utils/__tests__/excel.test.ts @@ -1,9 +1,15 @@ import { describe, expect, it } from 'vitest'; -import { downloadExcel } from '../excel'; +import { downloadExcel, downloadSubjectTableExcel } from '../excel'; describe('downloadExcel', () => { it('should be defined', () => { expect(downloadExcel).toBeDefined(); }); }); + +describe('downloadSujectTableExcel', () => { + it('should be defined', () => { + expect(downloadSubjectTableExcel).toBeDefined(); + }); +}); diff --git a/apps/web/src/utils/excel.ts b/apps/web/src/utils/excel.ts index b420e82ae..5d320b34a 100644 --- a/apps/web/src/utils/excel.ts +++ b/apps/web/src/utils/excel.ts @@ -6,3 +6,9 @@ export function downloadExcel(filename: string, recordsExport: InstrumentRecords utils.book_append_sheet(workbook, utils.json_to_sheet(recordsExport), 'ULTRA_LONG'); writeFileXLSX(workbook, filename); } + +export function downloadSubjectTableExcel(filename: string, records: { [key: string]: any }[]) { + const workbook = utils.book_new(); + utils.book_append_sheet(workbook, utils.json_to_sheet(records), 'ULTRA_LONG'); + writeFileXLSX(workbook, filename); +}