Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CDE-64 Implement records sorting in the mapping step/view #33

Merged
merged 13 commits into from
Mar 27, 2024
52 changes: 47 additions & 5 deletions lib/components/steps/mapping/MappingTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
AccordionSummary,
Box,
TextField,
Typography
Typography,
IconButton
} from "@mui/material"
import ModalHeightWrapper from "../../common/ModalHeightWrapper.tsx"
import {
Expand All @@ -23,6 +24,7 @@ import {Option, SelectableCollection, FiltersState} from "../../../models.ts";
import {getId, getType, isRowMapped} from "../../../helpers/rowHelpers.ts";
import {useServicesContext} from "../../../contexts/services/ServicesContext.ts";
import {mapRowToOption} from "../../../helpers/mappers.ts";
import {VariableNameFilter, CdeSortingFilter, StatusFilter, SortingStrategy} from "../../../sortingStrategies.ts";
import {usePairingSuggestions} from "../../../hooks/usePairingSuggestions.ts";
import {
getAbbreviationFromOption,
Expand Down Expand Up @@ -93,6 +95,13 @@ const styles = {
'& svg': {
cursor: 'pointer',
}
},
sortButton: {
padding: '0.25rem',
borderRadius: '0.25rem',
'&:hover': {
backgroundColor: '#ECEDEE'
}
}
}

Expand All @@ -115,6 +124,7 @@ const MappingTab = ({defaultCollection}: MappingProps) => {

const [visibleRows, setVisibleRows] = useState<string[]>([]);
const [selectableCollections, setSelectableCollections] = useState<SelectableCollection[]>([]);
const [currentFilterStrategy, setCurrentFilterStrategy] = useState<SortingStrategy>();
const [selectedOptionsMap, setSelectedOptionsMap] = useState<{ [id: string]: Option }>({});
const [createdCustomDictionaryFields, setCreatedCustomDictionaryFields] = useState<{ [id: string]: Option }>({});

Expand Down Expand Up @@ -158,7 +168,6 @@ const MappingTab = ({defaultCollection}: MappingProps) => {
);
};


const searchInCollections = useCallback(
async (queryString: string): Promise<Option[]> => {
const selectedCollections = selectableCollections
Expand Down Expand Up @@ -253,6 +262,33 @@ const MappingTab = ({defaultCollection}: MappingProps) => {
await handleSelection(variableName, option, newIsSelectedState)
}

const isSameStrategyType = (filter: SortingStrategy, newFilter: SortingStrategy) => {
if (!filter || !newFilter) {
return false;
}
return filter.constructor === newFilter.constructor;
}

const sortRows = useCallback((fitlerStrategy: SortingStrategy) => {
const result = fitlerStrategy.doSort(visibleRows, datasetMapping, headerIndexes);
setVisibleRows([...result]);
}, [datasetMapping, headerIndexes, visibleRows])

const handleSortingStrategy = (newCurrentSortingStrategy: SortingStrategy) => {
if (currentFilterStrategy && isSameStrategyType(currentFilterStrategy, newCurrentSortingStrategy)) {
currentFilterStrategy.toggleSortOrder();
sortRows(currentFilterStrategy);
} else {
setCurrentFilterStrategy(newCurrentSortingStrategy)
}
}

useEffect(() => {
if(currentFilterStrategy){
sortRows(currentFilterStrategy);
}
}, [currentFilterStrategy, sortRows])

const searchText = "Search in " + (selectableCollections.length === 1 ? `${selectableCollections[0].name} collection` : 'multiple collections');

return (
Expand All @@ -264,16 +300,22 @@ const MappingTab = ({defaultCollection}: MappingProps) => {
<Box sx={styles.root}>
<Box sx={styles.head}>
<Box sx={styles.col}>
<SortIcon/>
<IconButton sx={styles.sortButton} onClick={() => handleSortingStrategy(new StatusFilter())}>
<SortIcon direction={currentFilterStrategy instanceof StatusFilter ? currentFilterStrategy.sortState : 0}/>
</IconButton>
</Box>
<Box sx={styles.col}>
<Typography>Column headers from dataset</Typography>
<SortIcon/>
<IconButton onClick={() => handleSortingStrategy(new VariableNameFilter())} sx={styles.sortButton}>
<SortIcon direction={currentFilterStrategy instanceof VariableNameFilter ? currentFilterStrategy.sortState : 0}/>
</IconButton>
</Box>
<Box sx={styles.col}/>
<Box sx={styles.col}>
<Typography>CDEs/ Data Dictionary fields</Typography>
<SortIcon/>
<IconButton sx={styles.sortButton} onClick={() => handleSortingStrategy(new CdeSortingFilter)}>
<SortIcon direction={currentFilterStrategy instanceof CdeSortingFilter ? currentFilterStrategy.sortState : 0}/>
</IconButton>
</Box>
</Box>
<Box sx={styles.wrap}>
Expand Down
14 changes: 10 additions & 4 deletions lib/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,16 @@ export const MagnifyGlassIcon = () => (
</svg>
)

export const SortIcon: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.35354 10.6462C7.40003 10.6926 7.43691 10.7477 7.46207 10.8084C7.48723 10.8691 7.50018 10.9342 7.50018 10.9999C7.50018 11.0656 7.48723 11.1307 7.46207 11.1914C7.43691 11.2521 7.40003 11.3072 7.35354 11.3537L5.35354 13.3537C5.3071 13.4001 5.25196 13.437 5.19126 13.4622C5.13056 13.4873 5.0655 13.5003 4.99979 13.5003C4.93408 13.5003 4.86902 13.4873 4.80832 13.4622C4.74762 13.437 4.69248 13.4001 4.64604 13.3537L2.64604 11.3537C2.59958 11.3072 2.56273 11.252 2.53759 11.1914C2.51245 11.1307 2.49951 11.0656 2.49951 10.9999C2.49951 10.9342 2.51245 10.8692 2.53759 10.8085C2.56273 10.7478 2.59958 10.6926 2.64604 10.6462C2.73986 10.5523 2.86711 10.4996 2.99979 10.4996C3.06549 10.4996 3.13054 10.5126 3.19124 10.5377C3.25193 10.5628 3.30708 10.5997 3.35354 10.6462L4.49979 11.793V2.99991C4.49979 2.8673 4.55247 2.74012 4.64624 2.64635C4.74 2.55258 4.86718 2.49991 4.99979 2.49991C5.1324 2.49991 5.25957 2.55258 5.35334 2.64635C5.44711 2.74012 5.49979 2.8673 5.49979 2.99991V11.793L6.64604 10.6462C6.69248 10.5997 6.74762 10.5628 6.80832 10.5376C6.86902 10.5125 6.93408 10.4995 6.99979 10.4995C7.0655 10.4995 7.13056 10.5125 7.19126 10.5376C7.25196 10.5628 7.3071 10.5997 7.35354 10.6462ZM13.3535 4.64615L11.3535 2.64616C11.3071 2.59967 11.252 2.56279 11.1913 2.53763C11.1306 2.51246 11.0655 2.49951 10.9998 2.49951C10.9341 2.49951 10.869 2.51246 10.8083 2.53763C10.7476 2.56279 10.6925 2.59967 10.646 2.64616L8.64604 4.64615C8.55222 4.73998 8.49951 4.86722 8.49951 4.99991C8.49951 5.13259 8.55222 5.25984 8.64604 5.35366C8.73986 5.44748 8.86711 5.50018 8.99979 5.50018C9.13247 5.50018 9.25972 5.44748 9.35354 5.35366L10.4998 4.20678V12.9999C10.4998 13.1325 10.5525 13.2597 10.6462 13.3535C10.74 13.4472 10.8672 13.4999 10.9998 13.4999C11.1324 13.4999 11.2596 13.4472 11.3533 13.3535C11.4471 13.2597 11.4998 13.1325 11.4998 12.9999V4.20678L12.646 5.35366C12.7399 5.44748 12.8671 5.50018 12.9998 5.50018C13.1325 5.50018 13.2597 5.44748 13.3535 5.35366C13.4474 5.25984 13.5001 5.13259 13.5001 4.99991C13.5001 4.86722 13.4474 4.73998 13.3535 4.64615Z" fill="#676C74"/>
</svg>
export const SortIcon: React.FC<SVGProps<SVGSVGElement>> = ({direction, ...props}) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<mask id="mask0_2962_36017" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_2962_36017)">
<path d="M10.0001 14.6668L6.66675 11.3335L7.61675 10.4002L9.33342 12.1168V7.3335H10.6667V12.1168L12.3834 10.4002L13.3334 11.3335L10.0001 14.6668Z" fill={direction === 2 ? "#373A3E" : "#A9ACB2"}/>
<path d="M5.33317 8.66683V3.8835L3.6165 5.60016L2.6665 4.66683L5.99984 1.3335L9.33317 4.66683L8.38317 5.60016L6.6665 3.8835V8.66683H5.33317Z" fill={direction === 1 ? "#373A3E" : "#A9ACB2"}/>
</g>
</svg>
)

export const BulletIcon: React.FC<SVGProps<SVGSVGElement>> = (props) => (
Expand Down
95 changes: 95 additions & 0 deletions lib/sortingStrategies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { HeaderIndexes, DatasetMapping } from "./models";
import { getType, getPreciseAbbreviation } from "./helpers/rowHelpers";


export interface SortingStrategy {
doSort(data: string[], datasetMapping?: DatasetMapping, headerIndexes?: HeaderIndexes): string[];
toggleSortOrder(): void;
sortState: number;
}

export enum SortState {
Off = 0,
Ascending = 1,
Descending = 2
}

abstract class SortingStrategyBase implements SortingStrategy {
public sortState: SortState = SortState.Ascending;

public toggleSortOrder(): void {
this.sortState = (this.sortState + 1) % 3;
}

abstract doSort(data: string[], datasetMapping: DatasetMapping, headerIndexes: HeaderIndexes): string[];
}

export class CdeSortingFilter extends SortingStrategyBase {
public doSort(data: string[], datasetMapping: DatasetMapping, headerIndexes: HeaderIndexes): string[] {
if (this.sortState === SortState.Off) {
return Object.keys(datasetMapping);
}

const sortedData = data.slice();

sortedData.sort((a, b) => {
const rowA = datasetMapping[a];
const rowB = datasetMapping[b];
let comparisonResult = getPreciseAbbreviation(rowA, headerIndexes).localeCompare(getPreciseAbbreviation(rowB, headerIndexes));

if (this.sortState === SortState.Descending) {
comparisonResult *= -1;
}

return comparisonResult;
});

return sortedData;
}
}

export class VariableNameFilter extends SortingStrategyBase {
public doSort(data: string[], datasetMapping: DatasetMapping): string[] {
if (this.sortState === SortState.Off) {
return Object.keys(datasetMapping);
}

const sortedData = data.slice();

sortedData.sort((a, b) => {
let comparisonResult = a.localeCompare(b);

if (this.sortState === SortState.Descending) {
comparisonResult *= -1;
}

return comparisonResult;
});

return sortedData;
}
}

export class StatusFilter extends SortingStrategyBase {
public doSort(data: string[], datasetMapping: DatasetMapping, headerIndexes: HeaderIndexes): string[] {
if (this.sortState === SortState.Off) {
return Object.keys(datasetMapping)
}

const sortedData = data.slice();

sortedData.sort((a, b) => {
const rowA = datasetMapping[a];
const rowB = datasetMapping[b];
let comparisonResult = getType(rowA, headerIndexes).localeCompare(getType(rowB, headerIndexes));

if (this.sortState === SortState.Descending) {
comparisonResult *= -1;
}

return comparisonResult;
});

return sortedData;
}
}