toggle-corp/react-store

View on GitHub
v2/Input/TreeInput/index.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import React, {
useCallback,
useMemo,
useState,
} from 'react';
import { _cs, isNotDefined, Obj } from '@togglecorp/fujs';
 
import { OptionKey } from '../../types';
 
import Button from '../../Action/Button';
import List from '../../View/List';
import Checkbox from '../Checkbox';
import HintAndError from '../HintAndError';
import Label from '../Label';
import { generateExtendedRelations, ExtendedRelation } from './utils';
 
import styles from './styles.scss';
 
interface TreeNodeProps<T, K extends OptionKey> {
className?: string;
keySelector: (datum: T) => K;
parentKeySelector: (datum: T) => K | undefined;
labelSelector: (datum: T) => string | number;
onChange: (keys: K[]) => void;
value: K[];
nodeKey: K;
nodeLabel: string | number;
 
disabled: boolean;
readOnly: boolean;
defaultCollapseLevel: number;
level: number;
 
relations: Obj<ExtendedRelation<T, K> | undefined>;
}
 
Function `TreeNode` has 139 lines of code (exceeds 100 allowed). Consider refactoring.
Function `TreeNode` has a Cognitive Complexity of 16 (exceeds 10 allowed). Consider refactoring.
function TreeNode<T, K extends OptionKey>(props: TreeNodeProps<T, K>) {
const {
className,
disabled,
readOnly,
nodeKey,
nodeLabel,
 
value,
labelSelector,
parentKeySelector,
keySelector,
level,
defaultCollapseLevel,
onChange,
relations,
} = props;
 
const [collapsed, setCollapsed] = useState(level >= defaultCollapseLevel);
 
const relation = relations[String(nodeKey)];
const allOwnOptions = relation ? relation.children : undefined;
 
const ownOptions = useMemo(
() => allOwnOptions && allOwnOptions.filter(
option => parentKeySelector(option) === nodeKey,
),
[allOwnOptions, parentKeySelector, nodeKey],
);
 
const isLeaf = ownOptions && ownOptions.length <= 0;
 
const someSelected = useMemo(
() => ownOptions && ownOptions.some((option) => {
const key = keySelector(option);
FIXME found
// FIXME: create a mapping to optimize check
const selected = value.includes(key);
return selected;
}),
[value, keySelector, ownOptions],
);
 
FIXME found
// FIXME: create a mapping to optimize check
const checked = value.includes(nodeKey);
 
const handleCollapseOption = useCallback(
() => {
setCollapsed(true);
},
[],
);
const handleToggleCollapseOption = useCallback(
() => {
setCollapsed(v => !v);
},
[],
);
const handleCheckboxChange = useCallback(
(val: boolean) => {
const oldKeys = new Set(value);
if (val) {
// NOTE: Add current node
oldKeys.add(nodeKey);
if (allOwnOptions) {
// NOTE: Add all children nodes
allOwnOptions.forEach((option) => {
oldKeys.add(keySelector(option));
});
}
} else {
// NOTE: Remove current node
oldKeys.delete(nodeKey);
// NOTE: Remove all children nodes
if (allOwnOptions) {
allOwnOptions.forEach((option) => {
oldKeys.delete(keySelector(option));
});
}
}
onChange([...oldKeys]);
},
[onChange, value, nodeKey, keySelector, allOwnOptions],
);
 
const handleTreeNodeChange = useCallback(
(newKeys: K[]) => {
// if all child keys are selected, then select current as well
const allChildSelected = ownOptions && ownOptions.every((item) => {
const itemKey = keySelector(item);
FIXME found
// FIXME: create a mapping to optimize check
const selected = newKeys.includes(itemKey);
return selected;
});
 
if (allChildSelected) {
onChange([...newKeys, nodeKey]);
FIXME found
// FIXME: create a mapping to optimize check
} else if (newKeys.includes(nodeKey)) {
// if not all child selected && current key is there
const filteredKeys = newKeys.filter(key => key !== nodeKey);
onChange(filteredKeys);
} else {
onChange(newKeys);
}
},
[onChange, keySelector, ownOptions, nodeKey],
);
 
return (
<div className={_cs(styles.treeNode, className, collapsed && styles.collapsed)}>
<div className={styles.left}>
<Button
className={styles.expandButton}
disabled={isLeaf}
onClick={handleToggleCollapseOption}
transparent
iconName="arrowDropright"
/>
{!collapsed && !isLeaf && (
<div
className={styles.stem}
role="button"
onClick={handleCollapseOption}
onKeyDown={handleCollapseOption}
tabIndex={-1}
>
<div className={styles.line} />
</div>
)}
</div>
<div className={styles.right}>
<Checkbox
className={styles.checkbox}
labelClassName={styles.label}
checkIconClassName={styles.checkIcon}
value={checked}
label={nodeLabel}
disabled={disabled}
readOnly={readOnly}
onChange={handleCheckboxChange}
indeterminate={someSelected}
/>
{ !isLeaf && (
<TreeNodeList
relations={relations}
className={styles.nodeList}
visibleOptions={ownOptions}
keySelector={keySelector}
disabled={disabled}
readOnly={readOnly}
labelSelector={labelSelector}
parentKeySelector={parentKeySelector}
value={value}
defaultCollapseLevel={defaultCollapseLevel}
level={level + 1}
onChange={handleTreeNodeChange}
/>
)}
</div>
</div>
);
}
 
