diff --git a/.github/workflows/release-package.yaml b/.github/workflows/release-package.yaml new file mode 100644 index 00000000..738a754e --- /dev/null +++ b/.github/workflows/release-package.yaml @@ -0,0 +1,35 @@ +#name: Node.js Package +# +#on: +# push: +# branches: +# - develop +# +#jobs: +# build: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v4 +# - uses: actions/setup-node@v3 +# with: +# node-version: 18.15.0 +# - run: npm ci +# - run: npm test +# +# publish-gpr: +# needs: build +# runs-on: ubuntu-latest +# permissions: +# packages: write +# contents: read +# steps: +# - uses: actions/checkout@v4 +# - uses: actions/setup-node@v3.4.1 +# with: +# node-version: 18.15.0 +# registry-url: https://npm.pkg.github.com/ +# - run: npm ci +# - run: npm publish --workspace packages/core +# env: +# NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} +# diff --git a/package-lock.json b/package-lock.json index 1ba31572..f414f785 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@swc/core": "^1.3.62", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.1.2", - "@types/jest": "^29.4.0", + "@types/jest": "^29.5.12", "@types/node": "20.4.1", "@types/react": "^18.2.21", "@typescript-eslint/eslint-plugin": "^5.31.0", @@ -46,7 +46,7 @@ "rollup-plugin-swc3": "^0.10.2", "rollup-swc-preserve-directives": "^0.5.0", "terser-webpack-plugin": "^5.3.3", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", "typescript": "5.0.2" }, @@ -9592,6 +9592,19 @@ "react-dom": "^18.0.0" } }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -9943,6 +9956,12 @@ "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", "dev": true }, + "node_modules/@types/flat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.5.tgz", + "integrity": "sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -10075,6 +10094,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonpath-plus": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonpath-plus/-/jsonpath-plus-5.0.5.tgz", + "integrity": "sha512-aaqqDf5LcGOtAfEntO5qKZS6ixT0MpNhUXNwbVPdLf7ET9hKsufJq+buZr7eXSnWoLRyGzVj2Yz2hbjVSK3wsA==", + "dev": true + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -23551,11 +23576,11 @@ ] }, "node_modules/jsonpath-plus": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", - "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-8.0.0.tgz", + "integrity": "sha512-+AOBHcQvRr8DcWVIkfOCCCLSlYgQuNZ+gFNqwkBrNpdUfdfkcrbO4ml3F587fWUMFOmoy6D9c+5wrghgjN3mbg==", "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/JSONStream": { @@ -37282,7 +37307,10 @@ "@swc/wasm": "^1.3.85", "@testing-library/react": "^14.0.0", "@types/estree": "^1.0.0", + "@types/flat": "^5.0.3", "@types/isomorphic-fetch": "^0.0.36", + "@types/jest": "^29.5.12", + "@types/jsonpath-plus": "5.0.5", "@types/lodash": "^4.14.191", "@types/papaparse": "^5.3.14", "@types/uuid": "^9.0.0", @@ -37292,7 +37320,7 @@ "rollup": "^3.29.2", "rollup-plugin-dts": "^6.0.2", "rollup-plugin-peer-deps-external": "^2.2.4", - "ts-jest": "^29.0.3" + "ts-jest": "^29.1.2" }, "peerDependencies": { "react": "^18.2.0", @@ -37363,7 +37391,7 @@ "@mdx-js/loader": "^2.1.5", "@mdx-js/mdx": "^2.1.5", "@next/mdx": "^14.1.0", - "@reduxjs/toolkit": "^1.9.3", + "@reduxjs/toolkit": "^1.9.5", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", @@ -37381,7 +37409,7 @@ "gray-matter": "^4.0.3", "jose": "^4.13.1", "js-cookie": "^3.0.5", - "jsonpath-plus": "^7.2.0", + "jsonpath-plus": "^8.0.0", "mantine-react-table": "^1.3.1", "minisearch": "^6.1.0", "next-compose-plugins": "^2.2.1", @@ -37416,6 +37444,7 @@ "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-terser": "^0.4.3", "@svgr/webpack": "^8.1.0", + "@testing-library/user-event": "^14.5.2", "@types/color": "^3.0.3", "@types/estree": "^1.0.0", "@types/file-saver": "^2.0.5", diff --git a/package.json b/package.json index c6bd257c..dffedfcc 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@swc/core": "^1.3.62", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.1.2", - "@types/jest": "^29.4.0", + "@types/jest": "^29.5.12", "@types/node": "20.4.1", "@types/react": "^18.2.21", "@typescript-eslint/eslint-plugin": "^5.31.0", @@ -61,7 +61,7 @@ "rollup-plugin-swc3": "^0.10.2", "rollup-swc-preserve-directives": "^0.5.0", "terser-webpack-plugin": "^5.3.3", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", "typescript": "5.0.2" } diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts index 325023ec..f44d67c9 100644 --- a/packages/core/jest.config.ts +++ b/packages/core/jest.config.ts @@ -1,6 +1,10 @@ module.exports = { preset: 'ts-jest', - testEnvironment: 'jsdom', + testEnvironment: 'node', + transform: { + 'node_modules/(flat)/.+\\.(j|t)s?$': 'ts-jest', + }, + transformIgnorePatterns: ['node_modules/(?!flat)/'], globalSetup: '/setupTests.ts', moduleNameMapper: { '^@/core/(.*)$': '/src/$1', @@ -8,5 +12,5 @@ module.exports = { modulePaths: [''], globals: { fetch: global.fetch, - } + }, }; diff --git a/packages/core/package.json b/packages/core/package.json index 4ce48f51..df4c55ca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,7 +23,9 @@ "scripts": { "compile": "tsc", "clean": "rimraf dist", + "types": "tsc --emitDeclarationOnly", "build": "npm run clean && npm run compile && rollup --config rollup.config.mjs", + "build:clean": "npm run clean && npm run compile && npm run types && rollup --config rollup.config.mjs", "build:watch": "npm run compile && npm run build -- --watch", "test": "jest unit", "test:watch": "jest unit --watch", @@ -42,9 +44,12 @@ "@swc/wasm": "^1.3.85", "@testing-library/react": "^14.0.0", "@types/estree": "^1.0.0", + "@types/jsonpath-plus": "5.0.5", "@types/isomorphic-fetch": "^0.0.36", + "@types/jest": "^29.5.12", "@types/lodash": "^4.14.191", "@types/papaparse": "^5.3.14", + "@types/flat": "^5.0.3", "@types/uuid": "^9.0.0", "eslint": "^8.36.0", "jest": "^29.7.0", @@ -52,7 +57,7 @@ "rollup": "^3.29.2", "rollup-plugin-dts": "^6.0.2", "rollup-plugin-peer-deps-external": "^2.2.4", - "ts-jest": "^29.0.3" + "ts-jest": "^29.1.2" }, "dependencies": { "flat": "^6.0.1", diff --git a/packages/core/rollup.config.mjs b/packages/core/rollup.config.mjs index d8279812..23b3103c 100644 --- a/packages/core/rollup.config.mjs +++ b/packages/core/rollup.config.mjs @@ -19,6 +19,8 @@ const globals = { 'react-cookie': 'reactCookie', 'swr': 'swr', 'jsonpath-plus': 'jsonpathPlus', + 'flat': 'flat', + 'papaparse': 'papaparse', }; const config = [ diff --git a/packages/core/src/features/cohort/cohortSlice.ts b/packages/core/src/features/cohort/cohortSlice.ts index e996df90..fd19f172 100644 --- a/packages/core/src/features/cohort/cohortSlice.ts +++ b/packages/core/src/features/cohort/cohortSlice.ts @@ -94,7 +94,7 @@ export const cohortSlice = createSlice({ state: Draft, action: PayloadAction ) => { - const { [action.payload]: _a, ...updated } = state.cohort.filters[action.payload].root; + const { [action.payload]: _rest, ...updated } = state.cohort.filters[action.payload].root; return { cohort: { ...state.cohort, ...{ ...state.cohort.filters, [action.payload]: updated } }, }; @@ -131,6 +131,7 @@ export const selectIndexedFilterByName = ( return state.cohorts.cohort.filters[index]?.root[name]; }; +const EmptyFilterSet: FilterSet = { mode: 'and', root: {} }; /** * Select a filter from the index. * returns undefined. @@ -141,7 +142,7 @@ export const selectIndexFilters = ( state: CoreState, index: string ): FilterSet => { - return state.cohorts.cohort.filters?.[index] ?? { mode: 'and', root: {}}; // TODO: check if this is undefined + return state.cohorts.cohort.filters?.[index] ?? EmptyFilterSet; // TODO: check if this is undefined }; export const cohortReducer = cohortSlice.reducer; diff --git a/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts b/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts index 4acfe5c6..71ea5641 100644 --- a/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts +++ b/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts @@ -29,7 +29,7 @@ describe('resolveDRSWithDataGUISOrg', () => { it('should return empty object if input is an empty array', async () => { const guidsForHostnameResolution: string[] = []; - await expect(resolveDRSWithDataGUISOrg(guidsForHostnameResolution)).toEqual( + expect(resolveDRSWithDataGUISOrg(guidsForHostnameResolution)).toEqual( expect.objectContaining({}), ); }); diff --git a/packages/core/src/features/guppy/conversion.ts b/packages/core/src/features/guppy/conversion.ts index 7d9e9886..777789e0 100644 --- a/packages/core/src/features/guppy/conversion.ts +++ b/packages/core/src/features/guppy/conversion.ts @@ -10,7 +10,7 @@ import Papa, { UnparseConfig } from 'papaparse'; * @param {JSON} json */ export function flattenJson(json: JSONObject) { - const flattenedJson : JSONArray = []; + const flattenedJson: JSONArray = []; Object.keys(json).forEach((key) => { flattenedJson.push(flatten(json[key], { delimiter: '_' })); }); @@ -28,7 +28,7 @@ export async function conversion(json: JSONArray, config: UnparseConfig) { /** * Converts JSON to a specified file format. - * Defaultes to JSON if file format is not supported. + * Defaults to JSON if file format is not supported. * @param {JSON} json * @param {string} format */ @@ -40,7 +40,7 @@ export async function jsonToFormat( const flatJson = await flattenJson(json); const data = await conversion(flatJson, { delimiter: FILE_DELIMITERS[format] as string, - } ); + }); return data; } return json; diff --git a/packages/core/src/features/guppy/guppyDownloadSlice.ts b/packages/core/src/features/guppy/guppyDownloadSlice.ts index 6ad77c03..b7375007 100644 --- a/packages/core/src/features/guppy/guppyDownloadSlice.ts +++ b/packages/core/src/features/guppy/guppyDownloadSlice.ts @@ -1,48 +1,36 @@ import { gen3Api } from '../gen3'; -import { GEN3_API } from '../../constants'; -import { FilterSet } from '../filters/types'; -import { Accessibility } from '../../constants'; +import { GEN3_GUPPY_API } from '../../constants'; import { convertFilterSetToGqlFilter, GQLFilter } from '../filters'; +import { BaseGuppyDataRequest, GuppyDownloadDataParams } from './types'; + +export interface GuppyDownloadDataQueryParams extends BaseGuppyDataRequest { + filter: GQLFilter; +} interface DownloadRequestStatus { readonly status: string; readonly message: string; } -interface BaseDownloadRequest { - type: string; - accessibility?: Accessibility; - fields?: string[]; - sort?: string[]; - format?: string; -} - -interface DownloadQueryParams extends BaseDownloadRequest { - filters: FilterSet; -} - -interface DownloadRequestParams extends BaseDownloadRequest { - readonly filter: GQLFilter; -} - export const downloadRequestApi = gen3Api.injectEndpoints({ endpoints: (builder) => ({ - downloadFromGuppy: builder.query({ + downloadFromGuppy: builder.mutation({ query: ({ type, - filters, + filter, accessibility, fields, sort, - }: DownloadQueryParams) => { - const queryBody: DownloadRequestParams = { - filter: convertFilterSetToGqlFilter(filters), + }: GuppyDownloadDataParams) => { + const queryBody: GuppyDownloadDataQueryParams = { + filter: convertFilterSetToGqlFilter(filter), ...{ type, accessibility, fields, sort }, }; return { - url: `${GEN3_API}/guppy/download`, + url: `${GEN3_GUPPY_API}/download`, method: 'POST', queryBody, + cache: 'no-cache', }; }, transformResponse: (response: DownloadRequestStatus) => { @@ -52,4 +40,4 @@ export const downloadRequestApi = gen3Api.injectEndpoints({ }), }); -export const { useDownloadFromGuppyQuery } = downloadRequestApi; +export const { useDownloadFromGuppyMutation } = downloadRequestApi; diff --git a/packages/core/src/features/guppy/guppySlice.ts b/packages/core/src/features/guppy/guppySlice.ts index db275716..0002db69 100644 --- a/packages/core/src/features/guppy/guppySlice.ts +++ b/packages/core/src/features/guppy/guppySlice.ts @@ -101,8 +101,6 @@ interface QueryCountsParams { accessibility?: Accessibility; } - - const explorerApi = guppyApi.injectEndpoints({ endpoints: (builder) => ({ getAllFieldsForType: builder.query({ diff --git a/packages/core/src/features/guppy/index.ts b/packages/core/src/features/guppy/index.ts index 5353300e..54bc7ede 100644 --- a/packages/core/src/features/guppy/index.ts +++ b/packages/core/src/features/guppy/index.ts @@ -1,3 +1,21 @@ export * from './guppylApi'; export * from './guppySlice'; -export * from './guppyDownloadSlice'; +import { downloadFromGuppy } from './utils'; +import { useDownloadFromGuppyMutation } from './guppyDownloadSlice'; +import { + type GuppyDownloadDataParams, + type BaseGuppyDataRequest, + type GuppyActionFunction, + type GuppyActionFunctionParams, + type GuppyDownloadActionFunctionParams, +} from './types'; + +export { + type BaseGuppyDataRequest, + type GuppyDownloadDataParams, + type GuppyActionFunctionParams, + type GuppyActionFunction, + type GuppyDownloadActionFunctionParams, + downloadFromGuppy, + useDownloadFromGuppyMutation, +}; diff --git a/packages/core/src/features/guppy/tests/downloadFromGuppy.unit.test.ts b/packages/core/src/features/guppy/tests/downloadFromGuppy.unit.test.ts new file mode 100644 index 00000000..b76ff86d --- /dev/null +++ b/packages/core/src/features/guppy/tests/downloadFromGuppy.unit.test.ts @@ -0,0 +1,51 @@ +import { downloadFromGuppy } from '../utils'; +import { Accessibility } from '../../../constants'; +import { DownloadFromGuppyParams, GuppyDownloadDataParams } from '../types'; + +describe('Test for downloadFromGuppy function', () => { + const onDoneMock = jest.fn(); + const onStartMock = jest.fn(); + const onErrorMock = jest.fn(); + const mockParameters: GuppyDownloadDataParams = { + type: 'file', + filter: { mode: 'and', root: {} }, + accessibility: Accessibility.ALL, + fields: ['file.mdSum'], + sort: ['asc:file'], + format: 'json', + }; + + const downloadOptions: DownloadFromGuppyParams = { + parameters: mockParameters, + onStart: onStartMock, + onDone: onDoneMock, + onError: onErrorMock, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call the function correctly', () => { + downloadFromGuppy(downloadOptions); + expect(onStartMock).toHaveBeenCalledTimes(1); + expect(downloadFromGuppy).toHaveBeenCalledWith(downloadOptions); + }); + + it('test for onDone function', () => { + downloadFromGuppy(downloadOptions); + expect(onDoneMock).toHaveBeenCalledTimes(1); + }); + + it('test for onError function', () => { + downloadFromGuppy(downloadOptions); + expect(onErrorMock).toHaveBeenCalledTimes(0); + }); + + // mock the fetch function + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({}), + }), + ) as jest.Mock; +}); diff --git a/packages/core/src/features/guppy/tests/jsonToFormat.unit.test.ts b/packages/core/src/features/guppy/tests/jsonToFormat.unit.test.ts new file mode 100644 index 00000000..ee1c1932 --- /dev/null +++ b/packages/core/src/features/guppy/tests/jsonToFormat.unit.test.ts @@ -0,0 +1,1692 @@ +import { jsonToFormat } from '../conversion'; +import { JSONObject } from '../../../types'; +import * as flat from 'flat'; +import { FILE_DELIMITERS } from '../../../constants'; + +describe('jsonToFormat function', () => { + afterEach(() => jest.clearAllMocks()); + + it('should return JSON in specified file format', async () => { + const jsonInput: JSONObject = { key1: 'value1', key2: 'value2' }; + + const jsonInput2 = { + data: { + case: [ + { + submitter_id: '639127-000542', + sex: 'Not Reported', + age_at_index: 79, + index_event: 'First COVID test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '482', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 80, + body_part_examined: ['CHEST'], + days_to_study: -52, + loinc_code: '43468-8', + loinc_contrast: null, + loinc_long_common_name: 'XR Unspecified body region Views', + loinc_method: 'XR', + loinc_system: 'Unspecified', + study_description: null, + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.671385.4752', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 1, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-009181', + sex: 'Not Reported', + age_at_index: 29, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '946', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 29, + body_part_examined: ['CHEST'], + days_to_study: 322, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.241777105162290781131991169908', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: 366, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 336, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 320, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 147, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 293, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 499, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 324, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 287, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 403, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: [ + { + days_to_medication_start: 213, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 711, + days_to_medication_end: null, + dose_sequence_number: 4, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 453, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 234, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 860, + days_to_medication_end: null, + dose_sequence_number: 5, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.252551449607234369736877729753', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '514382-012965', + sex: 'Not Reported', + age_at_index: 36, + index_event: 'COVID-19 Test', + race: 'Other', + ethnicity: 'Hispanic or Latino', + zip: 'US', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 36, + body_part_examined: ['CHEST'], + days_to_study: 45, + loinc_code: '30745-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Views', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'Chest', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.1567317', + }, + { + age_at_imaging: 36, + body_part_examined: ['PORT CHEST'], + days_to_study: 1, + loinc_code: '36554-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Single view', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VW, FRONTAL', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.1381291', + }, + { + age_at_imaging: 36, + body_part_examined: ['PORT CHEST'], + days_to_study: -2, + loinc_code: '36554-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Single view', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VW, FRONTAL', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.1712555', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 453, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: null, + test_days_from_index: 407, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 3, + _ct_series_file_count: 0, + _dx_series_file_count: 5, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-012398', + sex: 'Not Reported', + age_at_index: 79, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 79, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.310464004523196691764117439716', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: [ + { + days_to_medication_start: 8, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.459182072283698234299289198341', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-007513', + sex: 'Not Reported', + age_at_index: 60, + index_event: 'COVID-19 Test', + race: 'Not Reported', + ethnicity: 'Hispanic or Latino', + zip: '941', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 60, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.474092284662271536629184790059', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 11, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 14, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 25, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 5, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.810741460028941099018374381217', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '232451-001091', + sex: 'Not Reported', + age_at_index: 13, + index_event: 'First COVID test', + race: 'Black or African American', + ethnicity: 'Not Hispanic or Latino', + zip: 'US', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 13, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR P PORT CHEST 1 VIEW', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.55472', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 1, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '514382-039181', + sex: 'Not Reported', + age_at_index: 21, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: 'US', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 21, + body_part_examined: ['PORT CHEST'], + days_to_study: 0, + loinc_code: '36554-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Single view', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST PICC VERIFICATION', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.6029171', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 2, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 2, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-010559', + sex: 'Not Reported', + age_at_index: 56, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 56, + body_part_examined: ['CHEST'], + days_to_study: -2, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.180563485118550787879260910806', + }, + { + age_at_imaging: 56, + body_part_examined: ['CHEST'], + days_to_study: 5, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.367230495062884622986946202074', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -83, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -88, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -6, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -10, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -50, + }, + ], + medications: [ + { + days_to_medication_start: 214, + days_to_medication_end: null, + dose_sequence_number: 5, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 123, + days_to_medication_end: null, + dose_sequence_number: 4, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -137, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 242, + days_to_medication_end: null, + dose_sequence_number: 6, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -116, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 269, + days_to_medication_end: null, + dose_sequence_number: 7, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 102, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.163843159339111198747559950040', + ], + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.268158452269688349584707601394', + ], + }, + ], + _imaging_studies_count: 2, + _ct_series_file_count: 0, + _dx_series_file_count: 3, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-006028', + sex: 'Not Reported', + age_at_index: 38, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '947', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 38, + body_part_examined: ['CHEST'], + days_to_study: 239, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.724627111204085272645675138832', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 562, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 528, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 239, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 195, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 450, + }, + ], + medications: [ + { + days_to_medication_start: 621, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 384, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 412, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.250985117089692904162560720014', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '232451-001655', + sex: 'Not Reported', + age_at_index: 63, + index_event: 'First COVID test', + race: 'Black or African American', + ethnicity: 'Not Hispanic or Latino', + zip: 'US', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 3, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63901', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63904', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63913', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 4, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63898', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 1, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63907', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 2, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63895', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63910', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 2, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 7, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 7, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-009373', + sex: 'Not Reported', + age_at_index: 37, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '941', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 37, + body_part_examined: ['CHEST'], + days_to_study: 212, + loinc_code: '42272-5', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest PA and Lateral', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 2 VIEWS PA AND LATERAL', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.507061461553432315196526618864', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -126, + }, + ], + medications: [ + { + days_to_medication_start: 58, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 82, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.846240764585028846016503187378', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-011913', + sex: 'Not Reported', + age_at_index: 71, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 71, + body_part_examined: ['CHEST'], + days_to_study: 9, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.278107455876066137171677606988', + }, + { + age_at_imaging: 71, + body_part_examined: ['CHEST'], + days_to_study: 3, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.328243271539180794824930002963', + }, + { + age_at_imaging: 71, + body_part_examined: null, + days_to_study: 12, + loinc_code: '29252-4', + loinc_contrast: 'WO', + loinc_long_common_name: 'CT Chest WO contrast', + loinc_method: 'CT', + loinc_system: 'Chest', + study_description: 'CT CHEST WITHOUT CONTRAST', + study_location: null, + study_modality: ['CT'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.105731777963798038123529567873', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -388, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 7, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -584, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -355, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -227, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -289, + }, + ], + medications: [ + { + days_to_medication_start: -250, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -271, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.519200256772382868331768076862', + ], + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 8, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 1, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.126190299596310978922819788660', + ], + }, + ], + _imaging_studies_count: 3, + _ct_series_file_count: 7, + _dx_series_file_count: 2, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-012375', + sex: 'Not Reported', + age_at_index: 60, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 60, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '42272-5', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest PA and Lateral', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 2 VIEWS PA AND LATERAL', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.330957237716414669587476460080', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'Rapid antigen test', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -97, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: -98, + }, + ], + medications: [ + { + days_to_medication_start: 26, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91303', + medication_code_system: 'CPT', + medication_manufacturer: 'Janssen', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -37, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91303', + medication_code_system: 'CPT', + medication_manufacturer: 'Janssen', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.180137370330907200957672927457', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-007947', + sex: 'Not Reported', + age_at_index: 89, + index_event: 'COVID-19 Test', + race: 'Asian', + ethnicity: 'Not Hispanic or Latino', + zip: '941', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 89, + body_part_examined: ['CHEST'], + days_to_study: 61, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.222552150766148875375871550478', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: 61, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 239, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 242, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -118, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -3, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -190, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: -118, + }, + ], + medications: [ + { + days_to_medication_start: 341, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-012433', + sex: 'Not Reported', + age_at_index: 70, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 70, + body_part_examined: ['CHEST'], + days_to_study: 8, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.319316423651295112004324887047', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -285, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 8, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -243, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -93, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -224, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -294, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -165, + }, + ], + medications: [ + { + days_to_medication_start: -289, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91303', + medication_code_system: 'CPT', + medication_manufacturer: 'Janssen', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 2, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.164033539388468242078934854691', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-010716', + sex: 'Not Reported', + age_at_index: 56, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 56, + body_part_examined: ['CHEST'], + days_to_study: 31, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.127832308794752991138261441788', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 28, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: [ + { + days_to_medication_start: 217, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 53, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 457, + days_to_medication_end: null, + dose_sequence_number: 4, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 32, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.340258858670915453041960131347', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + ], + _aggregation: { + case: { + _totalCount: 16, + }, + }, + }, + }; + + const format: keyof typeof FILE_DELIMITERS = 'csv'; + + const spyFlat = jest.spyOn(flat, 'flatten').mockReturnValueOnce(jsonInput2); + + const result = await jsonToFormat(jsonInput, format); + + expect(result).toBeInstanceOf(Object); + expect(result).toBe(jsonInput); + expect(spyFlat).toHaveBeenCalledWith(jsonInput); + }); +}); diff --git a/packages/core/src/features/guppy/types.ts b/packages/core/src/features/guppy/types.ts new file mode 100644 index 00000000..d855ee47 --- /dev/null +++ b/packages/core/src/features/guppy/types.ts @@ -0,0 +1,45 @@ +import { FilterSet } from '../filters'; +import { Accessibility } from '../../constants'; + + +// Guppy data request parameters +export interface BaseGuppyDataRequest { + type: string; + accessibility?: Accessibility; + fields: string[]; + sort?: string[]; +} + +// Represents a request to download data from Guppy and convert it to a specific format. +export interface GuppyDownloadDataParams extends BaseGuppyDataRequest { + filter: FilterSet; // cohort filters + format: 'json' | 'csv' | 'tsv'; // the three supported formats + rootPath?: string; // a string (minus $.) JSONPath to the root of the data +} + +export interface GuppyActionFunctionParams extends Record { + type: string; + accessibility?: Accessibility; + fields: string[]; + sort?: string[]; + filter: FilterSet; +} + +export interface GuppyActionParams> { + parameters: T; // query parameters for the Guppy request + onStart?: () => void; // function to call when the download starts + onDone?: (blob: Blob) => void; // function to call when the download is done + onError?: (error: Error) => void; // function to call when the download fails + onAbort?: () => void; // function to call when the download is aborted + signal?: AbortSignal; // AbortSignal to use for the request +} + +export interface GuppyDownloadActionFunctionParams extends GuppyDownloadDataParams { + filename: string; +} + +// Function type for Guppy actions +export type GuppyActionFunction> = (args:GuppyActionParams) => void; + + +export type DownloadFromGuppyParams = GuppyActionParams; diff --git a/packages/core/src/features/guppy/utils.ts b/packages/core/src/features/guppy/utils.ts new file mode 100644 index 00000000..28209e94 --- /dev/null +++ b/packages/core/src/features/guppy/utils.ts @@ -0,0 +1,120 @@ +import { DownloadFromGuppyParams, GuppyDownloadDataParams } from './types'; +import { GEN3_GUPPY_API } from '../../constants'; +import { selectCSRFToken } from '../gen3'; +import { coreStore } from '../../store'; +import { convertFilterSetToGqlFilter } from '../filters'; +import { jsonToFormat } from './conversion'; +import { isJSONObject } from '../../types'; +import { JSONPath } from 'jsonpath-plus'; + +/** + * Represents a configuration for making a fetch request. + * + * @typedef {Object} FetchConfig + * @property {string} method - The HTTP method to use for the request. + * @property {Object} headers - The headers to include in the request. + * @property {string} body - The request body. + */ +export type FetchConfig = { + method: string; + headers: Record; + body: string; +}; + +/** + * Prepares a URL for downloading by appending '/download' to the provided apiUrl. + * + * @param {string} apiUrl - The base URL to be used for preparing the download URL. + * @returns {URL} - The prepared download URL as a URL object. + */ +const prepareUrl = (apiUrl: string) => new URL(apiUrl + '/download'); + +/** + * Prepares a fetch configuration object for downloading files from Guppy. + * + * @param {GuppyFileDownloadRequestParams} parameters - The parameters to include in the request body. + * @param {string} csrfToken - The CSRF token to include in the request headers. + * @returns {FetchConfig} - The prepared fetch configuration object. + */ +const prepareFetchConfig = ( + parameters: GuppyDownloadDataParams, + csrfToken?: string, +): FetchConfig => { + return { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(csrfToken !== undefined && { 'X-CSRFToken': csrfToken }), + }, + body: JSON.stringify({ + type: parameters.type, + filter: convertFilterSetToGqlFilter(parameters.filter), + accessibility: parameters.accessibility, + fields: parameters?.fields, + sort: parameters?.sort, + }), + }; +}; + +/** + * Downloads a file from Guppy using the provided parameters. + * It will optionally convert the data to the specified format. + * + * @param {DownloadFromGuppyParams} parameters - The parameters to use for the download request. + * @param onStart - The function to call when the download starts. + * @param onDone - The function to call when the download is done. + * @param onError - The function to call when the download fails. + * @param onAbort - The function to call when the download is aborted. + * @param signal - AbortSignal to use for the request. + */ +export const downloadFromGuppy = async ({ + parameters, + onStart = () => null, + onDone = (_: Blob) => null, + onError = (_: Error) => null, + onAbort = () => null, + signal = undefined +}: DownloadFromGuppyParams) => { + const csrfToken = selectCSRFToken(coreStore.getState()); + onStart?.(); + + const url = prepareUrl(GEN3_GUPPY_API); + const fetchConfig = prepareFetchConfig(parameters, csrfToken); + + fetch(url.toString(), {...fetchConfig, ...(signal ? { signal: signal } : {})} as RequestInit) + .then(async (response: Response) => { + if (!response.ok) { + throw new Error(response.statusText); + } + + let jsonData = await response.json(); + if (parameters?.rootPath && parameters.rootPath) { // if rootPath is provided, extract the data from the rootPath + jsonData = JSONPath({ + json: jsonData, + path: `$.[${parameters.rootPath}]`, + resultType: 'value', + }); + } + // convert the data to the specified format and return a Blob + let str = ''; + if (parameters.format === 'json') { + str = JSON.stringify(jsonData); + } else { + const convertedData = await jsonToFormat(jsonData, parameters.format); + if (isJSONObject(convertedData)) { + str = JSON.stringify(convertedData); + } else { + str = convertedData; + } + } + const bytes = new TextEncoder().encode(str); + return new Blob([bytes]); + }) + .then((blob) => onDone?.(blob)) + .catch((error) => { // Abort is handle as an exception + if (error.name == 'AbortError') { // handle abort() + onAbort?.(); + } + onError?.(error); + }); +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d915a6b8..e9823eee 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -import { GEN3_API, GEN3_DOMAIN, GEN3_COMMONS_NAME, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API } from './constants'; +import { GEN3_API, GEN3_DOMAIN, GEN3_COMMONS_NAME, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API, Accessibility } from './constants'; import { type CoreState } from './reducers'; export * from './features/user'; @@ -19,4 +19,4 @@ export * from './features/cohort'; export * from './features/filters'; export * from './features/guppy'; -export { type CoreState, GEN3_COMMONS_NAME, GEN3_DOMAIN, GEN3_API, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API }; +export { type CoreState, GEN3_COMMONS_NAME, GEN3_DOMAIN, GEN3_API, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API, Accessibility }; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 1ccb53e9..dd920b1b 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -19,12 +19,16 @@ export interface HistogramData { count: number; } -export type HistogramDataArray = Array; - +// type guard functions export const isHistogramRangeData = (key: any): key is [number, number] => { return Array.isArray(key) && key.length === 2 && key.every((item) => typeof item === 'number'); }; +export const isJSONObject = (data: any): data is JSONObject => { + return typeof data === 'object' && data !== null && !Array.isArray(data); +}; + +export type HistogramDataArray = Array; export const isHistogramData = (data: any): data is HistogramData => { return typeof data === 'object' && data !== null && 'key' in data && 'count' in data; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index b7928ba2..716879f1 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true, "declaration": true, "emitDeclarationOnly": true, + "strictNullChecks": true, "module": "esnext", "moduleResolution": "bundler", "noUnusedLocals": true, @@ -24,6 +25,7 @@ "resolveJsonModule": true, "allowJs": true, "sourceMap": true, + "allowJs": true, "lib": [ "dom", "esnext" diff --git a/packages/frontend/.npmignore b/packages/frontend/.npmignore index 08831260..a6112db0 100644 --- a/packages/frontend/.npmignore +++ b/packages/frontend/.npmignore @@ -1,12 +1,3 @@ -# General -node_modules -*.tgz -.DS_Store -.vscode -npm-debug.log -yarn-error.log -.env -dump.rdb -*.retry -coverage -.next/cache/* +src/ +tsconfig.* +setupTests.js diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 3e9f3aaf..e08c04c9 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -21,9 +21,9 @@ "copy-tailwind": "mkdir -p dist && cp src/tailwind.cjs dist/tailwind.cjs", "copy-style": "mkdir -p dist/styles && cp src/styles/* dist/styles/", "build": "npm run build:rollup && npm run types", - "build:clean": "npm run clean && npm run build", + "build:clean": "npm run clean && npm run build && npm run types", "build:rollup": "npm run compile && npm run copy-tailwind && rollup --config rollup.config.mjs", - "build:compile": "npm run compile && npm run build:rollup", + "build:compile": "npm run compile && npm run types && npm run build:rollup", "build:swc": "npm run compile && npm run copy-tailwind && swc src -d dist", "build:watch": "npm run build:rollup -- --watch", "dev": "npm run build:watch" @@ -57,7 +57,7 @@ "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.7", - "@reduxjs/toolkit": "^1.9.3", + "@reduxjs/toolkit": "^1.9.5", "@types/dompurify": "^3.0.3", "chart.js": "^4.0.1", "cookies-next": "^2.1.1", @@ -71,7 +71,7 @@ "gray-matter": "^4.0.3", "jose": "^4.13.1", "js-cookie": "^3.0.5", - "jsonpath-plus": "^7.2.0", + "jsonpath-plus": "^8.0.0", "mantine-react-table": "^1.3.1", "minisearch": "^6.1.0", "next-compose-plugins": "^2.2.1", @@ -106,6 +106,7 @@ "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-terser": "^0.4.3", "@svgr/webpack": "^8.1.0", + "@testing-library/user-event": "^14.5.2", "@types/color": "^3.0.3", "@types/estree": "^1.0.0", "@types/file-saver": "^2.0.5", diff --git a/packages/frontend/rollup.config.mjs b/packages/frontend/rollup.config.mjs index 722900bc..df93232b 100644 --- a/packages/frontend/rollup.config.mjs +++ b/packages/frontend/rollup.config.mjs @@ -29,12 +29,21 @@ const globals = { 'jsonpath-plus': 'jsonpathPlus', '@mantine/notifications': 'mantineNotifications', '@mantine/styles': 'mantineStyles', + '@mantine/hooks': 'mantineHooks', + '@mantine/core': 'mantineCore', + '@mantine/form': 'mantineForm', 'react-icons/fi': 'reactIcons', 'react-icons/lu': 'reactIcons', 'react-icons/md': 'reactIcons', + 'react-icons/io': 'reactIcons', + 'react-icons/ri': 'reactIcons', 'react-minisearch': 'reactMinisearch', 'lodash/uniq': 'lodashUniq', 'lodash/sum': 'lodashSum', + 'react-cookie': 'reactCookie', + 'yaml': 'yaml', + 'file-saver': 'fileSaver', + 'universal-cookie': 'universalCookie', }; const config = [ @@ -105,7 +114,7 @@ const config = [ tsconfig: 'tsconfig.json', jsc: {}, }, - swcPreserveDirectives(), + swcPreserveDirectives() ), ], }, diff --git a/packages/frontend/src/components/Buttons/ActionButton.tsx b/packages/frontend/src/components/Buttons/ActionButton.tsx index f081e227..a94d2702 100644 --- a/packages/frontend/src/components/Buttons/ActionButton.tsx +++ b/packages/frontend/src/components/Buttons/ActionButton.tsx @@ -8,7 +8,6 @@ const ActionButton = ({ type, leftIcon, rightIcon, - fileName, tooltipText, }: DownloadButtonProps) => { return ( diff --git a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx b/packages/frontend/src/components/Buttons/DownloadButtons/DownloadButton.tsx similarity index 83% rename from packages/frontend/src/components/DownloadButtons/DownloadButton.tsx rename to packages/frontend/src/components/Buttons/DownloadButtons/DownloadButton.tsx index 38a08bd7..0782140a 100644 --- a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx +++ b/packages/frontend/src/components/Buttons/DownloadButtons/DownloadButton.tsx @@ -1,6 +1,6 @@ import { Button, ButtonProps, Loader, Tooltip } from '@mantine/core'; import { FiDownload } from 'react-icons/fi'; -import download from '../../utils/download'; +import download, { DownloadFunctionParams } from '../../../utils/download'; import { hideModal, Modals, useCoreDispatch } from '@gen3/core'; import { Dispatch, SetStateAction, forwardRef } from 'react'; @@ -11,13 +11,10 @@ import { Dispatch, SetStateAction, forwardRef } from 'react'; * @property disabled - Whether the button is disabled. * @property inactiveText - The text to display when the button is inactive. * @property activeText - The text to display when the button is active. - * @property filename - The name of the file to download. - * @property size - The size of the download. + * @property params - The parameters to pass to the endpoint. * @property format - The format of the download. * @property fields - The fields to download. - * @property caseFilters - The case filters to download. * @property filters - The filters to download. - * @property extraParams - Any extra parameters to download. * @property method - The method to use for the download. * @property customStyle - Any custom styles to apply to the button. * @property showLoading - Whether to show the loading icon. @@ -31,17 +28,12 @@ import { Dispatch, SetStateAction, forwardRef } from 'react'; * @property toolTip - The tooltip to display. */ interface DownloadButtonProps { - endpoint: string; + endpoint?: string; disabled?: boolean; inactiveText: string; activeText: string; - filename?: string; - size?: number; format?: string; - fields?: Array; - caseFilters?: Record; - filters?: Record; - extraParams?: Record; + params: Record; method?: string; customStyle?: string; showLoading?: boolean; @@ -83,23 +75,18 @@ interface DownloadButtonProps { * @param toolTip - The tooltip to display. * @category Buttons */ + export const DownloadButton = forwardRef< HTMLButtonElement, DownloadButtonProps & ButtonProps >( ( { - endpoint, + endpoint = '', disabled = false, - filename, - size = 10000, - format = 'JSON', - fields = [], - caseFilters = {}, - filters = {}, inactiveText, activeText, - extraParams, + params = {}, method = 'POST', customStyle, setActive, @@ -108,8 +95,8 @@ export const DownloadButton = forwardRef< showIcon = true, preventClickEvent = false, active, - Modal400, - Modal403, + Modal400 = Modals.GeneralErrorModal, + Modal403 = Modals.NoAccessModal, toolTip, ...buttonProps }: DownloadButtonProps, @@ -142,24 +129,13 @@ export const DownloadButton = forwardRef< return; } dispatch(hideModal()); - const params = { - size, - attachment: true, - format, - fields: fields.join(), - case_filters: caseFilters, - filters, - pretty: true, - ...(filename ? { filename } : {}), - ...extraParams, - }; setActive && setActive(true); download({ - params, endpoint, + params, method, - done: () => setActive && setActive(false), dispatch, + done: () => setActive && setActive(false), Modal400, Modal403, }); diff --git a/packages/frontend/src/components/DownloadButtons/index.ts b/packages/frontend/src/components/Buttons/DownloadButtons/index.ts similarity index 100% rename from packages/frontend/src/components/DownloadButtons/index.ts rename to packages/frontend/src/components/Buttons/DownloadButtons/index.ts diff --git a/packages/frontend/src/components/Buttons/DropdownButtons/DropdownButton.tsx b/packages/frontend/src/components/Buttons/DropdownButtons/DropdownButton.tsx index 0bf623ca..f38b44c6 100644 --- a/packages/frontend/src/components/Buttons/DropdownButtons/DropdownButton.tsx +++ b/packages/frontend/src/components/Buttons/DropdownButtons/DropdownButton.tsx @@ -1,16 +1,16 @@ import { Menu, Button, Text } from '@mantine/core'; -import { type DropdownButtonsProps } from './types'; +import { type DropdownButtonProps } from './types'; import { Icon } from '@iconify/react'; -const DropdownButton = ({title, buttons} : DropdownButtonsProps ) : JSX.Element => { +const DropdownButton = ({title, dropdownItems} : DropdownButtonProps ) : JSX.Element => { return ( - + console.log(value)}> { - buttons.map((button) => { + dropdownItems.map((button) => { return ( diff --git a/packages/frontend/src/components/Buttons/DropdownButtons/index.ts b/packages/frontend/src/components/Buttons/DropdownButtons/index.ts index fedfe3d2..1d648ac1 100644 --- a/packages/frontend/src/components/Buttons/DropdownButtons/index.ts +++ b/packages/frontend/src/components/Buttons/DropdownButtons/index.ts @@ -1,8 +1,8 @@ -import { type DownloadButtonProps, type DropdownButtonsProps } from './types'; +import { type DownloadButtonProps, type DropdownButtonProps } from './types'; import DropdownButton from './DropdownButton'; export { type DownloadButtonProps, - type DropdownButtonsProps, + type DropdownButtonProps, DropdownButton, }; diff --git a/packages/frontend/src/components/Buttons/DropdownButtons/types.ts b/packages/frontend/src/components/Buttons/DropdownButtons/types.ts index c1a96699..167e24f2 100644 --- a/packages/frontend/src/components/Buttons/DropdownButtons/types.ts +++ b/packages/frontend/src/components/Buttons/DropdownButtons/types.ts @@ -1,14 +1,15 @@ export interface DownloadButtonProps { enabled?:boolean; - type: string; + type?: string; title: string; + actionTitle?: string; // string to show when action is in progress leftIcon?: string; rightIcon?: string; - fileName: string; tooltipText?: string; + action?: string; + actionArgs?: Record; } -export interface DropdownButtonsProps { - readonly title: string; - buttons: ReadonlyArray>; +export interface DropdownButtonProps extends Omit { + dropdownItems: ReadonlyArray>; } diff --git a/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx b/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx new file mode 100644 index 00000000..4b6ab887 --- /dev/null +++ b/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx @@ -0,0 +1,157 @@ +import { Button, Menu } from '@mantine/core'; +import { FloatingPosition } from '@mantine/core/lib/Floating/types'; +import { ReactNode } from 'react'; +import { Tooltip } from '@mantine/core'; +import { IoMdArrowDropdown as Dropdown } from 'react-icons/io'; +import { focusStyles } from '../../utils'; + +interface DropdownWithIconProps { + /** + * if true, doesn't set width to be "target" + */ + disableTargetWidth?: string; + /** + * Left Icon for the taret button, can be undefined too + */ + LeftIcon?: JSX.Element; + /** + * Right Icon for the taret button, can be undefined too (default to dropdown icon) + */ + RightIcon?: JSX.Element; + /** + * Content for target button + */ + TargetButtonChildren: ReactNode; + /** + * disables the target button and menu + */ + targetButtonDisabled?: boolean; + /** + * array dropdown items. Need to pass title, onClick and icon event handler is optional + */ + dropdownElements: Array<{ + title: string; + onClick?: () => void; + icon?: JSX.Element; + disabled?: boolean; // if true, disables the menu item + }>; + /** + * only provide menuLabelText if we want label for dropdown elements + */ + menuLabelText?: string; + /** + * custom class / stylings for menuLabelText + */ + menuLabelCustomClass?: string; + /** + * custom position for Menu + */ + customPosition?: FloatingPosition; + /** + * whether the dropdown should fill the height of its parent + */ + fullHeight?: boolean; + /** + * custom z-index for Menu, defaults to undefined + */ + zIndex?: number; + /** + * custom test id + */ + customDataTestId?: string; + + /** + tooltip + */ + tooltip?: string; + + /** + * aria-label for the button + */ + buttonAriaLabel?: string; +} + +export const DropdownWithIcon = ({ + disableTargetWidth, + LeftIcon, + RightIcon = ( +