From 9bc8dc0de20ac35b09bff13ac6c9d03e34d8b1f0 Mon Sep 17 00:00:00 2001 From: Alexander Lesnenko <alxnddr@users.noreply.github.com> Date: Fri, 25 Feb 2022 13:42:37 -0500 Subject: [PATCH] Convert Tree component to Typescript (#20692) * Convert Tree component to Typescript * fix types * review --- .../components/tree/{Tree.jsx => Tree.tsx} | 24 +++----- .../components/tree/Tree.unit.spec.tsx} | 0 ...reeNode.styled.jsx => TreeNode.styled.tsx} | 26 +++++---- .../tree/{TreeNode.jsx => TreeNode.tsx} | 56 ++++++++----------- .../{TreeNodeList.jsx => TreeNodeList.tsx} | 33 +++++------ .../components/tree/{index.js => index.ts} | 0 .../src/metabase/components/tree/types.ts | 10 ++++ .../components/tree/{utils.jsx => utils.tsx} | 9 ++- 8 files changed, 78 insertions(+), 80 deletions(-) rename frontend/src/metabase/components/tree/{Tree.jsx => Tree.tsx} (67%) rename frontend/{test/metabase/components/Tree.unit.spec.js => src/metabase/components/tree/Tree.unit.spec.tsx} (100%) rename frontend/src/metabase/components/tree/{TreeNode.styled.jsx => TreeNode.styled.tsx} (80%) rename frontend/src/metabase/components/tree/{TreeNode.jsx => TreeNode.tsx} (54%) rename frontend/src/metabase/components/tree/{TreeNodeList.jsx => TreeNodeList.tsx} (68%) rename frontend/src/metabase/components/tree/{index.js => index.ts} (100%) create mode 100644 frontend/src/metabase/components/tree/types.ts rename frontend/src/metabase/components/tree/{utils.jsx => utils.tsx} (60%) diff --git a/frontend/src/metabase/components/tree/Tree.jsx b/frontend/src/metabase/components/tree/Tree.tsx similarity index 67% rename from frontend/src/metabase/components/tree/Tree.jsx rename to frontend/src/metabase/components/tree/Tree.tsx index 86932e7cb48..8ef433df23a 100644 --- a/frontend/src/metabase/components/tree/Tree.jsx +++ b/frontend/src/metabase/components/tree/Tree.tsx @@ -1,26 +1,23 @@ import React, { useState, useCallback } from "react"; -import PropTypes from "prop-types"; import { TreeNodeList } from "./TreeNodeList"; -import { TreeNode } from "./TreeNode"; import { getInitialExpandedIds } from "./utils"; +import { ColorScheme, ITreeNodeItem } from "./types"; -const propTypes = { - TreeNodeComponent: PropTypes.object, - data: PropTypes.array.isRequired, - onSelect: PropTypes.func.isRequired, - colorScheme: PropTypes.oneOf(["default", "admin"]), - selectedId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - emptyState: PropTypes.node, -}; +interface TreeProps { + data: ITreeNodeItem[]; + onSelect: (item: ITreeNodeItem) => void; + selectedId?: ITreeNodeItem["id"]; + colorScheme?: ColorScheme; + emptyState?: React.ReactNode; +} export function Tree({ - TreeNodeComponent = TreeNode, data, onSelect, selectedId, colorScheme = "default", emptyState = null, -}) { +}: TreeProps) { const [expandedIds, setExpandedIds] = useState( new Set(selectedId != null ? getInitialExpandedIds(selectedId, data) : []), ); @@ -43,7 +40,6 @@ export function Tree({ return ( <TreeNodeList colorScheme={colorScheme} - TreeNodeComponent={TreeNodeComponent} items={data} onSelect={onSelect} onToggleExpand={handleToggleExpand} @@ -53,5 +49,3 @@ export function Tree({ /> ); } - -Tree.propTypes = propTypes; diff --git a/frontend/test/metabase/components/Tree.unit.spec.js b/frontend/src/metabase/components/tree/Tree.unit.spec.tsx similarity index 100% rename from frontend/test/metabase/components/Tree.unit.spec.js rename to frontend/src/metabase/components/tree/Tree.unit.spec.tsx diff --git a/frontend/src/metabase/components/tree/TreeNode.styled.jsx b/frontend/src/metabase/components/tree/TreeNode.styled.tsx similarity index 80% rename from frontend/src/metabase/components/tree/TreeNode.styled.jsx rename to frontend/src/metabase/components/tree/TreeNode.styled.tsx index 7e2304d2122..12113f080ac 100644 --- a/frontend/src/metabase/components/tree/TreeNode.styled.jsx +++ b/frontend/src/metabase/components/tree/TreeNode.styled.tsx @@ -1,7 +1,8 @@ import styled from "@emotion/styled"; import { css } from "@emotion/react"; import colors, { lighten } from "metabase/lib/colors"; -import Icon from "metabase/components/Icon"; +import Icon, { IconProps } from "metabase/components/Icon"; +import { ColorScheme } from "./types"; const COLOR_SCHEMES = { admin: { @@ -14,7 +15,13 @@ const COLOR_SCHEMES = { }, }; -export const TreeNodeRoot = styled.li` +interface TreeNodeRootProps { + isSelected: boolean; + depth: number; + colorScheme: ColorScheme; +} + +export const TreeNodeRoot = styled.li<TreeNodeRootProps>` display: flex; align-items: center; color: ${props => @@ -45,7 +52,11 @@ export const ExpandToggleButton = styled.button` visibility: ${props => (props.hidden ? "hidden" : "visible")}; `; -export const ExpandToggleIcon = styled(Icon)` +interface ExpandToggleIconProps { + isExpanded: boolean; +} + +export const ExpandToggleIcon = styled(Icon)<ExpandToggleIconProps & IconProps>` transition: transform 200ms; ${props => @@ -72,12 +83,3 @@ export const IconContainer = styled.div` padding: 0.25rem; opacity: 0.5; `; - -export const RightArrowContainer = styled.div` - display: flex; - align-items: center; - color: ${props => - props.isSelected - ? colors["white"] - : COLOR_SCHEMES[props.colorScheme].text()}; -`; diff --git a/frontend/src/metabase/components/tree/TreeNode.jsx b/frontend/src/metabase/components/tree/TreeNode.tsx similarity index 54% rename from frontend/src/metabase/components/tree/TreeNode.jsx rename to frontend/src/metabase/components/tree/TreeNode.tsx index 3b25a01c4b0..e952a7d78ca 100644 --- a/frontend/src/metabase/components/tree/TreeNode.jsx +++ b/frontend/src/metabase/components/tree/TreeNode.tsx @@ -1,8 +1,8 @@ +/* eslint-disable react/prop-types */ import React from "react"; -import PropTypes from "prop-types"; import _ from "underscore"; -import Icon, { iconPropTypes } from "metabase/components/Icon"; +import Icon from "metabase/components/Icon"; import { TreeNodeRoot, @@ -10,31 +10,23 @@ import { ExpandToggleIcon, NameContainer, IconContainer, - RightArrowContainer, } from "./TreeNode.styled"; +import { ITreeNodeItem } from "./types"; -const propTypes = { - isExpanded: PropTypes.bool.isRequired, - isSelected: PropTypes.bool.isRequired, - hasChildren: PropTypes.bool.isRequired, - onToggleExpand: PropTypes.func, - onSelect: PropTypes.func.isRequired, - depth: PropTypes.number.isRequired, - item: PropTypes.shape({ - name: PropTypes.string.isRequired, - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape(iconPropTypes), - ]), - hasRightArrow: PropTypes.string, - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - }).isRequired, - colorScheme: PropTypes.oneOf(["default", "admin"]), -}; +export interface TreeNodeProps { + item: ITreeNodeItem; + depth: number; + hasChildren: boolean; + isExpanded: boolean; + isSelected: boolean; + colorScheme: "default" | "admin"; + onSelect: (item: ITreeNodeItem) => void; + onToggleExpand: (id: ITreeNodeItem["id"]) => void; +} // eslint-disable-next-line react/display-name export const TreeNode = React.memo( - React.forwardRef(function TreeNode( + React.forwardRef<HTMLLIElement, TreeNodeProps>(function TreeNode( { isExpanded, isSelected, @@ -47,7 +39,7 @@ export const TreeNode = React.memo( }, ref, ) { - const { name, icon, hasRightArrow, id } = item; + const { name, icon, id } = item; const iconProps = _.isObject(icon) ? icon : { name: icon }; @@ -56,7 +48,7 @@ export const TreeNode = React.memo( onToggleExpand(id); }; - const handleKeyDown = ({ key }) => { + const handleKeyDown: React.KeyboardEventHandler = ({ key }) => { switch (key) { case "Enter": onSelect(item); @@ -82,24 +74,20 @@ export const TreeNode = React.memo( onKeyDown={handleKeyDown} > <ExpandToggleButton hidden={!hasChildren}> - <ExpandToggleIcon isExpanded={isExpanded} /> + <ExpandToggleIcon + isExpanded={isExpanded} + name="chevronright" + size={12} + /> </ExpandToggleButton> {icon && ( - <IconContainer colorScheme={colorScheme}> + <IconContainer> <Icon {...iconProps} /> </IconContainer> )} <NameContainer>{name}</NameContainer> - - {hasRightArrow && ( - <RightArrowContainer isSelected={isSelected}> - <Icon name="chevronright" size={14} /> - </RightArrowContainer> - )} </TreeNodeRoot> ); }), ); - -TreeNode.propTypes = propTypes; diff --git a/frontend/src/metabase/components/tree/TreeNodeList.jsx b/frontend/src/metabase/components/tree/TreeNodeList.tsx similarity index 68% rename from frontend/src/metabase/components/tree/TreeNodeList.jsx rename to frontend/src/metabase/components/tree/TreeNodeList.tsx index fad2d3ccacc..2a0816a7c0e 100644 --- a/frontend/src/metabase/components/tree/TreeNodeList.jsx +++ b/frontend/src/metabase/components/tree/TreeNodeList.tsx @@ -1,20 +1,19 @@ import React from "react"; -import PropTypes from "prop-types"; import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount"; +import { ColorScheme, ITreeNodeItem } from "./types"; +import { TreeNode } from "./TreeNode"; -const propTypes = { - TreeNodeComponent: PropTypes.object.isRequired, - items: PropTypes.array.isRequired, - onToggleExpand: PropTypes.func, - onSelect: PropTypes.func.isRequired, - expandedIds: PropTypes.instanceOf(Set), - selectedId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - depth: PropTypes.number.isRequired, - colorScheme: PropTypes.oneOf(["default", "admin"]), -}; +interface TreeNodeListProps { + items: ITreeNodeItem[]; + onToggleExpand: (id: ITreeNodeItem["id"]) => void; + onSelect: (item: ITreeNodeItem) => void; + expandedIds: Set<ITreeNodeItem["id"]>; + selectedId?: ITreeNodeItem["id"]; + depth: number; + colorScheme: ColorScheme; +} export function TreeNodeList({ - TreeNodeComponent, items, onToggleExpand, onSelect, @@ -22,7 +21,7 @@ export function TreeNodeList({ selectedId, depth, colorScheme, -}) { +}: TreeNodeListProps) { const selectedRef = useScrollOnMount(); return ( @@ -35,7 +34,7 @@ export function TreeNodeList({ return ( <React.Fragment key={item.id}> - <TreeNodeComponent + <TreeNode ref={isSelected ? selectedRef : null} colorScheme={colorScheme} item={item} @@ -49,8 +48,8 @@ export function TreeNodeList({ {isExpanded && ( <TreeNodeList colorScheme={colorScheme} - TreeNodeComponent={TreeNodeComponent} - items={item.children} + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + items={item.children!} onToggleExpand={onToggleExpand} onSelect={onSelect} expandedIds={expandedIds} @@ -64,5 +63,3 @@ export function TreeNodeList({ </ul> ); } - -TreeNodeList.propTypes = propTypes; diff --git a/frontend/src/metabase/components/tree/index.js b/frontend/src/metabase/components/tree/index.ts similarity index 100% rename from frontend/src/metabase/components/tree/index.js rename to frontend/src/metabase/components/tree/index.ts diff --git a/frontend/src/metabase/components/tree/types.ts b/frontend/src/metabase/components/tree/types.ts new file mode 100644 index 00000000000..5d338e0a606 --- /dev/null +++ b/frontend/src/metabase/components/tree/types.ts @@ -0,0 +1,10 @@ +import { IconProps } from "../Icon"; + +export interface ITreeNodeItem { + id: string | number; + name: string; + icon: string | IconProps; + children?: ITreeNodeItem[]; +} + +export type ColorScheme = "admin" | "default"; diff --git a/frontend/src/metabase/components/tree/utils.jsx b/frontend/src/metabase/components/tree/utils.tsx similarity index 60% rename from frontend/src/metabase/components/tree/utils.jsx rename to frontend/src/metabase/components/tree/utils.tsx index dbd4529959d..6dddeb54e29 100644 --- a/frontend/src/metabase/components/tree/utils.jsx +++ b/frontend/src/metabase/components/tree/utils.tsx @@ -1,4 +1,9 @@ -export const getInitialExpandedIds = (selectedId, nodes) => +import { ITreeNodeItem } from "./types"; + +export const getInitialExpandedIds = ( + selectedId: ITreeNodeItem["id"], + nodes: ITreeNodeItem[], +): ITreeNodeItem["id"][] => nodes .map(node => { if (node.id === selectedId) { @@ -9,5 +14,7 @@ export const getInitialExpandedIds = (selectedId, nodes) => const path = getInitialExpandedIds(selectedId, node.children); return path.length > 0 ? [node.id, ...path] : []; } + + return []; }) .flat(); -- GitLab