refactor(sidebar): Optimize FlowSidebarComponent with improved search and performance (#5411)

*  (flowSidebarComponent/index.tsx): reorganize imports and remove unnecessary comments for better code readability and maintainability
♻️ (flowSidebarComponent/index.tsx): refactor filtering logic to improve performance and readability
🔧 (flowSidebarComponent/index.tsx): update variable names and state management for better clarity and consistency
💡 (flowSidebarComponent/index.tsx): add comments and organize code structure for easier understanding and future development

📝 (flowSidebarComponent/index.tsx): refactor useEffect hooks to improve code readability and maintainability
📝 (flowSidebarComponent/index.tsx): refactor drag handling function to use useCallback for better performance
📝 (flowSidebarComponent/index.tsx): refactor bundle and category checks to use useMemo for better performance

* ♻️ (flowSidebarComponent/index.tsx): Remove unnecessary comments and optimize code readability by removing redundant comments and improving code structure.

* package-lock
This commit is contained in:
Cristhian Zanforlin Lousa 2024-12-23 17:16:33 -03:00 committed by GitHub
commit b4673d3656
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1881 additions and 2736 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,3 @@
import Fuse from "fuse.js";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook"; // Import useHotkeys
import {
Sidebar,
SidebarContent,
@ -17,7 +13,10 @@ import {
SIDEBAR_BUNDLES,
SIDEBAR_CATEGORIES,
} from "@/utils/styleUtils";
import Fuse from "fuse.js";
import { cloneDeep } from "lodash";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import { useTypesStore } from "../../../../stores/typesStore";
@ -40,6 +39,11 @@ import { traditionalSearchMetadata } from "./helpers/traditional-search-metadata
const CATEGORIES = SIDEBAR_CATEGORIES;
const BUNDLES = SIDEBAR_BUNDLES;
interface FlowSidebarComponentProps {
showLegacy: boolean;
setShowLegacy: (value: boolean) => void;
}
export function FlowSidebarComponent() {
const { data, templates } = useTypesStore(
useCallback(
@ -64,34 +68,158 @@ export function FlowSidebarComponent() {
);
const hasStore = useStoreStore((state) => state.hasStore);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { setOpen } = useSidebar();
const addComponent = useAddComponent();
// State
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
const [fuse, setFuse] = useState<Fuse<any> | null>(null);
const [openCategories, setOpenCategories] = useState<string[]>([]);
const [showConfig, setShowConfig] = useState(false);
const [showBeta, setShowBeta] = useState(true);
const [showLegacy, setShowLegacy] = useState(false);
const [isInputFocused, setIsInputFocused] = useState(false);
const searchInputRef = useRef<HTMLInputElement | null>(null);
// Memoized values
const chatInputAdded = useMemo(() => checkChatInput(nodes), [nodes]);
const customComponent = useMemo(() => {
return data?.["custom_component"]?.["CustomComponent"] ?? null;
}, [data]);
const getFilteredData = useCallback(
(searchTerm: string, sourceData: any, fuseInstance: Fuse<any> | null) => {
if (!searchTerm) return sourceData;
const searchResults = useMemo(() => {
if (!search || !fuse) return null;
let filteredData = cloneDeep(sourceData);
// ... rest of your filtering logic
return filteredData;
const searchTerm = normalizeString(search);
const fuseResults = fuse.search(search).map((result) => ({
...result,
item: { ...result.item, score: result.score },
}));
return {
fuseResults,
fuseCategories: fuseResults.map((result) => result.item.category),
combinedResults: combinedResultsFn(fuseResults, data),
traditionalResults: traditionalSearchMetadata(data, searchTerm),
};
}, [search, fuse, data]);
const searchFilteredData = useMemo(() => {
if (!search || !searchResults) return cloneDeep(data);
return filteredDataFn(
data,
searchResults.combinedResults,
searchResults.traditionalResults,
);
}, [data, search, searchResults]);
const sortedCategories = useMemo(() => {
if (!searchResults || !searchFilteredData) return [];
return Object.keys(searchFilteredData)
.filter(
(category) =>
Object.keys(searchFilteredData[category]).length > 0 &&
(CATEGORIES.find((c) => c.name === category) ||
BUNDLES.find((b) => b.name === category)),
)
.toSorted((a, b) =>
searchResults.fuseCategories.indexOf(b) <
searchResults.fuseCategories.indexOf(a)
? 1
: -1,
);
}, [searchResults, searchFilteredData, CATEGORIES, BUNDLES]);
const finalFilteredData = useMemo(() => {
let filteredData = searchFilteredData;
if (getFilterEdge?.length > 0) {
filteredData = applyEdgeFilter(filteredData, getFilterEdge);
}
if (!showBeta) {
filteredData = applyBetaFilter(filteredData);
}
if (!showLegacy) {
filteredData = applyLegacyFilter(filteredData);
}
return filteredData;
}, [searchFilteredData, getFilterEdge, showBeta, showLegacy]);
const hasResults = useMemo(() => {
return Object.entries(dataFilter).some(
([category, items]) =>
Object.keys(items).length > 0 &&
(CATEGORIES.find((c) => c.name === category) ||
BUNDLES.find((b) => b.name === category)),
);
}, [dataFilter]);
const handleKeyDownInput = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>, name: string) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setOpenCategories((prev) =>
prev.includes(name)
? prev.filter((cat) => cat !== name)
: [...prev, name],
);
}
},
[],
);
// Effect optimizations
const handleClearSearch = useCallback(() => {
setSearch("");
setFilterData(data);
setOpenCategories([]);
}, [data]);
const handleInputFocus = useCallback(() => {
setIsInputFocused(true);
}, []);
const handleInputBlur = useCallback(() => {
setIsInputFocused(false);
}, []);
const handleSearchInput = useCallback((value: string) => {
setSearch(value);
}, []);
const handleInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
handleSearchInput(event.target.value);
},
[handleSearchInput],
);
useEffect(() => {
if (filterType) {
setOpen(true);
}
}, [filterType]);
}, [filterType, setOpen]);
useEffect(() => {
const fuseOptions = {
setFilterData(finalFilteredData);
if (search !== "" || filterType || getFilterEdge.length > 0) {
const newOpenCategories = Object.keys(finalFilteredData).filter(
(cat) => Object.keys(finalFilteredData[cat]).length > 0,
);
setOpenCategories(newOpenCategories);
}
}, [finalFilteredData, search, filterType, getFilterEdge]);
useEffect(() => {
const options = {
keys: ["display_name", "description", "type", "category"],
threshold: 0.2,
includeScore: true,
@ -105,40 +233,20 @@ export function FlowSidebarComponent() {
})),
);
setFuse(new Fuse(fuseData, fuseOptions));
setFuse(new Fuse(fuseData, options));
}, [data]);
const handleKeyDownInput = (
e: React.KeyboardEvent<HTMLDivElement>,
name: string,
) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setOpenCategories((prev) =>
prev.includes(name)
? prev.filter((cat) => cat !== name)
: [...prev, name],
);
useEffect(() => {
if (getFilterEdge.length !== 0) {
setSearch("");
}
};
}, [getFilterEdge, data]);
const [isInputFocused, setIsInputFocused] = useState(false);
const searchInputRef = useRef<HTMLInputElement | null>(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
const addComponent = useAddComponent();
const [fuse, setFuse] = useState<Fuse<any> | null>(null);
const [openCategories, setOpenCategories] = useState<string[]>([]);
const [showConfig, setShowConfig] = useState(false);
const [showBeta, setShowBeta] = useState(true);
const [showLegacy, setShowLegacy] = useState(false);
const { setOpen } = useSidebar();
useEffect(() => {
if (search === "" && getFilterEdge.length === 0) {
setOpenCategories([]);
}
}, [search, getFilterEdge]);
const searchComponentsSidebar = useShortcutsStore(
(state) => state.searchComponentsSidebar,
@ -169,195 +277,42 @@ export function FlowSidebarComponent() {
},
);
useEffect(() => {
if (filterType) {
setOpen(true);
}
}, [filterType]);
useEffect(() => {
filterComponents();
}, [data, search, filterType, getFilterEdge, showBeta, showLegacy]);
useEffect(() => {
// show components with error on load
let errors: string[] = [];
Object.keys(templates).forEach((component) => {
if (templates[component].error) {
errors.push(component);
}
});
if (errors.length > 0)
setErrorData({ title: " Components with errors: ", list: errors });
}, []);
useEffect(() => {
if (getFilterEdge.length !== 0) {
setSearch("");
}
}, [getFilterEdge, data]);
useEffect(() => {
const options = {
keys: ["display_name", "description", "type", "category"],
threshold: 0.2,
includeScore: true,
};
const fuseData = Object.entries(data).flatMap(([category, items]) =>
Object.entries(items).map(([key, value]) => ({
...value,
category,
key,
})),
);
setFuse(new Fuse(fuseData, options));
handleSearchInput(search);
}, [data]);
useEffect(() => {
if (search === "" && getFilterEdge.length === 0) {
setOpenCategories([]);
}
}, [search, getFilterEdge]);
const hasResults = useMemo(() => {
return Object.entries(dataFilter).some(
([category, items]) =>
Object.keys(items).length > 0 &&
(CATEGORIES.find((c) => c.name === category) ||
BUNDLES.find((b) => b.name === category)),
);
}, [dataFilter]);
const [sortedCategories, setSortedCategories] = useState<string[]>([]);
const filterComponents = () => {
let filteredData = cloneDeep(data);
if (search) {
const searchTerm = normalizeString(search);
let combinedResults = {};
if (fuse) {
const fuseResults = fuse.search(search).map((result) => ({
...result,
item: { ...result.item, score: result.score },
}));
const fuseCategories = fuseResults.map(
(result) => result.item.category,
);
setSortedCategories(fuseCategories);
combinedResults = combinedResultsFn(fuseResults, data);
const traditionalResults = traditionalSearchMetadata(data, searchTerm);
filteredData = filteredDataFn(
data,
combinedResults,
traditionalResults,
);
setSortedCategories(
Object.keys(filteredData)
.filter(
(category) =>
Object.keys(filteredData[category]).length > 0 &&
(CATEGORIES.find((c) => c.name === category) ||
BUNDLES.find((b) => b.name === category)),
)
.toSorted((a, b) =>
fuseCategories.indexOf(b) < fuseCategories.indexOf(a) ? 1 : -1,
),
);
}
}
// Apply edge filter
if (getFilterEdge?.length > 0) {
filteredData = applyEdgeFilter(filteredData, getFilterEdge);
}
// Apply beta filter
if (!showBeta) {
filteredData = applyBetaFilter(filteredData);
}
// Apply legacy filter
if (!showLegacy) {
filteredData = applyLegacyFilter(filteredData);
}
setFilterData(filteredData);
if (search !== "" || filterType || getFilterEdge.length > 0) {
setOpenCategories(
Object.keys(filteredData).filter(
(cat) => Object.keys(filteredData[cat]).length > 0,
),
);
}
};
const handleSearchInput = useCallback(
(value: string) => {
setSearch(value);
const filtered = getFilteredData(value, data, fuse);
setFilterData(filtered);
},
[data, fuse],
);
function onDragStart(
event: React.DragEvent<any>,
data: { type: string; node?: APIClassType },
): void {
//start drag event
var crt = event.currentTarget.cloneNode(true);
crt.style.position = "absolute";
crt.style.width = "215px";
crt.style.top = "-500px";
crt.style.right = "-500px";
crt.classList.add("cursor-grabbing");
document.body.appendChild(crt);
event.dataTransfer.setDragImage(crt, 0, 0);
event.dataTransfer.setData("genericNode", JSON.stringify(data));
}
const hasBundleItems = BUNDLES.some(
(item) =>
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
);
const hasCategoryItems = CATEGORIES.some(
(item) =>
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
);
function handleClearSearch() {
setSearch("");
setFilterData(data);
setOpenCategories([]);
}
const handleInputFocus = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setIsInputFocused(true);
const onDragStart = useCallback(
(
event: React.DragEvent<any>,
data: { type: string; node?: APIClassType },
) => {
var crt = event.currentTarget.cloneNode(true);
crt.style.position = "absolute";
crt.style.width = "215px";
crt.style.top = "-500px";
crt.style.right = "-500px";
crt.classList.add("cursor-grabbing");
document.body.appendChild(crt);
event.dataTransfer.setDragImage(crt, 0, 0);
event.dataTransfer.setData("genericNode", JSON.stringify(data));
},
[],
);
const handleInputBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setIsInputFocused(false);
},
[],
const hasBundleItems = useMemo(
() =>
BUNDLES.some(
(item) =>
dataFilter[item.name] &&
Object.keys(dataFilter[item.name]).length > 0,
),
[dataFilter],
);
const handleInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
handleSearchInput(event.target.value);
},
[],
const hasCategoryItems = useMemo(
() =>
CATEGORIES.some(
(item) =>
dataFilter[item.name] &&
Object.keys(dataFilter[item.name]).length > 0,
),
[dataFilter],
);
return (
@ -434,4 +389,15 @@ export function FlowSidebarComponent() {
FlowSidebarComponent.displayName = "FlowSidebarComponent";
export default memo(FlowSidebarComponent);
export default memo(
FlowSidebarComponent,
(
prevProps: FlowSidebarComponentProps,
nextProps: FlowSidebarComponentProps,
) => {
return (
prevProps.showLegacy === nextProps.showLegacy &&
prevProps.setShowLegacy === nextProps.setShowLegacy
);
},
);