Skip to content

Commit

Permalink
fixes part types selector memoization losing the current part's state…
Browse files Browse the repository at this point in the history
…. Complex memoization is fun
  • Loading branch information
replaysMike committed May 11, 2023
1 parent dcfc0f7 commit 243c915
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 107 deletions.
206 changes: 103 additions & 103 deletions Binner/Binner.Web/ClientApp/src/components/PartTypeSelectorMemoized.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,52 @@ import "./PartTypeSelector.css";
* Part type selector dropdown (treeview with icons)
* [memoized]
*/
export default function PartTypeSelectorMemoized(props) {
export default function PartTypeSelectorMemoized({ partTypes, loadingPartTypes, label, name, value, onSelect, onBlur, onFocus }) {
const { t } = useTranslation();
PartTypeSelectorMemoized.abortController = new AbortController();
const [partTypes, setPartTypes] = useState(props.partTypes);
const [partTypesFiltered, setPartTypesFiltered] = useState([]);
const [internalPartTypes, setInternalPartTypes] = useState(partTypes);
const [internalPartTypesFiltered, setInternalPartTypesFiltered] = useState([]);
const [partTypeId, setPartTypeId] = useState(0);
const [partType, setPartType] = useState({ partTypeId: 0, name: ""});
const [filter, setFilter] = useState('');
const [expandedNodeIds, setExpandedNodeIds] = useState([]);
const [loadingPartTypes, setLoadingPartTypes] = useState(false);
const [loading, setLoading] = useState(false);

const getPartTypeFromId = useCallback((partTypeId) => {
let partTypeIdInt = partTypeId;
if (typeof partTypeId === "string")
partTypeIdInt = parseInt(partTypeId);
else if(typeof partTypeId === "object")
return partTypeId;
return _.find(partTypes, (i) => i.partTypeId === partTypeIdInt);
}, [partTypes]);

const getPartTypeFromName = (name) => {
const lcName = name.toLowerCase();
return _.find(partTypes, (i) => i.name.toLowerCase() === lcName)
};
return _.find(internalPartTypes, (i) => i.partTypeId === partTypeIdInt);
}, [internalPartTypes]);

useEffect(() => {
setPartTypes(props.partTypes);
setPartTypesFiltered(props.partTypes);
}, [props.partTypes]);
setInternalPartTypes(partTypes);
setInternalPartTypesFiltered(partTypes);
}, [partTypes]);

useEffect(() => {
setLoadingPartTypes(props.loadingPartTypes);
}, [props.loadingPartTypes]);
setLoading(loadingPartTypes);
}, [loadingPartTypes]);

useEffect(() => {
const type = typeof props.value;
const type = typeof value;
let newPartTypeId = 0;
if (type === "string") {
newPartTypeId = parseInt(props.value);
newPartTypeId = parseInt(value);
} else if (type === "number") {
newPartTypeId = props.value;
newPartTypeId = value;
} else {
console.error(`Unknown value type specified: ${props.value} = ${type}`);
console.error(`Unknown value type specified: ${value} = ${type}`);
return;
}
const newPartType = getPartTypeFromId(newPartTypeId);
if (newPartTypeId !== 0) {
setPartTypeId(newPartTypeId);
setPartType(newPartType);
}
}, [props.value, props.partTypes, getPartTypeFromId]);
}, [value, partTypes, getPartTypeFromId]);