interface TreeNodeListProps<T, K extends OptionKey> {
className?: string;
keySelector: (datum: T) => K;
parentKeySelector: (datum: T) => K | undefined;
labelSelector: (datum: T) => string | number;
 
onChange: (keys: K[]) => void;
value: K[];
 
visibleOptions: T[];
 
disabled: boolean;
readOnly: boolean;
 
defaultCollapseLevel: number;
level: number;
 
relations: Obj<ExtendedRelation<T, K> | undefined>;
}
function TreeNodeList<T, K extends OptionKey>(props: TreeNodeListProps<T, K>) {
const {
className,
// options,
keySelector,
disabled,
readOnly,
labelSelector,
parentKeySelector,
value,
onChange,
 
// childOptions,
visibleOptions,
 
level,
defaultCollapseLevel,
relations,
} = props;
 
const rendererParams = useCallback(
(key: K, v: T) => ({
disabled,
readOnly,
 
nodeLabel: labelSelector(v),
nodeKey: key,
 
// For children
keySelector,
labelSelector,
parentKeySelector,
value,
defaultCollapseLevel,
level,
onChange,
relations,
}),
[
value, onChange, relations,
readOnly, disabled,
defaultCollapseLevel, level,
keySelector, labelSelector, parentKeySelector,
],
);
 
return (
<div className={_cs(styles.treeNodeList, className)}>
<List
keySelector={keySelector}
data={visibleOptions}
renderer={TreeNode}
rendererParams={rendererParams}
/>
</div>
);
}
TreeNodeList.defaultProps = {
visibleOptions: [],
};
 
export interface TreeProps<T, K extends OptionKey> {
// autoFocus?: boolean;
className?: string;
disabled: boolean;
error?: string;
hint?: string;
keySelector: (datum: T) => K;
parentKeySelector: (datum: T) => K | undefined;
label?: string;
labelClassName?: string;
labelSelector: (datum: T) => string | number;
onChange: (keys: K[]) => void;
options: T[];
readOnly: boolean;
showHintAndError: boolean;
showLabel: boolean;
title?: string;
value: K[];
 
defaultCollapseLevel: number;
 
labelRightComponent?: React.ReactNode;
labelRightComponentClassName?: string;
}
 
function TreeInput<T, K extends OptionKey = string>(props: TreeProps<T, K>) {
const {
className: classNameFromProps,
disabled,
error,
hint,
label,
labelClassName,
labelRightComponent,
labelRightComponentClassName,
showHintAndError,
showLabel,
title,
keySelector,
parentKeySelector,
labelSelector,
onChange,
options,
readOnly,
value,
defaultCollapseLevel,
} = props;
 
const className = _cs(
classNameFromProps,
'tree',
disabled && 'disabled',
error && 'error',
);
 
const visibleOptions = useMemo(
() => options.filter((option) => {
const parentKey = parentKeySelector(option);
return isNotDefined(parentKey);
}),
[options, parentKeySelector],
);
 
const relations = useMemo(
() => generateExtendedRelations(
options,
keySelector,
parentKeySelector,
),
[options, keySelector, parentKeySelector],
);
 
return (
<div
className={className}
title={title}
>
{showLabel && (
<Label
className={labelClassName}
disabled={disabled}
error={!!error}
title={label}
rightComponent={labelRightComponent}
rightComponentClassName={labelRightComponentClassName}
>
{label}
</Label>
)}
<TreeNodeList
className={styles.nodeList}
defaultCollapseLevel={defaultCollapseLevel}
level={0}
 
readOnly={readOnly}
disabled={disabled}
 
keySelector={keySelector}
parentKeySelector={parentKeySelector}
labelSelector={labelSelector}
value={value}
relations={relations}
 
onChange={onChange}
visibleOptions={visibleOptions}
/>
{showHintAndError && (
<HintAndError
error={error}
hint={hint}
/>
)}
</div>
);
}
TreeInput.defaultProps = {
disabled: false,
readOnly: false,
showHintAndError: true,
showLabel: true,
value: [],
options: [],
defaultCollapseLevel: 1,
};
 
export default TreeInput;