Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, {
useCallback,
useEffect,
useState,
} from "react";

import {Tree} from "antd";
import type {TreeDataNode} from "antd/es";
import type {TreeProps} from "antd/es/tree";

import styles from "./index.module.css";
import {getListHeight} from "./utils";


const {DirectoryTree} = Tree;


interface DirectoryTreePopupProps {
checkedKeys: string[];
expandedKeys: string[];
treeData: TreeDataNode[];
onCheck: (keys: string[]) => void;
onExpand: (keys: string[]) => void;
onLoadData: (path: string) => Promise<void>;
}

/**
* Renders a popup component containing a DirectoryTree for path selection.
*
* @param props
* @param props.checkedKeys
* @param props.expandedKeys
* @param props.treeData
* @param props.onCheck
* @param props.onExpand
* @param props.onLoadData
* @return
*/
const DirectoryTreePopup = ({
checkedKeys,
expandedKeys,
treeData,
onCheck,
onExpand,
onLoadData,
}: DirectoryTreePopupProps) => {
const [height, setHeight] = useState<number>(getListHeight);

const handleCheck: TreeProps["onCheck"] = useCallback((
checked: React.Key[] | {checked: React.Key[]; halfChecked: React.Key[]}
) => {
const keys = Array.isArray(checked) ?
checked :
checked.checked;

onCheck(keys as string[]);
}, [onCheck]);

const handleExpand = useCallback((keys: React.Key[]) => {
onExpand(keys as string[]);
}, [onExpand]);

const handleLoadData = useCallback(async (node: TreeDataNode) => {
await onLoadData(node.key as string);
}, [onLoadData]);

useEffect(() => {
const handleResize = () => {
setHeight(getListHeight());
};

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize);
};
}, []);

return (
<div
className={styles["directoryTreePopup"]}
style={{height: height, overflow: "auto"}}
>
<DirectoryTree
checkable={true}
checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
loadData={handleLoadData}
treeData={treeData}
onCheck={handleCheck}
onExpand={handleExpand}/>
</div>
);
};
Comment on lines +79 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider memoizing the inline style object.

The component is well-structured with the DirectoryTree properly configured. However, the inline style object is recreated on every render.

