From 59f2cae1e00e012a284cc809d9849db71b3c2e9e Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 14 Feb 2024 19:50:04 +0000 Subject: [PATCH 01/14] CDE-40 feat: Connect collections page --- demo/integration.js | 19 +++++- lib/CdeContext.ts | 4 +- lib/CdeContextProvider.tsx | 16 +++-- .../steps/{StepOne.tsx => CollectionsTab.tsx} | 40 +++++++++--- lib/components/steps/MappingStep.tsx | 61 +++++++++++-------- .../steps/{StepThree.tsx => MappingTab.tsx} | 9 ++- lib/models.ts | 4 +- 7 files changed, 109 insertions(+), 44 deletions(-) rename lib/components/steps/{StepOne.tsx => CollectionsTab.tsx} (58%) rename lib/components/steps/{StepThree.tsx => MappingTab.tsx} (99%) diff --git a/demo/integration.js b/demo/integration.js index be127e6..9950474 100644 --- a/demo/integration.js +++ b/demo/integration.js @@ -1,5 +1,6 @@ import {init} from './cde-mapper.js'; + export function mapAndInit(datasetMappingFile, additionalDatasetMappingsFiles, datasetFile) { let datasetMappings = []; let additionalDatasetMappings = []; @@ -64,7 +65,7 @@ export function mapAndInit(datasetMappingFile, additionalDatasetMappingsFiles, d datasetMapping: datasetMappings, additionalDatasetMappings: additionalDatasetMappings, datasetSample: datasetSample, - collections: [], + collections: getCollections(), config: {width: '60%', height: '80%'}, name: 'TestLabName', callback: (cdeFileMapping) => console.log(cdeFileMapping), @@ -95,4 +96,20 @@ export function mapAndInit(datasetMappingFile, additionalDatasetMappingsFiles, d }; startProcessing(); +} + +function getCollections() { + return [ + { + id: 'global', + name: "Global", + fetch: fetch, + suggested: true, + } + ] +} + +function fetch(queryString) { + console.log(queryString) + return [] } \ No newline at end of file diff --git a/lib/CdeContext.ts b/lib/CdeContext.ts index 351dbbd..b449d2d 100644 --- a/lib/CdeContext.ts +++ b/lib/CdeContext.ts @@ -17,7 +17,7 @@ export const CdeContext = createContext<{ handleUpdateDatasetMappingRow: (key: string, newData: Entity) => void; getSuggestions: () => Suggestions; headerMapping: HeaderMapping; - collections: Collection[]; + collections: { [key: string]: Collection }; config: Config; @@ -46,7 +46,7 @@ export const CdeContext = createContext<{ titleIndex: TITLE_INDEX, interlexIdIndex: INTERLEX_ID_INDEX, }, - collections: [], + collections: {}, config: { width: "100%", height: "100%", diff --git a/lib/CdeContextProvider.tsx b/lib/CdeContextProvider.tsx index 97c9406..d209a4d 100644 --- a/lib/CdeContextProvider.tsx +++ b/lib/CdeContextProvider.tsx @@ -1,5 +1,5 @@ import {PropsWithChildren, useMemo, useState} from 'react'; -import {DatasetMapping, Entity, InitParams, STEPS} from "./models.ts"; +import {Collection, DatasetMapping, Entity, InitParams, STEPS} from "./models.ts"; import theme from "./theme/index.tsx"; import {ThemeProvider} from "@mui/material"; import CssBaseline from '@mui/material/CssBaseline'; @@ -24,7 +24,7 @@ export const CdeContextProvider = ({ datasetMapping: rawDatasetMapping, additionalDatasetMappings: rawAdditionalDatasetMappings = [], headerMapping: providedHeaderMapping = defaultHeaderMapping, - collections, + collections: rawCollections, config, name, callback, @@ -92,6 +92,12 @@ export const CdeContextProvider = ({ return suggestions }; + const collectionsDictionary = useMemo(() => { + return rawCollections.reduce((acc, collection) => { + acc[collection.id] = collection; + return acc; + }, {} as { [key: string]: Collection }); + }, [rawCollections]); const handleClose = () => { setErrorMessage(null); @@ -118,7 +124,7 @@ export const CdeContextProvider = ({ handleUpdateDatasetMappingRow, getSuggestions, headerMapping, - collections, + collections: collectionsDictionary, config, step, setStep, @@ -128,10 +134,12 @@ export const CdeContextProvider = ({ setErrorMessage, handleClose }; + + const hasErrors = areFilesValid || rawCollections.length == 0 return ( - {areFilesValid ? ( + {hasErrors ? ( {children} diff --git a/lib/components/steps/StepOne.tsx b/lib/components/steps/CollectionsTab.tsx similarity index 58% rename from lib/components/steps/StepOne.tsx rename to lib/components/steps/CollectionsTab.tsx index f78f978..8e9c29c 100644 --- a/lib/components/steps/StepOne.tsx +++ b/lib/components/steps/CollectionsTab.tsx @@ -1,13 +1,30 @@ import {useState} from 'react'; import {Stack, Typography, Box, Button, Link} from '@mui/material'; import StyledCard from '../common/StyledCard.tsx'; +import {useCdeContext} from "../../CdeContext.ts"; -function StepOne() { - const [selectedRadioValue, setSelectedRadioValue] = useState("Spinal Cord Injury (SCI)"); +interface CollectionsProps { + changeToNextTab: () => void; + setDefaultCollection: (collectionId: string) => void +} + +function CollectionsTab({changeToNextTab, setDefaultCollection}: CollectionsProps) { + const { + collections, + } = useCdeContext(); + + const collectionKeys = Object.keys(collections); + + const [selectedCollection, setSelectedCollection] = useState(collectionKeys.length > 0 ? collectionKeys[0] : ''); const handleRadioChange = (value: string) => { - setSelectedRadioValue(value); + setSelectedCollection(value); + }; + + const handleConfirm = () => { + setDefaultCollection(selectedCollection); + changeToNextTab(); }; return ( @@ -18,7 +35,6 @@ function StepOne() { justifyContent='center' alignItems='center' height={1} - // maxHeight='calc(100vh - (3.9375rem + 3.5625rem + 4.4375rem + 2rem + 2rem))' p='1.5rem' pt={6} pb={6} @@ -33,16 +49,22 @@ function StepOne() { - - + {collectionKeys.map(key => ( + handleRadioChange(key)} + /> + ))} @@ -57,4 +79,4 @@ function StepOne() { ); } -export default StepOne; +export default CollectionsTab; diff --git a/lib/components/steps/MappingStep.tsx b/lib/components/steps/MappingStep.tsx index a0edee2..9b78872 100644 --- a/lib/components/steps/MappingStep.tsx +++ b/lib/components/steps/MappingStep.tsx @@ -1,8 +1,8 @@ import {Box, Button, Tab, Tabs, Tooltip, Typography, Divider, BoxProps} from '@mui/material'; import React, {Fragment} from 'react'; -import StepOne from './StepOne.tsx'; +import CollectionsTab from './CollectionsTab.tsx'; import StepTwo from './StepTwo.tsx'; -import StepThree from './StepThree.tsx'; +import MappingTab from './MappingTab.tsx'; import ModalHeightWrapper from '../common/ModalHeightWrapper.tsx'; import {vars} from '../../theme/variables.ts'; @@ -44,20 +44,6 @@ enum TabsEnum { } -const renderTabComponent = (step: number, changeToNextTab: () => void) => { - switch (step) { - case TabsEnum.Collection: - return ; - case TabsEnum.Suggestions: - return ; - case TabsEnum.Mapping: - return ; - // Add cases for other steps - default: - return
Unknown step
; - } -}; - interface CustomTabPanelProps extends BoxProps { children?: React.ReactNode; index: number; @@ -82,14 +68,35 @@ const CustomTabPanel: React.FC = ({children, value, index, function MappingStep() { - const [value, setValue] = React.useState(0); + const [tabIndex, setTabIndex] = React.useState(0); + const [defaultCollection, setDefaultCollection] = React.useState(""); const handleChange = (_event: React.SyntheticEvent, newValue: number) => { - setValue(newValue); + setTabIndex(newValue); }; const changeToNextTab = () => { - setValue((prevValue) => (prevValue + 1) % tabsArr.length); + setTabIndex((prevValue) => (prevValue + 1) % tabsArr.length); + }; + + const renderTabComponent = () => { + switch (tabIndex) { + case TabsEnum.Collection: + return ( + + + + ); + case TabsEnum.Suggestions: + return ; + case TabsEnum.Mapping: + return ; + default: + return
Unknown step
; + } }; return ( @@ -98,7 +105,7 @@ function MappingStep() { borderBottom: `0.0625rem solid ${gray100}`, padding: '0 1.5rem', }} display='flex' justifyContent='space-between' alignItems='center'> - + {tabsArr?.map((tab, index) => ( - {value === TabsEnum.Suggestions ? ( - ) : value === TabsEnum.Mapping && (<> + ) : tabIndex === TabsEnum.Mapping && (<> {tabsArr?.map((_tab, index) => ( - {renderTabComponent(index, changeToNextTab)} + + {renderTabComponent()} + ))} ); diff --git a/lib/components/steps/StepThree.tsx b/lib/components/steps/MappingTab.tsx similarity index 99% rename from lib/components/steps/StepThree.tsx rename to lib/components/steps/MappingTab.tsx index 2e1923a..4ba2994 100644 --- a/lib/components/steps/StepThree.tsx +++ b/lib/components/steps/MappingTab.tsx @@ -67,7 +67,11 @@ const styles = { } } -const StepThree = () => { +interface MappingProps { + defaultCollection: string; +} + +const MappingTab = ({ defaultCollection }: MappingProps) => { const [age, setAge] = React.useState('0'); const handleChange = (event: SelectChangeEvent) => { @@ -187,6 +191,7 @@ const StepThree = () => { const searchCDE = () => mockCDE; + console.log(defaultCollection) return ( <> @@ -665,4 +670,4 @@ const StepThree = () => { ) } -export default StepThree; \ No newline at end of file +export default MappingTab; \ No newline at end of file diff --git a/lib/models.ts b/lib/models.ts index 46946c1..bdb1458 100644 --- a/lib/models.ts +++ b/lib/models.ts @@ -21,8 +21,10 @@ export interface HeaderMapping { } export interface Collection { + id: string; name: string; - fetch: (query: string) => Promise; + fetch: (queryString: string) => Promise; + suggested: boolean; } export interface Config { From 3f0fd7618c216d34afde26f1a825b26a03a861df Mon Sep 17 00:00:00 2001 From: afonso Date: Thu, 15 Feb 2024 17:34:40 +0000 Subject: [PATCH 02/14] CDE-40 feat: Update collection suggested logic --- lib/CdeContextProvider.tsx | 7 +++++-- lib/models.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/CdeContextProvider.tsx b/lib/CdeContextProvider.tsx index d209a4d..8db6dbc 100644 --- a/lib/CdeContextProvider.tsx +++ b/lib/CdeContextProvider.tsx @@ -93,8 +93,11 @@ export const CdeContextProvider = ({ }; const collectionsDictionary = useMemo(() => { - return rawCollections.reduce((acc, collection) => { - acc[collection.id] = collection; + return rawCollections.reduce((acc, collection, index) => { + acc[collection.id] = { + ...collection, + suggested: index === 0 // Set 'suggested' to true for the first collection, false for others + }; return acc; }, {} as { [key: string]: Collection }); }, [rawCollections]); diff --git a/lib/models.ts b/lib/models.ts index bdb1458..d227d80 100644 --- a/lib/models.ts +++ b/lib/models.ts @@ -24,7 +24,7 @@ export interface Collection { id: string; name: string; fetch: (queryString: string) => Promise; - suggested: boolean; + suggested: boolean | null } export interface Config { From 5e2aabc3d513a9fbc0c71d2d6890fbd67bb1ed19 Mon Sep 17 00:00:00 2001 From: afonso Date: Fri, 16 Feb 2024 01:01:23 +0000 Subject: [PATCH 03/14] CDE-40 feat: Use dataset header to create dataset mapping --- lib/CdeContextProvider.tsx | 78 +++++++++++++++++---------- lib/services/initialMappingService.ts | 26 ++++++--- lib/services/validatorsService.ts | 12 +++-- lib/settings.ts | 6 ++- 4 files changed, 83 insertions(+), 39 deletions(-) diff --git a/lib/CdeContextProvider.tsx b/lib/CdeContextProvider.tsx index 8db6dbc..f85b1dd 100644 --- a/lib/CdeContextProvider.tsx +++ b/lib/CdeContextProvider.tsx @@ -3,7 +3,7 @@ import {Collection, DatasetMapping, Entity, InitParams, STEPS} from "./models.ts import theme from "./theme/index.tsx"; import {ThemeProvider} from "@mui/material"; import CssBaseline from '@mui/material/CssBaseline'; -import {validateDatasetMapping,} from "./services/validatorsService.ts"; +import {validateDataset, validateDatasetMapping,} from "./services/validatorsService.ts"; import {mapStringTableToDatasetMapping} from "./services/initialMappingService.ts"; import {updateDatasetMappingRow} from "./services/updateMappingService.ts"; import ErrorPage from "./components/ErrorPage.tsx"; @@ -35,34 +35,58 @@ export const CdeContextProvider = ({ const [loadingMessage, setLoadingMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); - const headerMapping = useMemo(() => ({ - ...defaultHeaderMapping, - ...providedHeaderMapping - }), [providedHeaderMapping]); + const headerMapping = useMemo(() => { + // Check if rawDatasetMapping is provided and has content + if (!rawDatasetMapping || rawDatasetMapping.length === 0) { + return defaultHeaderMapping; + } + // Merge providedHeaderMapping with defaultHeaderMapping if rawDatasetMapping is valid + return { + ...defaultHeaderMapping, + ...providedHeaderMapping + }; + }, [providedHeaderMapping, rawDatasetMapping]); + + + + // validate dataset sample + + const isDatasetInvalid = useMemo(() => { + let tmpIsDatasetInvalid = false; + + try { + validateDataset(datasetSample); + } catch (error) { + const message = error instanceof Error ? error.message : 'An unknown error occurred'; + const errorMessage = `Invalid dataset: ${message}`; + console.error(errorMessage); + tmpIsDatasetInvalid = true; + } + return tmpIsDatasetInvalid; + }, [datasetSample]); // Process and validate datasetMapping - const [initialDatasetMapping, initialDatasetMappingHeader, areFilesValid] = useMemo(() => { - let localDatasetMapping = {}; - let localDatasetMappingHeader: string[] = []; - let localAreFilesValid = true; - - if (rawDatasetMapping && rawDatasetMapping.length > 0) { - try { - validateDatasetMapping(rawDatasetMapping, headerMapping.variableNameIndex); - } catch (error) { - const message = error instanceof Error ? error.message : 'An unknown error occurred'; - const errorMessage = `Invalid dataset mapping: ${message}`; - console.error(errorMessage); - localAreFilesValid = false; - } - const datasetMappingData = mapStringTableToDatasetMapping(rawDatasetMapping, headerMapping); - localDatasetMapping = datasetMappingData[0]; - localDatasetMappingHeader = datasetMappingData[1] + const [initialDatasetMapping, initialDatasetMappingHeader, isDatasetMappingInvalid] = useMemo(() => { + let tmpDatasetMapping = {}; + let tmpDatasetHeader: string[] = []; + + try { + validateDatasetMapping(rawDatasetMapping, headerMapping.variableNameIndex); + } catch (error) { + const message = error instanceof Error ? error.message : 'An unknown error occurred'; + const errorMessage = `Invalid dataset mapping: ${message}`; + console.error(errorMessage); + return [tmpDatasetMapping, tmpDatasetHeader, true] } + const datasetHeader = datasetSample[0] + const datasetMappingData = mapStringTableToDatasetMapping(rawDatasetMapping, headerMapping, datasetHeader); + + tmpDatasetMapping = datasetMappingData[0]; + tmpDatasetHeader = datasetMappingData[1] - return [localDatasetMapping, localDatasetMappingHeader, localAreFilesValid]; - }, [rawDatasetMapping, headerMapping]); + return [tmpDatasetMapping, tmpDatasetHeader, false]; + }, [rawDatasetMapping, headerMapping, datasetSample]); const [datasetMapping, setDatasetMapping] = useState(initialDatasetMapping); @@ -138,15 +162,15 @@ export const CdeContextProvider = ({ handleClose }; - const hasErrors = areFilesValid || rawCollections.length == 0 + const hasErrors = isDatasetInvalid || isDatasetMappingInvalid || rawCollections.length == 0 return ( - {hasErrors ? ( + {hasErrors ? : ( {children} - ) : } + )} diff --git a/lib/services/initialMappingService.ts b/lib/services/initialMappingService.ts index 84a95c3..f0debbd 100644 --- a/lib/services/initialMappingService.ts +++ b/lib/services/initialMappingService.ts @@ -1,18 +1,28 @@ import {DatasetMapping, HeaderMapping} from "../models.ts"; +import {DEFAULT_HEADERS} from "../settings.ts"; // Mapper for datasetMapping -export const mapStringTableToDatasetMapping = (rawMapping: string[][], headerMapping: HeaderMapping): [DatasetMapping, string[]] => { - if (rawMapping.length < 2) return [{}, []]; +export const mapStringTableToDatasetMapping = (rawMapping: string[][] | undefined, headerMapping: HeaderMapping, includeHeaders?: string[]): [DatasetMapping, string[]] => { + const datasetMapping: DatasetMapping = {}; + let headers: string[]; + // Determine headers + if (rawMapping && rawMapping.length > 0) { + [headers, ...rawMapping] = rawMapping; + } else { + headers = Array.from({ length: Math.max(...Object.values(headerMapping)) + 1 }, (_, i) => DEFAULT_HEADERS[i] || ''); + } - const [headers, ...rows] = rawMapping; - const datasetMapping: DatasetMapping = {}; + // Initialize datasetMapping keys + const mappingKeys = includeHeaders || (rawMapping ? rawMapping.map(row => row[headerMapping.variableNameIndex]) : []); + mappingKeys.forEach(key => datasetMapping[key] = new Array(headers.length).fill('')); - rows.forEach((row) => { + // Populate datasetMapping with rawMapping data + rawMapping?.forEach(row => { const variableNameValue = row[headerMapping.variableNameIndex]; - - // Key the mapping by the variableName value - datasetMapping[variableNameValue] = row; + if (variableNameValue in datasetMapping) { + datasetMapping[variableNameValue] = row; + } }); return [datasetMapping, headers]; diff --git a/lib/services/validatorsService.ts b/lib/services/validatorsService.ts index 328311d..80ec72a 100644 --- a/lib/services/validatorsService.ts +++ b/lib/services/validatorsService.ts @@ -1,6 +1,12 @@ -export const validateDatasetMapping = (datasetMapping: string[][], variableNameIndex: number): void => { - if (datasetMapping.length === 0) { - throw new Error("Dataset mapping is empty."); +export const validateDataset = (dataset: string[][]) => { + if (dataset.length < 2) { + throw new Error("Dataset must have at least two rows (one for headers and one for data)."); + } +}; + +export const validateDatasetMapping = (datasetMapping: string[][] | undefined, variableNameIndex: number): void => { + if (datasetMapping == undefined || datasetMapping.length === 0) { + return } const headers = datasetMapping[0]; if (headers.length < 3) { diff --git a/lib/settings.ts b/lib/settings.ts index b99a117..cc75e70 100644 --- a/lib/settings.ts +++ b/lib/settings.ts @@ -5,4 +5,8 @@ export const INTERLEX_ID_INDEX = 11 export const MAX_SUGGESTIONS = 3 -export const CDE_BASE_URL = "http://uri.interlex.org/base/" \ No newline at end of file +export const CDE_BASE_URL = "http://uri.interlex.org/base/" + +// TODO: Use variables so that we can reuse them with the CDE Search mapping +export const DEFAULT_HEADERS = ["Variable Name (UI)", "Abbreviation", "Title", "Unit of Measure", "Description", "DataType", + "Multiple Values", "Permitted Values", "Minimum Value", "Maximum Value", "Comments", "InterLex ID", "CDE Level"]; From e37ce4449a30f5468c89609d4feff6f5c8029621 Mon Sep 17 00:00:00 2001 From: afonso Date: Fri, 16 Feb 2024 01:02:04 +0000 Subject: [PATCH 04/14] CDE-40 refactor: Move ModalHeightWrapper to CollectionsTab --- lib/components/steps/CollectionsTab.tsx | 8 +++++--- lib/components/steps/MappingStep.tsx | 13 ++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/components/steps/CollectionsTab.tsx b/lib/components/steps/CollectionsTab.tsx index 8e9c29c..2cf37fd 100644 --- a/lib/components/steps/CollectionsTab.tsx +++ b/lib/components/steps/CollectionsTab.tsx @@ -2,6 +2,7 @@ import {useState} from 'react'; import {Stack, Typography, Box, Button, Link} from '@mui/material'; import StyledCard from '../common/StyledCard.tsx'; import {useCdeContext} from "../../CdeContext.ts"; +import ModalHeightWrapper from "../common/ModalHeightWrapper.tsx"; interface CollectionsProps { @@ -28,7 +29,7 @@ function CollectionsTab({changeToNextTab, setDefaultCollection}: CollectionsProp }; return ( - <> + handleRadioChange(key)} /> @@ -75,7 +76,8 @@ function CollectionsTab({changeToNextTab, setDefaultCollection}: CollectionsProp
- + + ); } diff --git a/lib/components/steps/MappingStep.tsx b/lib/components/steps/MappingStep.tsx index d48aa49..71cabbc 100644 --- a/lib/components/steps/MappingStep.tsx +++ b/lib/components/steps/MappingStep.tsx @@ -3,7 +3,6 @@ import React, {Fragment} from 'react'; import CollectionsTab from './CollectionsTab.tsx'; import SuggestionsStep from './Suggestions/SuggestionsStep.tsx'; import MappingTab from './MappingTab.tsx'; -import ModalHeightWrapper from '../common/ModalHeightWrapper.tsx'; import {vars} from '../../theme/variables.ts'; const { @@ -82,14 +81,10 @@ function MappingStep() { const renderTabComponent = () => { switch (tabIndex) { case TabsEnum.Collection: - return ( - - - - ); + return case TabsEnum.Suggestions: return ; case TabsEnum.Mapping: From 64132bff36afd4804fe7b71306ec56d02830b841 Mon Sep 17 00:00:00 2001 From: afonso Date: Fri, 16 Feb 2024 01:03:15 +0000 Subject: [PATCH 05/14] CDE-40 refactor: Refactor out search components --- .../steps/Mapping/MappingSearch.tsx | 52 + .../Mapping/SearchCollectionSelector.tsx | 74 + lib/components/steps/MappingTab.tsx | 1282 +++++++++-------- 3 files changed, 771 insertions(+), 637 deletions(-) create mode 100644 lib/components/steps/Mapping/MappingSearch.tsx create mode 100644 lib/components/steps/Mapping/SearchCollectionSelector.tsx diff --git a/lib/components/steps/Mapping/MappingSearch.tsx b/lib/components/steps/Mapping/MappingSearch.tsx new file mode 100644 index 0000000..4b616eb --- /dev/null +++ b/lib/components/steps/Mapping/MappingSearch.tsx @@ -0,0 +1,52 @@ +import React, {useState} from "react"; +import {Box, Button, InputAdornment, TextField} from "@mui/material"; +import {FilterIcon, SearchIcon} from "../../../icons"; +import Filters from "../../common/Filters.tsx"; + + +interface MappingSearchProps { + onChange: () => void; +} + +export default function MappingSearch({onChange}: MappingSearchProps) { + + const [searchString, setSearchString] = useState(''); + + const [anchorEl, setAnchorEl] = useState(null); + + + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchString(event.target.value); + onChange() + }; + + const handleFiltersClose = () => { + setAnchorEl(null); + onChange() + }; + const open = Boolean(anchorEl); + const id = open ? 'filter-popover' : undefined; + + + return + + }} + value={searchString} + onChange={handleSearchChange} + /> + + + + ; +} \ No newline at end of file diff --git a/lib/components/steps/Mapping/SearchCollectionSelector.tsx b/lib/components/steps/Mapping/SearchCollectionSelector.tsx new file mode 100644 index 0000000..90d5d0d --- /dev/null +++ b/lib/components/steps/Mapping/SearchCollectionSelector.tsx @@ -0,0 +1,74 @@ +import {useState} from 'react'; +import {Box, ListSubheader, Typography} from '@mui/material'; +import {CheckIcon, DownIcon} from "../../../icons"; + + +interface SelectableCollection { + id: string; + name: string; + selected: boolean; +} + +interface SearchCollectionSelectorProps { + collections: SelectableCollection[]; + onCollectionSelect: (collection: SelectableCollection) => void; +} + +export default function SearchCollectionSelector(props: SearchCollectionSelectorProps) { + const [toggleMenu, setToggleMenu] = useState(false); + + return + setToggleMenu(!toggleMenu)} + style={{ + position: "relative", + display: "flex", + alignItems: "center", + cursor: "pointer", + justifyContent: "space-between", + }} + > + + {props.collections.find(collection => collection.selected)?.name || 'Select a Collection'} + + + {toggleMenu && ( + +
    + {props.collections.map(collection => ( +
  • { + props.onCollectionSelect(collection); + setToggleMenu(false); + }}> + + {collection.name} + + {collection.selected && } +
  • + ))} +
+
+ )} +
+
; +} diff --git a/lib/components/steps/MappingTab.tsx b/lib/components/steps/MappingTab.tsx index 4ba2994..ad22b1b 100644 --- a/lib/components/steps/MappingTab.tsx +++ b/lib/components/steps/MappingTab.tsx @@ -1,673 +1,681 @@ -import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Chip, FormControl, IconButton, InputAdornment, MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography } from "@mui/material" +import {useState} from "react"; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Chip, + FormControl, + IconButton, + MenuItem, + Select, + TextField, + Tooltip, + Typography +} from "@mui/material" import ModalHeightWrapper from "../common/ModalHeightWrapper" -import { ArrowIcon, BulletIcon, CheckIcon, CrossIcon, FilterIcon, GlobeIcon, InfoIcon, PairIcon, SearchIcon, SortIcon } from "../../icons"; -import React from "react"; -import CustomEntitiesDropdown from "../common/CustomMappingDropdown"; +import { + ArrowIcon, + BulletIcon, + CheckIcon, + CrossIcon, + GlobeIcon, + InfoIcon, + PairIcon, + SortIcon +} from "../../icons"; +import CustomEntitiesDropdown from "../common/CustomMappingDropdown.tsx"; import CdeDetails from "../common/CdeDetails"; -import Filters from "../common/Filters"; import PreviewBox from "../common/PreviewBox"; +import MappingSearch from "./Mapping/MappingSearch.tsx"; +import {useCdeContext} from "../../CdeContext.ts"; const styles = { - root: { - boxSizing: 'border-box', - }, - head: { - display: 'flex', - boxSizing: 'border-box', - columnGap: '1.5rem', - padding: '0.75rem 0', - borderBottom: '0.0625rem solid #ECEDEE', - }, - wrap: {}, - row: { - display: 'flex', - boxSizing: 'border-box', - columnGap: '1.5rem', - flexWrap: 'wrap', - padding: '1.5rem 0', - borderBottom: '0.0625rem solid #ECEDEE', - }, - col: { - display: 'flex', - alignItems: 'center', - flexShrink: 0, - gap: '0.75rem', - boxSizing: 'border-box', - - '&:first-of-type': { - width: '11.25rem', - boxSizing: 'border-box', + root: { + boxSizing: 'border-box', }, - - '&:nth-child(2)': { - width: '18.75rem', - boxSizing: 'border-box', - }, - - '&:nth-child(3)': { - width: '1rem', - boxSizing: 'border-box', + head: { + display: 'flex', + boxSizing: 'border-box', + columnGap: '1.5rem', + padding: '0.75rem 0', + borderBottom: '0.0625rem solid #ECEDEE', }, - - '&:nth-child(4)': { - width: 'calc(100% - (11.25rem + 18.75rem + 1rem + 1.5rem + 1.5rem + 1.5rem))', - boxSizing: 'border-box', - }, - - '& p': { - fontSize: '0.75rem', - fontWeight: 500, - lineHeight: '150%', - color: '#676C74', + wrap: {}, + row: { + display: 'flex', + boxSizing: 'border-box', + columnGap: '1.5rem', + flexWrap: 'wrap', + padding: '1.5rem 0', + borderBottom: '0.0625rem solid #ECEDEE', }, + col: { + display: 'flex', + alignItems: 'center', + flexShrink: 0, + gap: '0.75rem', + boxSizing: 'border-box', + + '&:first-of-type': { + width: '11.25rem', + boxSizing: 'border-box', + }, - '& svg': { - cursor: 'pointer', - } - } -} - -interface MappingProps { - defaultCollection: string; -} - -const MappingTab = ({ defaultCollection }: MappingProps) => { - const [age, setAge] = React.useState('0'); - - const handleChange = (event: SelectChangeEvent) => { - setAge(event.target.value as string); - }; + '&:nth-child(2)': { + width: '18.75rem', + boxSizing: 'border-box', + }, - const [anchorEl, setAnchorEl] = React.useState(null); + '&:nth-child(3)': { + width: '1rem', + boxSizing: 'border-box', + }, - const filterToggle = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; + '&:nth-child(4)': { + width: 'calc(100% - (11.25rem + 18.75rem + 1rem + 1.5rem + 1.5rem + 1.5rem))', + boxSizing: 'border-box', + }, - const handleClose = () => { - setAnchorEl(null); - }; + '& p': { + fontSize: '0.75rem', + fontWeight: 500, + lineHeight: '150%', + color: '#676C74', + }, - const open = Boolean(anchorEl); - const id = open ? 'filter-popover' : undefined; + '& svg': { + cursor: 'pointer', + } + } +} - const mockCDE = [ +const mockCDE = [ { - "id": "5304", - "group": 'Origins', - "label": "GUID", - "content": [ - { - "title": "Name", - "value": "GUID" - }, - { - "title": "Variable Name", - "value": "Subject" - }, - { - "title": "Title", - "value": "Unique identification of each mouse ID" - } - ] + "id": "5304", + "group": 'Origins', + "label": "GUID", + "content": [ + { + "title": "Name", + "value": "GUID" + }, + { + "title": "Variable Name", + "value": "Subject" + }, + { + "title": "Title", + "value": "Unique identification of each mouse ID" + } + ] }, { - "id": "32845", - "group": 'Origins', - "label": "SmallSpeciesStrainTyp", - "content": [ - { - "title": "Name", - "value": "SmallSpeciesStrainTyp" - }, - { - "title": "Variable Name", - "value": "Subject" - }, - { - "title": "Title", - "value": "Unique identification of each mouse ID" - } - ] + "id": "32845", + "group": 'Origins', + "label": "SmallSpeciesStrainTyp", + "content": [ + { + "title": "Name", + "value": "SmallSpeciesStrainTyp" + }, + { + "title": "Variable Name", + "value": "Subject" + }, + { + "title": "Title", + "value": "Unique identification of each mouse ID" + } + ] }, { - "id": "47428", - "group": 'Origins', - "label": "StudySpeciesTyp", - "content": [ - { - "title": "Name", - "value": "StudySpeciesTyp" - }, - { - "title": "Variable Name", - "value": "Subject" - }, - { - "title": "Title", - "value": "Unique identification of each mouse ID" - } - ] + "id": "47428", + "group": 'Origins', + "label": "StudySpeciesTyp", + "content": [ + { + "title": "Name", + "value": "StudySpeciesTyp" + }, + { + "title": "Variable Name", + "value": "Subject" + }, + { + "title": "Title", + "value": "Unique identification of each mouse ID" + } + ] }, { - "id": "12822", - "group": 'Origins', - "label": "Weight", - "content": [ - { - "title": "Name", - "value": "Weight" - }, - { - "title": "Variable Name", - "value": "Subject" - }, - { - "title": "Title", - "value": "Unique identification of each mouse ID" - } - ] + "id": "12822", + "group": 'Origins', + "label": "Weight", + "content": [ + { + "title": "Name", + "value": "Weight" + }, + { + "title": "Variable Name", + "value": "Subject" + }, + { + "title": "Title", + "value": "Unique identification of each mouse ID" + } + ] }, { - "id": "1798", - "group": 'Origins', - "label": "AgeVal", - "content": [ - { - "title": "Name", - "value": "AgeVal" - }, - { - "title": "Variable Name", - "value": "Subject" - }, - { - "title": "Title", - "value": "Unique identification of each mouse ID" - } - ] + "id": "1798", + "group": 'Origins', + "label": "AgeVal", + "content": [ + { + "title": "Name", + "value": "AgeVal" + }, + { + "title": "Variable Name", + "value": "Subject" + }, + { + "title": "Title", + "value": "Unique identification of each mouse ID" + } + ] }, - ]; - - const searchCDE = () => mockCDE; - - console.log(defaultCollection) - return ( - <> - - - - }} - /> - - - - - - - - - - - - - Column headers from dataset - - - - - CDEs/ Data Dictionary fields - - - - - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[1], - }}/> - - - - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[2] ?? "", - }}/> - - - - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[3] ?? "", - }}/> - - - - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[0] ?? "", - }}/> - +]; + +interface MappingProps { + defaultCollection: string; +} + +const MappingTab = ({defaultCollection}: MappingProps) => { + + const [rows, setRows] = useState([]); + const { datasetMapping } = useCdeContext(); - - - - - Pairing suggestions - - This is a Tooltip - - Tooltips are used to describe or identify an element. In most scenarious, tooltips help the user understand meaning, function or alt-text. - - - } - > - - - - - - - div': { - display: 'flex', - alignItems: 'center', - }, - - '&:before': { - content: '""', - position: 'absolute', - left: '-1.375rem', - top: '50%', - transform: 'translateY(-50%)', - width: '0.75rem', - height: '0.125rem', - background: '#ECEDEE', - borderTopRightRadius: '3.125rem', - borderBottomRightRadius: '3.125rem', - } - }} mb={1.5}> - - - - + + const handleFiltering = () => { + // TODO: to implement + console.log("To be implemented" + rows) + setRows([]) + } + + const searchCDE = () => mockCDE; + + console.log(datasetMapping) + console.log(defaultCollection) + return ( + <> + + + + + + + + - - - - - - Subject_name - - - Name of each subject in the dataset - - - - - - - - - - - + + Column headers from dataset + + + + + CDEs/ Data Dictionary fields + - - - - - - - div': { - display: 'flex', - alignItems: 'center', - }, - - '&:before': { - content: '""', - position: 'absolute', - left: '-1.375rem', - top: '50%', - transform: 'translateY(-50%)', - width: '0.75rem', - height: '0.125rem', - background: '#ECEDEE', - borderTopRightRadius: '3.125rem', - borderBottomRightRadius: '3.125rem', - } - }} mb={1.5}> - - - - + + + + } + /> + + + + + + + + + searchCDE(), + value: mockCDE[1], + }}/> + + + + + + } + /> + + + + + + + + + searchCDE(), + value: mockCDE[2] ?? "", + }}/> + + + + + + } + /> + + + + + + + + + searchCDE(), + value: mockCDE[3] ?? "", + }}/> + - - - - - - Subject_name - - - Name of each subject in the dataset - - - - - - - - - - - + + + + } + /> + + + + + + + + + searchCDE(), + value: mockCDE[0] ?? "", + }}/> + + + + + + + Pairing suggestions + + This is a Tooltip + + Tooltips are used to describe or identify an element. In + most scenarious, tooltips help the user understand meaning, + function or alt-text. + + + } + > + + + + + + + div': { + display: 'flex', + alignItems: 'center', + }, + + '&:before': { + content: '""', + position: 'absolute', + left: '-1.375rem', + top: '50%', + transform: 'translateY(-50%)', + width: '0.75rem', + height: '0.125rem', + background: '#ECEDEE', + borderTopRightRadius: '3.125rem', + borderBottomRightRadius: '3.125rem', + } + }} mb={1.5}> + + + + + + + + + + + Subject_name + + + Name of each subject in the dataset + + + + + + + + + + + + + + + + + + + + div': { + display: 'flex', + alignItems: 'center', + }, + + '&:before': { + content: '""', + position: 'absolute', + left: '-1.375rem', + top: '50%', + transform: 'translateY(-50%)', + width: '0.75rem', + height: '0.125rem', + background: '#ECEDEE', + borderTopRightRadius: '3.125rem', + borderBottomRightRadius: '3.125rem', + } + }} mb={1.5}> + + + + + + + + + + + Subject_name + + + Name of each subject in the dataset + + + + + + + + + + + + + + + + + + + + + + + + } + /> + + + + + + + + + + - - + + + + } + /> + + + + + + + + + + + + + + + } + /> + + + + + + + + + + + + - - - - - - - - - } - /> - - - - - - + - - - - - - - - } - /> - - - - - - - - - - - - - - - } - /> - - - - - - - - - - - - - - - - + - - - ) + + + ) } export default MappingTab; \ No newline at end of file From adb4a6af69a13cd35d1c0efeff721d6da8aad3e8 Mon Sep 17 00:00:00 2001 From: afonso Date: Fri, 16 Feb 2024 17:39:12 +0000 Subject: [PATCH 06/14] CDE-40 refactor: Replace placeholder data with context on mapping tab --- .../common/CustomMappingDropdown.tsx | 199 +------ .../steps/Mapping/PairingSuggestion.tsx | 113 ++++ .../steps/Mapping/PairingTooltip.tsx | 30 + lib/components/steps/MappingTab.tsx | 539 +++--------------- lib/helpers/functions.ts | 13 +- lib/models.ts | 5 +- lib/services/suggestionsService.ts | 2 +- 7 files changed, 257 insertions(+), 644 deletions(-) create mode 100644 lib/components/steps/Mapping/PairingSuggestion.tsx create mode 100644 lib/components/steps/Mapping/PairingTooltip.tsx diff --git a/lib/components/common/CustomMappingDropdown.tsx b/lib/components/common/CustomMappingDropdown.tsx index 9496b3d..56a1069 100644 --- a/lib/components/common/CustomMappingDropdown.tsx +++ b/lib/components/common/CustomMappingDropdown.tsx @@ -1,10 +1,11 @@ import React, {useState} from 'react'; import {FormControl, InputAdornment, MenuItem, Popper, Select, SelectChangeEvent, Stack, Tooltip} from "@mui/material"; import {TextField, Box, Typography, Button, ListSubheader, Chip} from '@mui/material'; -import {AddIcon, CheckIcon, ChevronDown, DownIcon, GlobeIcon, MagicWandIcon, MagnifyGlassIcon} from "../../icons"; -import HoveredOptionContent from "./HoveredOptionContent"; -import NoResultField from './NoResultField'; -import {vars} from '../../theme/variables'; +import {AddIcon, CheckIcon, ChevronDown, GlobeIcon, MagnifyGlassIcon} from "../../icons"; +import HoveredOptionContent from "./HoveredOptionContent.tsx"; +import NoResultField from './NoResultField.tsx'; +import {vars} from '../../theme/variables.ts'; +import SearchCollectionSelector from "../steps/Mapping/SearchCollectionSelector.tsx"; const { buttonOutlinedBorderColor, @@ -177,6 +178,7 @@ interface CustomEntitiesDropdownProps { }; } + export default function CustomEntitiesDropdown({ placeholder, options: { @@ -190,7 +192,6 @@ export default function CustomEntitiesDropdown({ }: CustomEntitiesDropdownProps) { const [searchValue] = useState(""); const [anchorEl, setAnchorEl] = React.useState(null); - const [toggleMenu, setToggleMenu] = useState(false); const [age, setAge] = React.useState('0'); const handleChange = (event: SelectChangeEvent) => { @@ -622,172 +623,7 @@ export default function CustomEntitiesDropdown({ } } }} key={group}> - - setToggleMenu(!toggleMenu)} - style={{ - position: 'relative', - display: "flex", - alignItems: "center", - cursor: 'pointer', - justifyContent: "space-between", - }} - > - - Spinal Cord Injury (SCI) - - - {toggleMenu && ( - e.stopPropagation()} sx={{ - borderRadius: '0.5rem', - overflow: 'hidden', - position: 'absolute', - width: 'calc(100% - 1.875rem)', - top: '2.125rem', - left: '0.9375rem', - border: '0.0625rem solid #E4E5E7', - background: '#FFF', - boxShadow: '0rem 0.25rem 0.375rem -0.125rem rgba(7, 8, 8, 0.03), 0rem 0.75rem 1rem -0.25rem rgba(7, 8, 8, 0.08)', - - '& .simple-list': { - gap: 0, - '& li': { - borderRadius: 0, - paddingLeft: '1rem', - paddingRight: '1rem', - borderBottom: '0.0625rem solid #ECEDEE', - '&:hover': { - borderRadius: 0, - }, - '& .MuiTypography-root': { - fontWeight: '500 !important', - }, - } - } - }}> -
    -
  • - Spinal Cord Injury (SCI) -
  • -
  • - Trauma Brain Injury (TBI) -
  • -
- - - Common Data Element (CDE) - - - -
    -
  • - Spinal Cord Injury (SCI) - -
  • -
  • - Trauma Brain Injury (TBI) -
  • -
-
-
- )} -
-
- - - - - Suggestions - -
    -
  • - GUID - SCI - -
  • -
  • - SmallSpeciesStrainTyp - SCI -
  • -
-
- + {toggleCustomView && @@ -838,27 +674,6 @@ export default function CustomEntitiesDropdown({ - - - Spinal Cord Injury (SCI) - - -
    {groupedOptions[group] .filter((option: Option) => diff --git a/lib/components/steps/Mapping/PairingSuggestion.tsx b/lib/components/steps/Mapping/PairingSuggestion.tsx new file mode 100644 index 0000000..1c18589 --- /dev/null +++ b/lib/components/steps/Mapping/PairingSuggestion.tsx @@ -0,0 +1,113 @@ +import {Box, FormControl, IconButton, MenuItem, Select, Typography} from "@mui/material"; +import {ArrowIcon, CheckIcon, CrossIcon, GlobeIcon} from "../../../icons"; +import CdeDetails from "../../common/CdeDetails.tsx"; + +export function PairingSuggestion(props: { + value: string, + onChange: () => void, + selectOptions: { value: string, label: string }[], + subjectName: string, + subjectDescription: string +}) { + return ( + + div": { + display: "flex", + alignItems: "center", + }, + "&:before": { + content: "\"\"", + position: "absolute", + left: "-1.375rem", + top: "50%", + transform: "translateY(-50%)", + width: "0.75rem", + height: "0.125rem", + background: "#ECEDEE", + borderTopRightRadius: "3.125rem", + borderBottomRightRadius: "3.125rem", + } + }} mb={1.5}> + + + + + + + + + + + {props.subjectName} + + + {props.subjectDescription} + + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/lib/components/steps/Mapping/PairingTooltip.tsx b/lib/components/steps/Mapping/PairingTooltip.tsx new file mode 100644 index 0000000..367ccda --- /dev/null +++ b/lib/components/steps/Mapping/PairingTooltip.tsx @@ -0,0 +1,30 @@ +import {Box, Tooltip, Typography} from "@mui/material"; +import {InfoIcon} from "../../../icons"; +import {DatasetMapping, HeaderMapping} from "../../../models.ts"; + +export function PairingTooltip(props: { datasetMapping: DatasetMapping, key: string, headerMapping: HeaderMapping }) { + return + We have seen this before + + {props.datasetMapping[props.key][props.headerMapping.preciseAbbreviationIndex]} is typically used + with the following fellow CDEs + + + } + > + + ; +} diff --git a/lib/components/steps/MappingTab.tsx b/lib/components/steps/MappingTab.tsx index ad22b1b..6bd0e84 100644 --- a/lib/components/steps/MappingTab.tsx +++ b/lib/components/steps/MappingTab.tsx @@ -5,30 +5,23 @@ import { AccordionSummary, Box, Chip, - FormControl, - IconButton, - MenuItem, - Select, TextField, - Tooltip, Typography } from "@mui/material" import ModalHeightWrapper from "../common/ModalHeightWrapper" import { ArrowIcon, BulletIcon, - CheckIcon, - CrossIcon, - GlobeIcon, - InfoIcon, PairIcon, SortIcon } from "../../icons"; import CustomEntitiesDropdown from "../common/CustomMappingDropdown.tsx"; -import CdeDetails from "../common/CdeDetails"; import PreviewBox from "../common/PreviewBox"; import MappingSearch from "./Mapping/MappingSearch.tsx"; import {useCdeContext} from "../../CdeContext.ts"; +import {getTypeFromRow, isRowMapped} from "../../helpers/functions.ts"; +import {PairingTooltip} from "./Mapping/PairingTooltip.tsx"; +import {PairingSuggestion} from "./Mapping/PairingSuggestion.tsx"; const styles = { root: { @@ -192,18 +185,49 @@ interface MappingProps { defaultCollection: string; } + + const MappingTab = ({defaultCollection}: MappingProps) => { const [rows, setRows] = useState([]); - const { datasetMapping } = useCdeContext(); + const {datasetMapping, headerMapping} = useCdeContext(); const handleFiltering = () => { // TODO: to implement - console.log("To be implemented" + rows) + console.log("To be implemented " + rows) setRows([]) } + const getChipComponent = (key: string) => { + const isMapped = isRowMapped(datasetMapping[key], headerMapping); + const label = getTypeFromRow(datasetMapping[key], headerMapping) + const color = isMapped ? 'success' : 'default'; + const iconColor = isMapped ? '#12B76A' : '#676C74'; + + return ( + } + /> + ); + }; + + const getPairingSuggestions = (key: string) => { + // TODO: to implement + console.log("To be implemented " + key) + return [] + }; + + const hasPairingSuggestions = (key: string) => { + // TODO: to implement + console.log("To be implemented " + key) + return false + }; + + const searchCDE = () => mockCDE; console.log(datasetMapping) @@ -230,444 +254,63 @@ const MappingTab = ({defaultCollection}: MappingProps) => { - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[1], - }}/> - - - - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[2] ?? "", - }}/> - - - - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[3] ?? "", - }}/> - - - - - - } - /> - - - - - - - - - searchCDE(), - value: mockCDE[0] ?? "", - }}/> - - - - - - - Pairing suggestions - - This is a Tooltip - - Tooltips are used to describe or identify an element. In - most scenarious, tooltips help the user understand meaning, - function or alt-text. - - - } - > - - - - - - - div': { - display: 'flex', - alignItems: 'center', - }, - - '&:before': { - content: '""', - position: 'absolute', - left: '-1.375rem', - top: '50%', - transform: 'translateY(-50%)', - width: '0.75rem', - height: '0.125rem', - background: '#ECEDEE', - borderTopRightRadius: '3.125rem', - borderBottomRightRadius: '3.125rem', - } - }} mb={1.5}> - - - - - - - - - - - Subject_name - - - Name of each subject in the dataset - - - - - - - - - - - - + {Object.keys(datasetMapping).map((key, index) => ( + + + {getChipComponent(key)} + + + + + + + + + searchCDE(), + value: mockCDE[2] ?? "", + }}/> + + + {hasPairingSuggestions(key) && ( + + + + + Pairing suggestions + + + + + {getPairingSuggestions(key).map(() => ( + {}} + selectOptions={[]} + subjectName="To be implemented" + subjectDescription="To be implemented" + /> + ))} - - - - - - - div': { - display: 'flex', - alignItems: 'center', - }, - - '&:before': { - content: '""', - position: 'absolute', - left: '-1.375rem', - top: '50%', - transform: 'translateY(-50%)', - width: '0.75rem', - height: '0.125rem', - background: '#ECEDEE', - borderTopRightRadius: '3.125rem', - borderBottomRightRadius: '3.125rem', - } - }} mb={1.5}> - - - - - - - - - - - Subject_name - - - Name of each subject in the dataset - - - - - - - - - - - - - - - - - - - - - - - - } - /> - - - - - - - - - + + + + )} - - - - - } - /> - - - - - - - - - - - - - - - } - /> - - - - - - - - - - - - + ))} diff --git a/lib/helpers/functions.ts b/lib/helpers/functions.ts index 6d4ac6d..297d2d9 100644 --- a/lib/helpers/functions.ts +++ b/lib/helpers/functions.ts @@ -6,6 +6,17 @@ export const getVariableName = (row: string[], headerMapping: HeaderMapping) => export const getPreciseAbbreviation = (row: string[], headerMapping: HeaderMapping) => row[headerMapping.preciseAbbreviationIndex]; export const isRowMapped = (row: string[], headerMapping: HeaderMapping) => row[headerMapping.preciseAbbreviationIndex] != '' +export const getTypeFromRow = (row: string[], headerMapping: HeaderMapping): EntityType => { + const isMapped = isRowMapped(row, headerMapping) + const hasInterLexId = row[headerMapping.interlexIdIndex] !== ''; + + if (isMapped) { + return hasInterLexId ? EntityType.MappedCDE : EntityType.MappedCustomDataDictionary; + } else { + return EntityType.Unmapped; + } +}; + export const getEntityFromRow = ( row: string[], datasetMappingHeader: string[], @@ -16,7 +27,7 @@ export const getEntityFromRow = ( preciseAbbrev: row[headerMapping.preciseAbbreviationIndex], title: row[headerMapping.titleIndex], interlexId: row[headerMapping.interlexIdIndex], - type: row[headerMapping.interlexIdIndex] ? EntityType.CDE : EntityType.CustomDataDictionary, + type: row[headerMapping.interlexIdIndex] ? EntityType.MappedCDE : EntityType.MappedCustomDataDictionary, }; datasetMappingHeader.forEach((header, index) => { diff --git a/lib/models.ts b/lib/models.ts index d227d80..0e161a6 100644 --- a/lib/models.ts +++ b/lib/models.ts @@ -45,8 +45,9 @@ export interface Entity { } export enum EntityType { - CDE = 'CDE', - CustomDataDictionary = 'CustomDataDictionary' + MappedCDE = 'MappedCDE', + MappedCustomDataDictionary = 'MappedCustomDataDictionary', + Unmapped = 'Unmapped' } export interface Suggestions { diff --git a/lib/services/suggestionsService.ts b/lib/services/suggestionsService.ts index 8acf8e7..c03c0a4 100644 --- a/lib/services/suggestionsService.ts +++ b/lib/services/suggestionsService.ts @@ -60,7 +60,7 @@ export const computeSuggestions = ( } return b.count - a.count; // Sort by count, higher first } - return a.type === EntityType.CDE ? -1 : 1; // Prioritize 'cde' type + return a.type === EntityType.MappedCDE ? -1 : 1; // Prioritize 'cde' type }); suggestions[variableName] = entities.map(item => item.entity); }); From ae71ce925906ae3185cca5d1d6358d04bfd5bc4b Mon Sep 17 00:00:00 2001 From: afonso Date: Fri, 16 Feb 2024 18:12:51 +0000 Subject: [PATCH 07/14] CDE-40 feat: Connect collection options --- .../common/CustomMappingDropdown.tsx | 10 +++- .../Mapping/SearchCollectionSelector.tsx | 6 +- lib/components/steps/MappingTab.tsx | 58 ++++++++++++++----- lib/models.ts | 5 ++ 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/lib/components/common/CustomMappingDropdown.tsx b/lib/components/common/CustomMappingDropdown.tsx index 56a1069..23a4535 100644 --- a/lib/components/common/CustomMappingDropdown.tsx +++ b/lib/components/common/CustomMappingDropdown.tsx @@ -6,6 +6,7 @@ import HoveredOptionContent from "./HoveredOptionContent.tsx"; import NoResultField from './NoResultField.tsx'; import {vars} from '../../theme/variables.ts'; import SearchCollectionSelector from "../steps/Mapping/SearchCollectionSelector.tsx"; +import {SelectableCollection} from "../../models.ts"; const { buttonOutlinedBorderColor, @@ -175,6 +176,8 @@ interface CustomEntitiesDropdownProps { onSearch: (searchValue: string) => Option[]; value: Option; header?: Header; + collections: SelectableCollection[]; + onCollectionSelect: (collection: SelectableCollection) => void; }; } @@ -187,7 +190,9 @@ export default function CustomEntitiesDropdown({ noResultReason, onSearch, value, - header + header, + collections, + onCollectionSelect, }, }: CustomEntitiesDropdownProps) { const [searchValue] = useState(""); @@ -623,7 +628,8 @@ export default function CustomEntitiesDropdown({ } } }} key={group}> - + {toggleCustomView && diff --git a/lib/components/steps/Mapping/SearchCollectionSelector.tsx b/lib/components/steps/Mapping/SearchCollectionSelector.tsx index 90d5d0d..9a048ec 100644 --- a/lib/components/steps/Mapping/SearchCollectionSelector.tsx +++ b/lib/components/steps/Mapping/SearchCollectionSelector.tsx @@ -1,13 +1,9 @@ import {useState} from 'react'; import {Box, ListSubheader, Typography} from '@mui/material'; import {CheckIcon, DownIcon} from "../../../icons"; +import {SelectableCollection} from "../../../models.ts"; -interface SelectableCollection { - id: string; - name: string; - selected: boolean; -} interface SearchCollectionSelectorProps { collections: SelectableCollection[]; diff --git a/lib/components/steps/MappingTab.tsx b/lib/components/steps/MappingTab.tsx index 6bd0e84..4100841 100644 --- a/lib/components/steps/MappingTab.tsx +++ b/lib/components/steps/MappingTab.tsx @@ -1,4 +1,4 @@ -import {useState} from "react"; +import {useEffect, useState} from "react"; import { Accordion, AccordionDetails, @@ -22,6 +22,7 @@ import {useCdeContext} from "../../CdeContext.ts"; import {getTypeFromRow, isRowMapped} from "../../helpers/functions.ts"; import {PairingTooltip} from "./Mapping/PairingTooltip.tsx"; import {PairingSuggestion} from "./Mapping/PairingSuggestion.tsx"; +import {SelectableCollection} from "../../models.ts"; const styles = { root: { @@ -186,17 +187,42 @@ interface MappingProps { } - const MappingTab = ({defaultCollection}: MappingProps) => { - const [rows, setRows] = useState([]); - const {datasetMapping, headerMapping} = useCdeContext(); + const {datasetMapping, headerMapping, collections} = useCdeContext(); + const [visibleRows, setVisibleRows] = useState([]); + const [selectedCollections, setSelectedCollections] = useState([]); + + useEffect(() => { + // Initialize the selected collections state + const initialSelectedCollections = Object.keys(collections).map(key => ({ + id: key, + name: collections[key].name, + selected: key === defaultCollection + })); + + setSelectedCollections(initialSelectedCollections); + }, [collections, defaultCollection]); + + + const handleCollectionSelect = (selectedCollection: SelectableCollection) => { + setSelectedCollections(prevCollections => + prevCollections.map(collection => { + if (collection.id === selectedCollection.id) { + // Toggle the 'selected' state + return { ...collection, selected: !collection.selected }; + } else { + return collection; + } + }) + ); + }; const handleFiltering = () => { // TODO: to implement - console.log("To be implemented " + rows) - setRows([]) + console.log("To be implemented " + visibleRows) + setVisibleRows([]) } const getChipComponent = (key: string) => { @@ -270,13 +296,16 @@ const MappingTab = ({defaultCollection}: MappingProps) => { - searchCDE(), - value: mockCDE[2] ?? "", - }}/> + searchCDE(), + collections: selectedCollections, + onCollectionSelect: handleCollectionSelect, + value: mockCDE[2] ?? "", + }}/> {hasPairingSuggestions(key) && ( @@ -298,7 +327,8 @@ const MappingTab = ({defaultCollection}: MappingProps) => { {getPairingSuggestions(key).map(() => ( {}} + onChange={() => { + }} selectOptions={[]} subjectName="To be implemented" subjectDescription="To be implemented" diff --git a/lib/models.ts b/lib/models.ts index 0e161a6..bcf21e2 100644 --- a/lib/models.ts +++ b/lib/models.ts @@ -63,3 +63,8 @@ export enum STEPS { COLLECTION, } +export interface SelectableCollection { + id: string; + name: string; + selected: boolean; +} \ No newline at end of file From d1d0794429b05b3c6b0c6a7424b5f6529c9ef3c7 Mon Sep 17 00:00:00 2001 From: afonso Date: Fri, 16 Feb 2024 18:18:17 +0000 Subject: [PATCH 08/14] CDE-40 feat: Update search placeholder text --- lib/components/steps/MappingTab.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/components/steps/MappingTab.tsx b/lib/components/steps/MappingTab.tsx index 4100841..f72e0fd 100644 --- a/lib/components/steps/MappingTab.tsx +++ b/lib/components/steps/MappingTab.tsx @@ -210,7 +210,7 @@ const MappingTab = ({defaultCollection}: MappingProps) => { prevCollections.map(collection => { if (collection.id === selectedCollection.id) { // Toggle the 'selected' state - return { ...collection, selected: !collection.selected }; + return {...collection, selected: !collection.selected}; } else { return collection; } @@ -256,8 +256,7 @@ const MappingTab = ({defaultCollection}: MappingProps) => { const searchCDE = () => mockCDE; - console.log(datasetMapping) - console.log(defaultCollection) + const searchText = "Search in " + (selectedCollections.length === 1 ? `${selectedCollections[0].name} collection` : 'multiple collections'); return ( <> @@ -299,7 +298,7 @@ const MappingTab = ({defaultCollection}: MappingProps) => { searchCDE(), collections: selectedCollections, From 0ba27cb3ac11e7f299d035c9cce6ad9f810c4073 Mon Sep 17 00:00:00 2001 From: afonso Date: Mon, 19 Feb 2024 17:50:06 +0000 Subject: [PATCH 09/14] CDE-40 chore: Add vite reverse proxy --- vite.config.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index add1482..33fc577 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,7 @@ import dts from 'vite-plugin-dts' // https://vitejs.dev/config/ -export default defineConfig(({ mode }) => ({ +export default defineConfig(({mode}) => ({ define: { 'process.env': {} }, @@ -23,4 +23,15 @@ export default defineConfig(({ mode }) => ({ sourcemap: mode == 'dev', emptyOutDir: true, copyPublicDir: false, - }})); + }, + server: { + proxy: { + '/api': { + target: 'https://scicrunch.org', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '/api/1/elastic/Interlex_pr/_search?key=I0FJ8VwuqLSVxfW586ICMpgzdT1m7miU'), + }, + }, + }, + +})); From 823e7ea7a8c170db73e316d90c61672318666b4e Mon Sep 17 00:00:00 2001 From: afonso Date: Mon, 19 Feb 2024 17:51:48 +0000 Subject: [PATCH 10/14] CDE-40 feat: Connect interlex search example --- demo/integration.js | 51 +++++- demo/query.js | 78 ++++++++ .../common/CustomMappingDropdown.tsx | 26 ++- lib/components/steps/MappingTab.tsx | 170 ++++++------------ lib/helpers/functions.ts | 42 ++++- lib/models.ts | 5 +- lib/services/suggestionsService.ts | 2 +- 7 files changed, 239 insertions(+), 135 deletions(-) create mode 100644 demo/query.js diff --git a/demo/integration.js b/demo/integration.js index caf4ea6..ce6179e 100644 --- a/demo/integration.js +++ b/demo/integration.js @@ -1,5 +1,6 @@ import {init} from './cde-mapper.js'; +import {getQueryObject} from "./query.js"; export function mapAndInit(datasetMappingFile, additionalDatasetMappingsFiles, datasetFile) { let datasetMappings = []; @@ -103,12 +104,52 @@ function getCollections() { { id: 'global', name: "Global", - fetch: fetch, + fetch: fetchElasticSearchData, } ] } -function fetch(queryString) { - console.log(queryString) - return [] -} \ No newline at end of file +async function fetchElasticSearchData(queryString) { + const query = getQueryObject(queryString) + const response = await fetch('/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...query + }), + }); + + const data = await response.json(); + return mapHitsToEntities(data.hits.hits || []) +} + + +function mapHitsToEntities(hits) { + return hits.map(hit => { + const source = hit._source; + const preciseAbbrev = (source.synonyms.find(s => s.type === 'abbrev') || {}).literal || ''; + const unitOfMeasure = (source.annotations.find(a => a.annotation_term_label === 'has unit') || {}).value; + const dataType = (source.annotations.find(a => a.annotation_term_label === 'allowedType') || {}).value; + const permittedValues = (source.annotations.find(a => a.annotation_term_label === 'allowedValues') || {}).value; + const minValue = (source.annotations.find(a => a.annotation_term_label === 'minValue') || {}).value; + const maxValue = (source.annotations.find(a => a.annotation_term_label === 'maxValue') || {}).value; + const cdeLevel = (source.annotations.find(a => a.annotation_term_label === 'hasCDELevel') || {}).value; + + return { + variableName: '', + preciseAbbrev, + title: source.label, + interlexId: source.ilx, + type: "CDE", + description: source.definition, + unitOfMeasure, + dataType, + permittedValues, + minValue, + maxValue, + cdeLevel, + }; + }); +} diff --git a/demo/query.js b/demo/query.js new file mode 100644 index 0000000..fdbd949 --- /dev/null +++ b/demo/query.js @@ -0,0 +1,78 @@ +export const getQueryObject = (queryString) => { + if (queryString) { + return { + "size": 20, + "from": 0, + "query": { + "bool": { + "must": [ + { + "query_string": { + "fields": [ + "label" + ], + "query": `${queryString}~`, + "type": "cross_fields", + "default_operator": "and", + "lenient": "true", + "fuzziness": 2, + "fuzzy_max_expansions": 50, + "fuzzy_prefix_length": 0, + "fuzzy_transpositions": "true" + } + } + ], + "should": [ + { + "match": { + "label": { + "query": `"${queryString}"`, + "boost": 20 + } + } + }, + { + "term": { + "label.aggregate": { + "term": `${queryString}`, + "boost": 2000 + } + } + } + ], + "filter": [ + { + "terms": { + "type.aggregate": [ + "cde" + ] + } + } + ] + } + }, + "aggregations": {} + } + } + + return { + "size": 20, + "from": 0, + "query": { + "bool": { + "filter": [ + { + "terms": { + "type.aggregate": [ + "cde" + ] + } + } + ] + } + } + } + +}; + + diff --git a/lib/components/common/CustomMappingDropdown.tsx b/lib/components/common/CustomMappingDropdown.tsx index 23a4535..6cbb6e0 100644 --- a/lib/components/common/CustomMappingDropdown.tsx +++ b/lib/components/common/CustomMappingDropdown.tsx @@ -173,8 +173,8 @@ interface CustomEntitiesDropdownProps { errors?: string; searchPlaceholder?: string; noResultReason?: string; - onSearch: (searchValue: string) => Option[]; - value: Option; + onSearch: (searchValue: string) => Promise; + value: Option | null; header?: Header; collections: SelectableCollection[]; onCollectionSelect: (collection: SelectableCollection) => void; @@ -211,17 +211,27 @@ export default function CustomEntitiesDropdown({ const id = open ? 'simple-popper' : undefined; const [hoveredOption, setHoveredOption] = useState