CEC-4564: add trie select component (#404)
* add TrieSelect * setup menu button * CEC-4564: add trie select component * CEC-4564: fix selectall bool check * update tests
This commit is contained in:
@@ -339,58 +339,79 @@ exports[`Render Render 1`] = `
|
|||||||
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiFormControl-root MuiFormControl-fullWidth"
|
class="MuiBox-root MuiBox-root-0"
|
||||||
>
|
>
|
||||||
<label
|
<button
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required"
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||||
data-shrink="false"
|
disabled=""
|
||||||
id="select-can-signals-label"
|
|
||||||
>
|
|
||||||
Select CAN signals
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl"
|
|
||||||
inputvariant="outlined"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
aria-labelledby="select-can-signals-label select-can-signals"
|
|
||||||
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiInputBase-input MuiInput-input"
|
|
||||||
id="select-can-signals"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiSelect-nativeInput"
|
|
||||||
required=""
|
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiButton-label"
|
||||||
|
>
|
||||||
|
Select CAN Signals
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<label
|
||||||
|
class="MuiFormControlLabel-root MuiFormControlLabel-labelPlacementStart"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiCheckbox-root MuiCheckbox-colorSecondary PrivateSwitchBase-checked-0 Mui-checked MuiIconButton-colorSecondary"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
class="PrivateSwitchBase-input-0"
|
||||||
|
data-indeterminate="false"
|
||||||
|
type="checkbox"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSelect-icon"
|
class="MuiSvgIcon-root"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M7 10l5 5 5-5z"
|
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
|
||||||
|
>
|
||||||
|
Select All 0
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiCollapse-root MuiCollapse-hidden"
|
||||||
|
style="min-height: 0px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiCollapse-wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiCollapse-wrapperInner"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="MuiList-root MuiList-padding"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ul
|
||||||
|
class="makeStyles-chipList-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import DateFnsUtils from '@date-io/date-fns';
|
import DateFnsUtils from '@date-io/date-fns';
|
||||||
import { Button, Checkbox, Chip, CircularProgress, FormControl, FormControlLabel, Grid, InputLabel, ListItemText, MenuItem, Select } from "@material-ui/core";
|
import { Button, Checkbox, CircularProgress, FormControlLabel, Grid } from "@material-ui/core";
|
||||||
import { KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
|
import { KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { logger } from "../../../services/monitoring";
|
import { logger } from "../../../services/monitoring";
|
||||||
@@ -7,6 +7,7 @@ import { CANSignalsExportProvider, useCANSignalsExportContext } from "../../Cont
|
|||||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
import { useUserContext } from "../../Contexts/UserContext";
|
import { useUserContext } from "../../Contexts/UserContext";
|
||||||
import useStyles from "../../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
|
import { TrieSelect } from '../../Controls/TrieSelect';
|
||||||
|
|
||||||
const MainForm = ({ id }) => {
|
const MainForm = ({ id }) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
@@ -72,27 +73,19 @@ const MainForm = ({ id }) => {
|
|||||||
setSelectedEndDate(value);
|
setSelectedEndDate(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectedItemsChange = (event) => {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value.some(item => item === "Select All")) {
|
|
||||||
setSelectAllCanSignals(true);
|
|
||||||
if (selectedCanSignals.length === canSignals.length) {
|
|
||||||
setSelectedCanSignals([]);
|
|
||||||
} else {
|
|
||||||
setSelectedCanSignals(canSignals.map(signal => signal.signal_name));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSelectAllCanSignals(false);
|
|
||||||
setSelectedCanSignals(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayTimeAsGMT = (date) => {
|
const displayTimeAsGMT = (date) => {
|
||||||
return gmtTimezone
|
return gmtTimezone
|
||||||
? date.toLocaleString("en-US", {timeZone: "Etc/GMT"})
|
? date.toLocaleString("en-US", { timeZone: "Etc/GMT" })
|
||||||
: date;
|
: date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canSignals.length === selectedCanSignals.length) {
|
||||||
|
setSelectAllCanSignals(true);
|
||||||
|
} else {
|
||||||
|
setSelectAllCanSignals(false);
|
||||||
|
}
|
||||||
|
}, [canSignals, selectedCanSignals, setSelectAllCanSignals]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.paper}>
|
<div className={classes.paper}>
|
||||||
@@ -175,36 +168,12 @@ const MainForm = ({ id }) => {
|
|||||||
</MuiPickersUtilsProvider>
|
</MuiPickersUtilsProvider>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FormControl fullWidth required>
|
<TrieSelect
|
||||||
<InputLabel id="select-can-signals-label">Select CAN signals</InputLabel>
|
label="Select CAN Signals"
|
||||||
<Select
|
classification="Signals"
|
||||||
labelId="select-can-signals-label"
|
options={canSignals.map((signal => signal.signal_name))}
|
||||||
id="select-can-signals"
|
onChange={setSelectedCanSignals}
|
||||||
multiple
|
/>
|
||||||
value={selectedCanSignals}
|
|
||||||
onChange={handleSelectedItemsChange}
|
|
||||||
fullWidth
|
|
||||||
inputvariant="outlined"
|
|
||||||
renderValue={(selected) => (
|
|
||||||
<div className={classes.chips}>
|
|
||||||
{selected.map((value) => (
|
|
||||||
<Chip key={value} label={value} className={classes.chip} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MenuItem value="Select All">
|
|
||||||
<Checkbox checked={selectedCanSignals.length === canSignals.length} />
|
|
||||||
<ListItemText primary="Select All" />
|
|
||||||
</MenuItem>
|
|
||||||
{canSignals.map((signal) => (
|
|
||||||
<MenuItem key={signal.signal_name} value={signal.signal_name}>
|
|
||||||
<Checkbox checked={selectedCanSignals.indexOf(signal.signal_name) > -1} />
|
|
||||||
<ListItemText primary={signal.signal_name} />
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
205
src/components/Controls/TrieSelect/TrieSelect.jsx
Normal file
205
src/components/Controls/TrieSelect/TrieSelect.jsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Chip,
|
||||||
|
Collapse,
|
||||||
|
FormControlLabel,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import ExpandLess from '@material-ui/icons/ExpandLess';
|
||||||
|
import ExpandMore from '@material-ui/icons/ExpandMore';
|
||||||
|
import { Trie } from "./trie";
|
||||||
|
import { TrieSelectProvider, useTrieSelect } from "./TrieSelectContext";
|
||||||
|
import useStyles from "../../useStyles";
|
||||||
|
|
||||||
|
export const TrieSelect = ({
|
||||||
|
label,
|
||||||
|
classification,
|
||||||
|
onChange = () => { },
|
||||||
|
options = [],
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TrieSelectProvider onChange={onChange}>
|
||||||
|
<TrieSelectList
|
||||||
|
label={label}
|
||||||
|
classification={classification}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</TrieSelectProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrieSelectList = ({
|
||||||
|
label,
|
||||||
|
classification,
|
||||||
|
options,
|
||||||
|
}) => {
|
||||||
|
const { selected, setSelected, remove } = useTrieSelect();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const classes = useStyles();
|
||||||
|
const trie = new Trie(options);
|
||||||
|
|
||||||
|
const handleExpand = () => {
|
||||||
|
setOpen(open => !open);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
setSelected((selected) => {
|
||||||
|
if (selected.length === 0) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Button onClick={handleExpand} variant="contained" color="primary" disabled={options.length === 0}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
<FormControlLabel
|
||||||
|
label={`Select All ${options.length}`}
|
||||||
|
labelPlacement="start"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={selected.length === options.length}
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Collapse in={open}>
|
||||||
|
<List>
|
||||||
|
<TrieSelectLevel
|
||||||
|
node={trie.getRoot()}
|
||||||
|
classification={classification}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
<ul className={classes.chipList}>
|
||||||
|
{selected.map((signal) => {
|
||||||
|
return (
|
||||||
|
<li key={signal}>
|
||||||
|
<Chip label={signal} onDelete={() => remove(signal)} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrieSelectLevel = ({
|
||||||
|
prefix = "",
|
||||||
|
node,
|
||||||
|
children,
|
||||||
|
classification
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { selected, add, remove } = useTrieSelect();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const completeChildren = Object.values(node.children).filter(child => child.isComplete);
|
||||||
|
const descendantCount = `${node.count} ${classification}`;
|
||||||
|
|
||||||
|
const handleExpand = () => {
|
||||||
|
setOpen(open => !open);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheck = (names = [], checked) => {
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
if (checked) {
|
||||||
|
remove(names[i]);
|
||||||
|
} else {
|
||||||
|
add(names[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWords = (node, prepend = prefix, result = new Set()) => {
|
||||||
|
if (prepend === prefix && node.isComplete) result.add(prepend);
|
||||||
|
|
||||||
|
for (const child of Object.values(node.children)) {
|
||||||
|
const word = (prepend ? prepend + "_" : "") + child.data;
|
||||||
|
if (child.isComplete) {
|
||||||
|
result.add(word);
|
||||||
|
}
|
||||||
|
if (child.children) {
|
||||||
|
getWords(child, word, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItems = (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
{Object.values(node.children).map((child) => {
|
||||||
|
const fullName = (prefix ? prefix + "_" : "") + child.data;
|
||||||
|
const isChecked = selected.includes(fullName);
|
||||||
|
return (
|
||||||
|
<TrieSelectLevel
|
||||||
|
prefix={fullName}
|
||||||
|
node={child}
|
||||||
|
key={fullName}
|
||||||
|
classification={classification}
|
||||||
|
>
|
||||||
|
{child.isComplete && (
|
||||||
|
<ListItem className={classes.whiteBackground}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Checkbox checked={isChecked} onClick={() => handleCheck([fullName], isChecked)} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={fullName} />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</TrieSelectLevel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const isParentOfMultiple = completeChildren.length > 1;
|
||||||
|
const isAdoptiveParentOfMultiple = completeChildren.length <= 1 && node.count > 1;
|
||||||
|
if (isParentOfMultiple || isAdoptiveParentOfMultiple) {
|
||||||
|
const allDescendants = getWords(node, prefix);
|
||||||
|
const isSelectAll = allDescendants.every(descendant => selected.includes(descendant));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListItem onClick={handleExpand} divider>
|
||||||
|
<ListItemText primary={prefix} secondary={descendantCount} />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
||||||
|
<FormControlLabel
|
||||||
|
label="Select All"
|
||||||
|
labelPlacement="start"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelectAll}
|
||||||
|
onClick={() => handleCheck(allDescendants, isSelectAll)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</Box>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={open}>
|
||||||
|
<List>
|
||||||
|
{listItems}
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return listItems;
|
||||||
|
}
|
||||||
54
src/components/Controls/TrieSelect/TrieSelect.test.tsx
Normal file
54
src/components/Controls/TrieSelect/TrieSelect.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { TrieSelect } from ".";
|
||||||
|
import addSnapshotSerializer from "../../../utils/snapshot";
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
"ace",
|
||||||
|
"ace_bev_one",
|
||||||
|
"ace_bev_two",
|
||||||
|
"ace_bev_three",
|
||||||
|
"bev",
|
||||||
|
"bev_chaz_deep",
|
||||||
|
"bev_chaz_deep_one",
|
||||||
|
"bev_chaz_deep_two",
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("TrieSelect", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
addSnapshotSerializer(expect);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Render", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TrieSelect
|
||||||
|
label={"The button label"}
|
||||||
|
classification="Signal"
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
/* render */
|
||||||
|
});
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("properly passes payload to callback", async () => {
|
||||||
|
const mockCallback = jest.fn();
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<TrieSelect
|
||||||
|
label={"The input label"}
|
||||||
|
classification="Signal"
|
||||||
|
options={options}
|
||||||
|
onChange={mockCallback}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectAll = getByText("Select All 8");
|
||||||
|
userEvent.click(selectAll);
|
||||||
|
expect(mockCallback).toHaveBeenCalledWith(options);
|
||||||
|
});
|
||||||
|
});
|
||||||
29
src/components/Controls/TrieSelect/TrieSelectContext.js
Normal file
29
src/components/Controls/TrieSelect/TrieSelectContext.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createContext, useState, useContext, useEffect } from "react";
|
||||||
|
|
||||||
|
const TrieSelectContext = createContext();
|
||||||
|
|
||||||
|
export const useTrieSelect = () => {
|
||||||
|
const context = useContext(TrieSelectContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useTrieSelect must be used within a TrieSelectProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TrieSelectProvider = ({ children, onChange }) => {
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
|
const add = (id) => setSelected(prev => [...prev, id]);
|
||||||
|
const remove = (id) => setSelected(prev => prev.filter(select => select !== id));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange(selected);
|
||||||
|
}, [onChange, selected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TrieSelectContext.Provider value={{ selected, setSelected, add, remove }}>
|
||||||
|
{children}
|
||||||
|
</TrieSelectContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
1
src/components/Controls/TrieSelect/index.js
Normal file
1
src/components/Controls/TrieSelect/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { TrieSelect } from "./TrieSelect";
|
||||||
62
src/components/Controls/TrieSelect/trie.js
Normal file
62
src/components/Controls/TrieSelect/trie.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
class Node {
|
||||||
|
#count
|
||||||
|
|
||||||
|
constructor(data, isComplete = false) {
|
||||||
|
this.data = data;
|
||||||
|
this.children = {};
|
||||||
|
this.isComplete = isComplete;
|
||||||
|
this.#count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get count() {
|
||||||
|
return this.#count;
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementCount() {
|
||||||
|
this.#count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Trie {
|
||||||
|
constructor(words = []) {
|
||||||
|
this.root = new Node();
|
||||||
|
this.populate(words);
|
||||||
|
}
|
||||||
|
|
||||||
|
populate(words) {
|
||||||
|
for (const word of words) {
|
||||||
|
this.add(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(parts, node = this.root) {
|
||||||
|
if (typeof parts === "string") {
|
||||||
|
parts = parts.split("_");
|
||||||
|
}
|
||||||
|
|
||||||
|
node.incrementCount();
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
node.isComplete = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const part = parts.shift();
|
||||||
|
|
||||||
|
if (node.children[part]) {
|
||||||
|
this.add(parts, node.children[part]);
|
||||||
|
} else {
|
||||||
|
const newNode = new Node(part);
|
||||||
|
node.children[part] = newNode;
|
||||||
|
this.add(parts, newNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoot() {
|
||||||
|
return this.root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Trie,
|
||||||
|
};
|
||||||
24
src/components/Controls/TrieSelect/trie.test.js
Normal file
24
src/components/Controls/TrieSelect/trie.test.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Trie } from "./trie";
|
||||||
|
|
||||||
|
describe("Trie", () => {
|
||||||
|
it("adds words from instantiation", () => {
|
||||||
|
const trie = new Trie(["AAA_BBB_CCC"]);
|
||||||
|
const { children: { AAA: root } } = trie.getRoot();
|
||||||
|
|
||||||
|
expect(root.data).toBe("AAA");
|
||||||
|
expect(root.children["BBB"].data).toBe("BBB");
|
||||||
|
expect(root.children["BBB"].isComplete).toBe(false);
|
||||||
|
expect(root.children["BBB"].children["CCC"].data).toBe("CCC");
|
||||||
|
expect(root.children["BBB"].children["CCC"].isComplete).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds words with add method", () => {
|
||||||
|
const trie = new Trie();
|
||||||
|
trie.add("AAA_BBB_CCC");
|
||||||
|
const { children: { AAA: root } } = trie.getRoot();
|
||||||
|
|
||||||
|
expect(root.data).toBe("AAA");
|
||||||
|
expect(root.children["BBB"].data).toBe("BBB");
|
||||||
|
expect(root.children["BBB"].children["CCC"].data).toBe("CCC");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -313,6 +313,13 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "12px",
|
gap: "12px",
|
||||||
},
|
},
|
||||||
|
chipList: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "4px 8px",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
listStyleType: "none",
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
flex: {
|
flex: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user