const StyledTreeItemRoot = styled(TreeItem)(({ theme }) => ({
color: theme.palette.text.secondary,
Expand Down Expand Up @@ -125,12 +120,12 @@ export default function PartTypeSelectorMemoized(props) {
);
};

const recursivePreFilter = (partTypes, parentPartTypeId, filterBy) => {
const recursivePreFilter = useCallback(() => (allPartTypes, parentPartTypeId, filterBy) => {
// go through every child, mark filtered matches

const filterByLowerCase = filterBy.toLowerCase();
const childrenComponents = [];
let partTypesInCategory = _.filter(partTypes, (i) => i.parentPartTypeId === parentPartTypeId);
let partTypesInCategory = _.filter(allPartTypes, (i) => i.parentPartTypeId === parentPartTypeId);
for(let i = 0; i < partTypesInCategory.length; i++){
partTypesInCategory[i].exactMatch = partTypesInCategory[i].name.toLowerCase() === filterByLowerCase;
if (partTypesInCategory[i].name.toLowerCase().includes(filterByLowerCase)){
Expand All @@ -141,7 +136,7 @@ export default function PartTypeSelectorMemoized(props) {
childrenComponents.push(partTypesInCategory[i]);

// now filter the children of this category
const childs = recursivePreFilter(partTypes, partTypesInCategory[i].partTypeId, filterBy);
const childs = recursivePreFilter(allPartTypes, partTypesInCategory[i].partTypeId, filterBy);
if (_.find(childs, i => i.filterMatch)) {
// make sure the parent matches the filter because it has children that does
partTypesInCategory[i].filterMatch = true;
Expand All @@ -151,20 +146,20 @@ export default function PartTypeSelectorMemoized(props) {
}
}
return childrenComponents;
};
}, []);

const recursiveTreeItem = (partTypes, parentPartTypeId = null) => {
const recursiveTreeItem = useCallback((allPartTypes, parentPartTypeId = null) => {
// build a tree graph

let children = _.filter(partTypes, (i) => i.parentPartTypeId === parentPartTypeId);
let children = _.filter(allPartTypes, (i) => i.parentPartTypeId === parentPartTypeId);

const childrenComponents = [];
if (children && children.length > 0) {
for (let i = 0; i < children.length; i++) {
const key = `${children[i].name}-${i}`;
const nodeId = `${children[i].name}`;
const childs = recursiveTreeItem(partTypes, children[i].partTypeId);
const basePartTypeName = _.find(partTypes, x => x.partTypeId === children[i].parentPartTypeId)?.name;
const childs = recursiveTreeItem(allPartTypes, children[i].partTypeId);
const basePartTypeName = _.find(allPartTypes, x => x.partTypeId === children[i].parentPartTypeId)?.name;
const partTypeName = children[i].name;
childrenComponents.push(
<StyledTreeItem
Expand All @@ -186,88 +181,93 @@ export default function PartTypeSelectorMemoized(props) {
}

return childrenComponents;
};

const handleOnSearchChange = (e, control) => {
// process keyboard input
setFilter(control.searchQuery);
let newPartTypesFiltered = recursivePreFilter(partTypes, null, control.searchQuery.toLowerCase());
// now remove all part types that don't match the filter
newPartTypesFiltered = _.filter(partTypes, i => i.filterMatch === true);
const newPartTypesFilteredOrdered = _.sortBy(newPartTypesFiltered, x => x.exactMatch ? 0 : 1);
setPartTypesFiltered(newPartTypesFilteredOrdered);
if (control.searchQuery.length > 1) {
setExpandedNodeIds(_.map(newPartTypesFiltered, (i) => (i.name)));
}else{
setExpandedNodeIds([]);
}
};

const handleOnNodeSelect = (e, selectedPartTypeName) => {
const selectedPartType = getPartTypeFromName(selectedPartTypeName);
if (selectedPartType) {
setPartType(selectedPartType);
// fire event
if (props.onSelect) props.onSelect(e, selectedPartType);
}
};

const handleOnNodeToggle = (e, node) => {
//e.preventDefault();
//e.stopPropagation();
// preventing event propagation leads to ui weirdness unfortunately
if (expandedNodeIds.includes(node))
setExpandedNodeIds(_.filter(expandedNodeIds, i => i !== node));
else
setExpandedNodeIds(node);
};

const handleOnBlur = (e, control) => {
e.stopPropagation();
if (props.onBlur) props.onBlur(e, control);
// reset the search filtering
setFilter(null);
setExpandedNodeIds([]);
setPartTypesFiltered([...partTypes]);
};

const handleOnFocus = (e, control) => {
setFilter('');
if (props.onFocus) props.onFocus(e, control);
};

const handleInternalOnBlur = (e, control) => {
if (props.onBlur) props.onBlur(e, control);
};

const handleInternalOnFocus = (e, control) => {
document.getElementById("partTypeDropdown").firstChild.focus();
if (props.onFocus) props.onFocus(e, control);
};
}, []);

const getSelectedText = (partType) => {
if (partType) {
return partType?.name || "";
}
return "";
};

const getSelectedIcon = (partType) => {
if (partType) {
const basePartTypeName = partType?.parentPartTypeId && _.find(partTypes, x => x.partTypeId === partType?.parentPartTypeId)?.name;
const partTypeName = partType?.name;
return (partType && getIcon(partType?.name, basePartTypeName)({className: `parttype parttype-${basePartTypeName || partTypeName}`}));
}
return "";
};
};

const render = useMemo(() => {
const getPartTypeFromName = (name) => {
const lcName = name.toLowerCase();
return _.find(internalPartTypes, (i) => i.name.toLowerCase() === lcName)
};

const handleOnSearchChange = (e, control) => {
// process keyboard input
setFilter(control.searchQuery);
let newPartTypesFiltered = recursivePreFilter(internalPartTypes, null, control.searchQuery.toLowerCase());
// now remove all part types that don't match the filter
newPartTypesFiltered = _.filter(internalPartTypes, i => i.filterMatch === true);
const newPartTypesFilteredOrdered = _.sortBy(newPartTypesFiltered, x => x.exactMatch ? 0 : 1);
setInternalPartTypesFiltered(newPartTypesFilteredOrdered);
if (control.searchQuery.length > 1) {
setExpandedNodeIds(_.map(newPartTypesFiltered, (i) => (i.name)));
}else{
setExpandedNodeIds([]);
}
};

const handleOnNodeSelect = (e, selectedPartTypeName) => {
const selectedPartType = getPartTypeFromName(selectedPartTypeName);
if (selectedPartType) {
setPartType(selectedPartType);
// fire event
if (onSelect) onSelect(e, selectedPartType);
}
};

const handleOnNodeToggle = (e, node) => {
//e.preventDefault();
//e.stopPropagation();
// preventing event propagation leads to ui weirdness unfortunately
if (expandedNodeIds.includes(node))
setExpandedNodeIds(_.filter(expandedNodeIds, i => i !== node));
else
setExpandedNodeIds(node);
};

const handleOnBlur = (e, control) => {
e.stopPropagation();
if (onBlur) onBlur(e, control);
// reset the search filtering
setFilter(null);
setExpandedNodeIds([]);
setInternalPartTypesFiltered([...internalPartTypes]);
};

const handleOnFocus = (e, control) => {
setFilter('');
if (onFocus) onFocus(e, control);
};

const handleInternalOnBlur = (e, control) => {
if (onBlur) onBlur(e, control);
};

const handleInternalOnFocus = (e, control) => {
document.getElementById("partTypeDropdown").firstChild.focus();
if (onFocus) onFocus(e, control);
};

const getSelectedIcon = (partType) => {
if (partType) {
const basePartTypeName = partType?.parentPartTypeId && _.find(internalPartTypes, x => x.partTypeId === partType?.parentPartTypeId)?.name;
const partTypeName = partType?.name;
return (partType && getIcon(partType?.name, basePartTypeName)({className: `parttype parttype-${basePartTypeName || partTypeName}`}));
}
return "";
};

return (
<div className="partTypeSelector-container">
<div className="icon">{getSelectedIcon(partType)}</div>
<Dropdown
id="partTypeDropdown"
name={props.name || ""}
name={name || ""}
text={getSelectedText(partType)}
search
floating
Expand All @@ -277,8 +277,8 @@ export default function PartTypeSelectorMemoized(props) {
onSearchChange={handleOnSearchChange}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
disabled={loadingPartTypes}
loading={loadingPartTypes}
disabled={loading}
loading={loading}
>
<Dropdown.Menu>
<Dropdown.Item>
Expand All @@ -296,17 +296,17 @@ export default function PartTypeSelectorMemoized(props) {
selected={partType?.name || ""}
sx={{ flexGrow: 1, maxWidth: "100%" }}
>
{recursiveTreeItem(partTypesFiltered).map((x) => x)}
{recursiveTreeItem(internalPartTypesFiltered).map((x) => x)}
</TreeView>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>);
}, [partType, partTypesFiltered, expandedNodeIds]);
}, [partType, internalPartTypes, internalPartTypesFiltered, expandedNodeIds, onSelect]);

return (
<>
<label>{props.label}</label>
<label>{label}</label>
{render}
</>
);
Expand Down
8 changes: 4 additions & 4 deletions Binner/Binner.Web/ClientApp/src/pages/Inventory.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo, useRef } from "react";
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { useParams, useNavigate, Link } from "react-router-dom";
import { useTranslation, Trans } from 'react-i18next';
import PropTypes from "prop-types";
Expand Down Expand Up @@ -653,7 +653,7 @@ export function Inventory(props) {
if (viewPreferences.rememberLast && !isEditing) updateViewPreferences({lastPartTypeId: partType.partTypeId});
setPart({...part, partTypeId: partType.partTypeId});
setIsDirty(true);
}
};

const handleChange = (e, control) => {
e.preventDefault();
Expand Down Expand Up @@ -946,7 +946,8 @@ export function Inventory(props) {
: t('page.inventory.addtitle', "Add Inventory");

/*<MatchingPartsMemoized part={part} metadataParts={metadataParts} partTypes={partTypes} setPartFromMetadata={setPartFromMetadata} />*/
const renderForm = useMemo(() => {
const renderForm = useMemo(() => {

return (
<>
<div className="page-banner">
Expand Down Expand Up @@ -1374,7 +1375,6 @@ export function Inventory(props) {
duplicateParts={duplicateParts}
onSetPart={setPart}
onSubmit={onSubmit}

/>
<Confirm
className="confirm"
Expand Down

0 comments on commit 243c915

Please sign in to comment.