♻️ Optional optimization to memoize the style object
+import React, {
+    useCallback,
+    useEffect,
+    useMemo,
+    useState,
+} from "react";
 const DirectoryTreePopup = ({
     checkedKeys,
     expandedKeys,
     treeData,
     onCheck,
     onExpand,
     onLoadData,
 }: DirectoryTreePopupProps) => {
     const [height, setHeight] = useState<number>(getListHeight);
+
+    const containerStyle = useMemo(() => ({
+        height,
+        overflow: "auto" as const,
+    }), [height]);

     // ... rest of the code

     return (
         <div
             className={styles["directoryTreePopup"]}
-            style={{height: height, overflow: "auto"}}
+            style={containerStyle}
         >

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@components/webui/client/src/pages/IngestPage/Compress/PathsSelectFormItem/DirectoryTreePopup.tsx
around lines 79 - 94, The inline style object passed to the wrapper div in
DirectoryTreePopup is recreated on each render; memoize it using React.useMemo
inside the DirectoryTreePopup component (e.g., compute a containerStyle that
returns { height, overflow: 'auto' } and depends on height) and replace the
inline style with that memoized variable so the object identity is stable across
renders.



export default DirectoryTreePopup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
useCallback,
useMemo,
} from "react";

import {Select} from "antd";
import type {TreeDataNode} from "antd/es";

import DirectoryTreePopup from "./DirectoryTreePopup";
import {TreeNode} from "./typings";
import {
filterToParents,
flatToHierarchy,
removeWithDescendants,
} from "./utils";


interface DirectoryTreeSelectProps {
checkedKeys: string[];
expandedKeys: string[];
treeData: TreeNode[];
onCheck: (keys: string[]) => void;
onExpand: (keys: string[]) => void;
onLoadData: (path: string) => Promise<void>;
}

/**
* Renders a select component that uses DirectoryTree for the dropdown. Provides folder/file
* icons and intuitive directory navigation.
*
* @param props
* @param props.checkedKeys
* @param props.expandedKeys
* @param props.treeData
* @param props.onCheck
* @param props.onExpand
* @param props.onLoadData
* @return
*/
Comment on lines +27 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove the empty @return tag.

The JSDoc includes an @return tag without a description. Since return type annotations are not preferred for React components in this codebase, and the tag provides no value, it should be removed.

📝 Proposed fix
 /**
  * Renders a select component that uses DirectoryTree for the dropdown. Provides folder/file
  * icons and intuitive directory navigation.
  *
  * @param props
  * @param props.checkedKeys
  * @param props.expandedKeys
  * @param props.treeData
  * @param props.onCheck
  * @param props.onExpand
  * @param props.onLoadData
- * @return
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Renders a select component that uses DirectoryTree for the dropdown. Provides folder/file
* icons and intuitive directory navigation.
*
* @param props
* @param props.checkedKeys
* @param props.expandedKeys
* @param props.treeData
* @param props.onCheck
* @param props.onExpand
* @param props.onLoadData
* @return
*/
/**
* Renders a select component that uses DirectoryTree for the dropdown. Provides folder/file
* icons and intuitive directory navigation.
*
* @param props
* @param props.checkedKeys
* @param props.expandedKeys
* @param props.treeData
* @param props.onCheck
* @param props.onExpand
* @param props.onLoadData
*/
🤖 Prompt for AI Agents
In
@components/webui/client/src/pages/IngestPage/Compress/PathsSelectFormItem/DirectoryTreeSelect.tsx
around lines 27 - 39, The JSDoc for the DirectoryTreeSelect component includes
an empty @return tag; remove the stray "@return" line from the comment block
above the DirectoryTreeSelect component so the JSDoc lists only relevant param
tags (e.g., props.checkedKeys, props.expandedKeys, props.treeData,
props.onCheck, props.onExpand, props.onLoadData) and no return annotation.

const DirectoryTreeSelect = ({
checkedKeys,
expandedKeys,
treeData,
onCheck,
onExpand,
onLoadData,
}: DirectoryTreeSelectProps) => {
const hierarchicalTreeData: TreeDataNode[] = useMemo(
() => flatToHierarchy(treeData),
[treeData]
);
const selectOptions = useMemo(
() => treeData.map((node) => ({label: node.value, value: node.value})),
[treeData]
);
const displayValue = useMemo(
() => filterToParents(treeData, checkedKeys),
[
checkedKeys,
treeData,
]
);

const handleClear = useCallback(() => {
onCheck([]);
}, [onCheck]);

const handleDeselect = useCallback((value: string) => {
onCheck(removeWithDescendants(treeData, checkedKeys, value));
}, [
checkedKeys,
onCheck,
treeData,
]);

const renderDropdown = useCallback(() => (
<DirectoryTreePopup
checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
treeData={hierarchicalTreeData}
onCheck={onCheck}
onExpand={onExpand}
onLoadData={onLoadData}/>
), [
checkedKeys,
expandedKeys,
hierarchicalTreeData,
onCheck,
onExpand,
onLoadData,
]);

return (
<Select
allowClear={true}
mode={"multiple"}
options={selectOptions}
placeholder={"Select paths to compress"}
popupRender={renderDropdown}
value={displayValue}
onClear={handleClear}
onDeselect={handleDeselect}/>
);
};


export default DirectoryTreeSelect;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.directoryTreePopup {
padding: 4px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,34 @@ import {
} from "react";

import {FileEntry} from "@webui/common/schemas/os";
import {
Form,
message,
TreeSelect,
TreeSelectProps,
} from "antd";
import {Form} from "antd";

import {listFiles} from "../../../../api/os";
import SwitcherIcon from "./SwitcherIcon";
import DirectoryTreeSelect from "./DirectoryTreeSelect";
import {
ROOT_NODE,
ROOT_PATH,
TreeNode,
} from "./typings";
import {
addServerPrefix,
getListHeight,
handleLoadError,
toTreeNode,
} from "./utils";


type LoadDataNode = Parameters<NonNullable<TreeSelectProps["loadData"]>>[0];

type TreeExpandKeys = Parameters<NonNullable<TreeSelectProps["onTreeExpand"]>>[0];

/**
* Form item with TreeSelect for selecting file paths for compression.
* Supports lazy loading and search with auto-expand.
* Uses DirectoryTree for intuitive folder/file navigation.
*
* @return
* @see https://ant.design/components/tree#tree-demo-directory
*/
const PathsSelectFormItem = () => {
const form = Form.useFormInstance();
const [treeData, setTreeData] = useState<TreeNode[]>([ROOT_NODE]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [listHeight, setListHeight] = useState<number>(getListHeight);

useEffect(() => {
const handleResize = () => {
setListHeight(getListHeight());
};

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);

// Use a ref, instead of a state passed to AntD's `treeLoadedKeys`, to dedupe load requests.
const loadedPathsRef = useRef(new Set<string>());
Expand Down Expand Up @@ -85,61 +66,41 @@ const PathsSelectFormItem = () => {
}
}, [addNodes]);

const handleCheck = useCallback((keys: string[]) => {
setCheckedKeys(keys);
form.setFieldValue("paths", keys);
}, [form]);

const handleExpand = useCallback((keys: string[]) => {
setExpandedKeys(keys);
}, []);

const handleLoadData = useCallback(async (path: string) => {
await loadPath(path).catch(handleLoadError);
}, [loadPath]);

// On initialization, load root directory contents.
useEffect(() => {
loadPath(ROOT_PATH)
.then(() => {
setExpandedKeys([ROOT_PATH]);
})
.catch((e: unknown) => {
console.error("Failed to load root directory:", e);
message.error(e instanceof Error ?
e.message :
"Failed to load root directory");
});
.catch(handleLoadError);
}, [loadPath]);

const handleLoadData = useCallback(async ({value}: LoadDataNode) => {
if ("string" !== typeof value) {
return;
}
try {
await loadPath(value);
} catch (e) {
console.error("Failed to load directory:", e);
message.error(e instanceof Error ?
e.message :
"Unknown error while loading paths");
}
}, [loadPath]);

const handleTreeExpand = useCallback((keys: TreeExpandKeys) => {
setExpandedKeys(keys as string[]);
}, []);

return (
<Form.Item
label={"Paths"}
name={"paths"}
rules={[{required: true, message: "Please select at least one path"}]}
>
<TreeSelect
allowClear={true}
listHeight={listHeight}
loadData={handleLoadData}
multiple={true}
placeholder={"Select paths to compress"}
showCheckedStrategy={TreeSelect.SHOW_PARENT}
showSearch={false}
switcherIcon={SwitcherIcon}

treeCheckable={true}
<DirectoryTreeSelect
checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
treeData={treeData}
treeDataSimpleMode={true}
treeExpandedKeys={expandedKeys}
treeLine={true}
treeNodeLabelProp={"value"}
onTreeExpand={handleTreeExpand}/>
onCheck={handleCheck}
onExpand={handleExpand}
onLoadData={handleLoadData}/>
</Form.Item>
);
};
Expand Down
Loading
Loading