/
solomin.dv
/
FrontendDeveloper
Обзор
Документация
Войти
/
solomin.dv
/
FrontendDeveloper
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
TreeViewClassifier/index.jsx
765 строк
27 KB
Даниил Соломин
create TreeViewClassifier/index.jsx
11 июн 2024, 17:32
11 июн 2024, 17:32
332fc20
Код
Авторство
О чём код?
import React, { useEffect, useState, useMemo, } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { makeStyles } from '@mui/styles'; import { TreeView, TreeItem } from '@mui/lab'; import { ExpandMore, ChevronRight, Close } from '@mui/icons-material/'; import { Alert, CircularProgress, Checkbox, Stack, IconButton, Typography, } from '@mui/material'; import _ from 'lodash'; import classNames from 'classnames'; import { isolateElements } from 'components/Viewer/viewer-helper'; import { setActiveClassifyingSide } from 'redux/actions/app'; import { getRgbString } from 'functions/colorizing'; function isEventFromIcon(e) { const testId = e.target.dataset.testid || e.target.parentElement.dataset.testid; return testId === 'ExpandMoreIcon' || testId === 'ChevronRightIcon'; } const useStyles = makeStyles(() => ({ root: { flexGrow: 1, textAlign: 'left', }, checkbox: { padding: '2px !important', }, relationshipElement: { color: 'rgba(0, 0, 0, 0.26)', fontSize: 15, }, colorMarker: { minWidth: '12px', minHeight: '12px', borderRadius: '50%', }, })); function TreeItems({ classifiersSearchValue, tree, code = 'null', side, checked, setChecked, setCheckedAll, disableSelection, disabledElements, classifiedElements, onDeleteClick, isShowClassifierCode, isShowVolume, elementsFilterValue, type, idsToColors = {}, }) { const classes = useStyles(); const { isShowColor } = useSelector((state) => state.classifiers); return ( <> {tree[code].length > 0 && tree[code].map((item) => { const nodeId = item.id; const isDisabled = disabledElements.indexOf(item.id) !== -1; const relationshipCheckboxClasses = classNames({ [classes.relationshipElement]: isDisabled, [classes.checkbox]: true, }); let classifiedItems; const classifiedValues = { area: { value: 0, unit: null }, length: { value: 0, unit: null }, volume: { value: 0, unit: null }, }; const getValuesSumm = (props, key) => ({ value: classifiedValues[key].value + props[key].value, unit: props[key].unit || classifiedValues[key].unit, }); if (classifiedElements[item.id]) { classifiedItems = classifiedElements[item.id] .map((element) => { if (type === 'model') { classifiedValues.area = getValuesSumm(element.props, 'area'); classifiedValues.length = getValuesSumm(element.props, 'length'); classifiedValues.volume = getValuesSumm(element.props, 'volume'); } return ({ relationshipId: element.id, data: element.data, props: element.props }); }); } const isBeforeLast = () => { let result = true; if (tree[item.code] && side === 'left') { tree[item.code].forEach((branch) => { if (tree[branch.code]) { result = false; } }); } else { result = false; } return result; }; const isAllDisabled = () => { let result = true; if (isBeforeLast()) { tree[item.code].forEach(({ id }) => { if (!(Array.isArray(disabledElements) ? disabledElements.indexOf(id) !== -1 : disabledElements === id)) { result = false; } }); } else { result = false; } return result; }; const isDisabledId = (id) => Boolean(disabledElements .find((disabledElement) => disabledElement === id)); const isInFilesList = (id) => Boolean(tree[item.code].find((file) => file.id === id)); const getSelectStatus = () => { const files = []; tree[item.code].forEach(({ id }) => { if (!(isDisabledId(id))) { files.push(id); } }); const selectedFilesLength = checked.filter((id) => isInFilesList(id)).length; const filesLength = files.length; return { checked: filesLength > 0 && selectedFilesLength === filesLength, indeterminate: selectedFilesLength > 0 && selectedFilesLength < filesLength, }; }; const selectAll = (event) => { const notDisabledIds = []; tree[item.code].forEach(({ id }) => { if (!(isDisabledId(id))) { notDisabledIds.push(id); setCheckedAll(notDisabledIds, event.target.checked); } }); }; return ( <TreeItem key={item.id} nodeId={String(nodeId)} className={isDisabled && classes.relationshipElement} collapseIcon={tree[item.code] || (classifiedItems && elementsFilterValue !== 'unrelated') ? '' : <div />} expandIcon={tree[item.code] || (classifiedItems && elementsFilterValue !== 'unrelated') ? '' : <div />} label={( <Stack spacing={1} direction="row" sx={{ py: '1px' }} alignItems="center" > {isBeforeLast() && !disableSelection && ( <Checkbox size="small" indeterminate={getSelectStatus().indeterminate} checked={getSelectStatus().checked} className={relationshipCheckboxClasses} onClick={(e) => e.stopPropagation()} onChange={selectAll} disabled={isAllDisabled()} /> )} {!tree[item.code] && !disableSelection && ( <Checkbox size="small" disabled={isDisabled} checked={Array.isArray(checked) ? checked.indexOf(nodeId) !== -1 : checked === nodeId} className={relationshipCheckboxClasses} onClick={(e) => e.stopPropagation()} onChange={(e) => setChecked(nodeId, e.target.checked)} /> )} {(idsToColors[item.id] && classifiedItems && isShowColor) && ( <div style={{ backgroundColor: getRgbString(idsToColors[item.id]) }} className={classes.colorMarker} /> )} {isShowClassifierCode && ((type === 'model' && side === 'right') || (type === 'file')) && ( <Typography sx={{ color: isDisabled ? 'rgba(0,0,0,0.26)' : '#9e9e9e', fontSize: 14, }} > {item.code} </Typography> )} <Typography sx={isAllDisabled() && { color: 'rgba(0,0,0,0.26)' }}> {item.description} </Typography> {isShowVolume && type === 'model' && elementsFilterValue !== 'unrelated' && !classifiersSearchValue && ( <Typography style={{ fontSize: 12, marginLeft: '8px', whiteSpace: 'nowrap', color: '#0E990E', }} > {`${ classifiedValues.volume.value ? classifiedValues.volume.value.toFixed(2) : ''} ${classifiedValues.volume.unit || ''}`} </Typography> )} </Stack> )} > {tree[item.code] && ( <TreeItems classifiersSearchValue={classifiersSearchValue} tree={tree} disabledElements={disabledElements} classifiedElements={classifiedElements} code={item.code} type={type} side={side} checked={checked} setChecked={setChecked} setCheckedAll={setCheckedAll} isShowClassifierCode={isShowClassifierCode} isShowVolume={isShowVolume} elementsFilterValue={elementsFilterValue} disableSelection={disableSelection} onDeleteClick={onDeleteClick} idsToColors={idsToColors} /> )} {classifiedItems && (elementsFilterValue !== 'unrelated') && ( classifiedItems.map(({ relationshipId, data, props }) => ( <> {data && ( <TreeItem key={`classified-${data.id}`} nodeId={`classified-${data.id}`} label={( <Stack direction="row" spacing={1} sx={{ marginLeft: '16px', marginBottom: '3px', }} > {isShowClassifierCode && type !== 'model' && ( <Typography sx={{ fontSize: '14px', color: (theme) => theme.palette.grey['500'], }} > {data.code} </Typography> )} <Stack direction="row" alignItems="center" spacing={1}> <Typography sx={{ fontSize: '14.3px' }}> {data.description} </Typography> {isShowVolume && type === 'model' && ( <span style={{ fontSize: 12, marginLeft: '8px', whiteSpace: 'nowrap', color: '#0E990E', }} > {`${props.volume.value ? props.volume.value.toFixed(2) : ''} ${props.volume.unit || ''}`} </span> )} <IconButton size="small" sx={{ p: '1px' }} onClick={() => onDeleteClick(relationshipId)} > <Close sx={{ fontSize: '15px !important' }} /> </IconButton> </Stack> </Stack> )} /> )} </> )) )} </TreeItem> ); })} </> ); } export default function TreeViewClassifier(props) { const { disabledElements, classifiersSearchValue, classifiers, classifiedElements, multiSelect = false, disableSelection = false, onDeleteClick, type, viewer, side, selected, onSelect = () => {}, onChecked = () => {}, idsToColors, expanded, setExpanded, isCollapse, setIsCollapse, fileToCompare, } = props; const classes = useStyles(); const dispatch = useDispatch(); const initialChecked = multiSelect ? [] : null; const [checked, setChecked] = useState(initialChecked); const selectedModel = useSelector((state) => state.projects.selectedModel); const activeSide = useSelector((state) => state.app.activeClassifyingSide); const { elementsFilterValue, isShowVolume, isShowClassifierCode, } = useSelector((state) => state.classifiers); const isAutoClassifying = useSelector((state) => state.app.isNeedToCreateRelationships); let classifiersTree = _.groupBy(classifiers, (c) => c.parent_code); const expandedSearchedElements = []; const searchedElements = []; const expandedBeforeSearch = useMemo(() => !classifiersSearchValue && expanded, [fileToCompare]); useEffect(() => { onChecked(checked); }, [checked]); function filterTree(tree, key) { const currentElements = tree[key]; return currentElements.filter((obj) => { const nestedTree = tree[obj.code]; let isRemove = false; if (nestedTree) { const filteredTree = filterTree(tree, obj.code); isRemove = filteredTree.length === 0; } else if (elementsFilterValue === 'related') { if (side === 'left') isRemove = !disabledElements.includes(obj.id); else isRemove = !Object.keys(classifiedElements).includes(String(obj.id)); } else if (elementsFilterValue === 'unrelated') { if (side === 'left') isRemove = disabledElements.includes(obj.id); else isRemove = false; } return !isRemove; }); } function collapseTree(tree, key) { const currentElements = tree[key]; return currentElements.filter((obj) => { const nestedTree = tree[obj.code]; let isRemove; if (nestedTree) { const collapsedTree = collapseTree(tree, obj.code); isRemove = collapsedTree.length === 0; } else { isRemove = !Object.keys(classifiedElements).includes(String(obj.id)); } return !isRemove; }); } function searchTree(tree, key) { const currentElements = tree[key]; function pushExpandedSearchedElements(classifier) { if (!expandedSearchedElements .find((expandedElement) => expandedElement.id === classifier.id)) { expandedSearchedElements.push(classifier); } const expandedIdsArray = expandedSearchedElements .map((expandedElement) => expandedElement.id.toString()); setExpanded(expandedIdsArray); } return currentElements.filter((obj) => { const nestedTree = tree[obj.code]; let isRemove = false; let isHaveTypeSize = false; if (classifiersSearchValue) { const isFindSearchValue = (objectDescription) => objectDescription.toLowerCase() .includes(classifiersSearchValue.toLowerCase()); if (nestedTree) { const searchedTree = searchTree(tree, obj.code); if (obj.parent_code === null && !isFindSearchValue(obj.description)) { pushExpandedSearchedElements(obj); } isRemove = searchedTree.length === 0; if (searchedTree.length > 0 && searchedTree .some((innerClassifier) => isFindSearchValue(innerClassifier.description))) { pushExpandedSearchedElements(obj); } if (isFindSearchValue(obj.description)) { searchedElements.push(obj); tree[obj.code].forEach((treeElement) => searchedElements.push(treeElement)); isRemove = false; } else { searchedElements.push(obj); tree.null.forEach((topClassifier) => { if (obj.parent_code === topClassifier.code) { searchedElements.push(obj); const innerClassifiers = tree[topClassifier.code]; const finiteClassifiers = tree[obj.code]; isRemove = searchedTree.length === 0; const isSearchedTopClassifier = isFindSearchValue(topClassifier.description) && !innerClassifiers .some((innerClassifier) => isFindSearchValue(innerClassifier.description)) && !finiteClassifiers .some((finiteClassifier) => isFindSearchValue(finiteClassifier.description)); const isSearchedFiniteClassifier = !isFindSearchValue(topClassifier.description) && !innerClassifiers .some((innerClassifier) => isFindSearchValue(innerClassifier.description)) && finiteClassifiers .some((finiteClassifier) => isFindSearchValue(finiteClassifier.description)); isRemove = !isSearchedTopClassifier && !searchedTree .some((innerClassifier) => isFindSearchValue(innerClassifier.description)); if (isSearchedFiniteClassifier) { isRemove = false; } Object.values(classifiedElements).forEach((classifiedElement) => { classifiedElement.forEach((typeSizeElement) => { finiteClassifiers.forEach((innerClassifier) => { const isClassifierHaveSearchedTypeSize = (innerClassifier.id === (type === 'model' ? typeSizeElement.classifier_id : typeSizeElement.classifier_id1)) || (innerClassifier.id === (type === 'model' ? typeSizeElement.classifier_id : typeSizeElement.classifier_id2)); if (isClassifierHaveSearchedTypeSize) { isRemove = false; } else if (searchedTree.length === 0) { isRemove = true; } }); }); }); } }); } } if (!nestedTree) { const innerClassifiers = tree[obj.parent_code]; if (classifiedElements) { Object.values(classifiedElements).forEach((classifiedElement) => { classifiedElement.forEach((element) => { if (((obj.id === (type === 'model' ? element.classifier_id : element.classifier_id2)) || (obj.id === (type === 'model' ? element.classifier_id : element.classifier_id1))) && isFindSearchValue(element.data.description)) { isHaveTypeSize = true; isRemove = false; setIsCollapse(false); pushExpandedSearchedElements(obj); } }); }); } isRemove = !isFindSearchValue(obj.description) && !isHaveTypeSize; searchedElements.forEach((searchedElement) => { if (searchedElement.code === obj.parent_code) { searchedElements.push(obj); if (obj.parent_code === searchedElement.code && isFindSearchValue(searchedElement.description)) { isRemove = false; } const topClassifier = tree.null .filter((treeElement) => treeElement.code === searchedElement.parent_code); if (!topClassifier.some((classifier) => isFindSearchValue(classifier.description)) && isFindSearchValue(searchedElement.description) && !innerClassifiers .some((innerClassifier) => isFindSearchValue(innerClassifier.description))) { isRemove = false; } const allInFirstLevel = topClassifier .some((classifier) => isFindSearchValue(classifier.description)) && !innerClassifiers .some((innerClassifier) => isFindSearchValue(innerClassifier.description)) && !isFindSearchValue(searchedElement.description); if (allInFirstLevel) { isRemove = false; } } }); } } if (!classifiersSearchValue) { setExpanded(expandedBeforeSearch); isRemove = false; } return !isRemove; }); } function getFilteredTree(tree) { const treeKeys = Object.keys(tree); const newTree = {}; for (let i = 0; i < treeKeys.length; i += 1) { newTree[treeKeys[i]] = filterTree(tree, treeKeys[i]); } return newTree; } function getCollapsedTree(tree) { const treeKeys = Object.keys(tree); const collapsedTree = {}; for (let i = 0; i < treeKeys.length; i += 1) { collapsedTree[treeKeys[i]] = collapseTree(tree, treeKeys[i]); } return collapsedTree; } function getSearchedTree(tree) { const treeKeys = Object.keys(tree); const searchedTree = {}; for (let i = 0; i < treeKeys.length; i += 1) { searchedTree[treeKeys[i]] = searchTree(tree, treeKeys[i]); } return searchedTree; } classifiersTree = useMemo( () => { let newTree = getFilteredTree(classifiersTree); newTree = getSearchedTree(newTree); if (elementsFilterValue !== 'all' && side === 'left' && viewer) { const idsByFiles = {}; Object.values(newTree) .forEach((elements) => { elements.forEach((element) => { const fileIds = Object.keys(element.data.idsByFilesId); const elementsIds = Object.values(element.data.idsByFilesId); fileIds.forEach((fileId, i) => { const modelUrn = selectedModel.urns .find(({ id: modelId }) => fileId === modelId).urn; if (idsByFiles[modelUrn]) { idsByFiles[modelUrn] = [ ...idsByFiles[modelUrn], ...elementsIds[i], ]; } else { idsByFiles[modelUrn] = elementsIds[i]; } }); }); }); isolateElements(idsByFiles, viewer); } else if (viewer) { isolateElements([], viewer); } return newTree; }, [classifiers, disabledElements, elementsFilterValue, classifiersSearchValue], ); useEffect(() => { setChecked(initialChecked); if (classifiedElements && Object.keys(classifiedElements).length === 0 && fileToCompare && fileToCompare.type === 'project') { setExpanded([]); } if (classifiedElements && isAutoClassifying) { const newExpanded = Object.keys(classifiedElements); setExpanded(newExpanded.concat(expanded)); setIsCollapse(false); } if (checked && checked.length !== 0) { setExpanded(expanded.concat(checked.toString())); } }, [disabledElements, classifiedElements, isAutoClassifying]); useEffect(() => { if (classifiedElements && classifiedElements.length !== 0 && fileToCompare && isCollapse === false) { const collapsedTree = getCollapsedTree(classifiersTree); const arrClassifiersTree = Object.values(collapsedTree); const arrClassifiedElements = Object.keys(classifiedElements); const classifiedFolders = []; arrClassifiersTree.forEach((classifier) => { if (classifier.length > 0) { const arrClassifier = Object.values(classifier); arrClassifier.forEach(({ id }) => { const isElementIdClassified = arrClassifiedElements .find((elementId) => elementId === String(id)); if (!isElementIdClassified) classifiedFolders.push(String(id)); }); } }); setExpanded(classifiedFolders.concat(expanded)); setIsCollapse(true); } }, [fileToCompare, classifiedElements, isAutoClassifying, isCollapse]); if (!classifiers) { return <CircularProgress />; } if (classifiers.length === 0) { return 'Нет информации'; } if (classifiersTree.null.length === 0) { if (classifiersSearchValue) { return ( <Alert severity="info" sx={{ margin: 0.5 }}> {`Не найдены классификаторы по поисковому запросу: "${classifiersSearchValue}"`} </Alert> ); } if (elementsFilterValue === 'related') { return ( <Alert severity="info" sx={{ margin: 0.5 }}> {type === 'file' || (type === 'model' && side === 'right') ? 'Нет сопоставленных классификаторов' : ''} {(type === 'model' && side === 'left') && 'Нет сопоставленных элементов'} </Alert> ); } if (elementsFilterValue === 'unrelated') { return ( <Alert severity="info" sx={{ margin: 0.5 }}> {type === 'file' && 'Все классификаторы сопоставлены'} {type === 'model' && 'Все элементы сопоставлены'} </Alert> ); } } const selectActiveSide = () => { if (activeSide !== side && type === 'model') { dispatch(setActiveClassifyingSide(side)); } }; const onNodeSelect = (e, ids) => { if (!isEventFromIcon(e)) { onSelect(ids); } selectActiveSide(); }; return ( <TreeView selected={selected} onNodeSelect={onNodeSelect} onClick={selectActiveSide} className={classes.root} defaultCollapseIcon={<ExpandMore />} defaultExpandIcon={<ChevronRight />} multiSelect disableSelection={disableSelection} expanded={expanded} onNodeToggle={(e, ids) => { if (isEventFromIcon(e)) { setExpanded(ids); } e.stopPropagation(); }} > <TreeItems classifiersSearchValue={classifiersSearchValue} tree={classifiersTree} idsToColors={idsToColors} disabledElements={disabledElements || []} classifiedElements={classifiedElements || {}} disableSelection={disableSelection} onDeleteClick={onDeleteClick} type={type} isShowClassifierCode={isShowClassifierCode} isShowVolume={isShowVolume} elementsFilterValue={elementsFilterValue} side={side} checked={checked} setChecked={(id, isChecked) => { let newSelected; if (multiSelect) { newSelected = isChecked ? [...checked, id] : checked.filter((item) => item !== id); } else { newSelected = isChecked ? id : null; } setChecked(newSelected); selectActiveSide(); }} setCheckedAll={(ids, isChecked) => { const newSelected = isChecked ? [...checked, ...ids] : checked.filter((item) => ids.indexOf(item) === -1); setChecked(newSelected); const uniqueNewSelected = newSelected .filter((element, index) => newSelected.indexOf(element) === index); setChecked(uniqueNewSelected); selectActiveSide(); }} /> </TreeView> ); }