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