feat: Enhance sidebar components (#4452)
* 📝 (pageLayout/index.tsx): update beta icon text from "BETA" to "Beta" for consistency and readability * ✨ (custom-parameter.tsx): add support for flex view in custom parameter title to improve UI flexibility and readability * ✨ (NodeInputField/index.tsx): refactor NodeInputField component to pass isFlexView prop to getCustomParameterTitle function for improved customization and flexibility * 📝 (NodeStatus/index.tsx): update text from "BETA" to "Beta" for consistency and readability * ✨ (flowSidebarComponent/index.tsx): Add new components and features to the Flow Sidebar Component to enhance user experience and functionality. Update the structure and logic of the component to improve search functionality, error handling, and component filtering. * ✨ (emptySearchComponent): add a new component NoResultsMessage to display a message when no components are found in the search result. The component includes options to clear the search, filter, and try a different query. * ✨ (featureTogglesComponent/index.tsx): add a new component FeatureToggles to manage feature toggles for Beta and Legacy features in the sidebar of the FlowPage. * 📝 (sidebarDraggableComponent): Update CSS classes for better styling and spacing 💡 (sidebarDraggableComponent): Improve readability by changing font weight of text elements 💡 (sidebarDraggableComponent): Update text content to be more consistent and capitalized * ✨ (index.tsx): Add a new component SidebarFooterButtons to display buttons in the sidebar footer for adding custom components and navigating to the store page. * ✨ (index.tsx): add a new component SidebarItemsList to render a list of items in the sidebar with tooltips and draggable functionality * ✨ (apply-beta-filter.ts): add a new helper function applyBetaFilter to filter out beta items from the given data based on a specific condition * ✨ (apply-edge-filter.ts): introduce a new helper function applyEdgeFilter to filter data based on edge filters for a better user experience. * ✨ (apply-legacy-filter.ts): introduce a new helper function applyLegacyFilter to filter out legacy data from APIDataType object. * ✨ (combined-results.ts): add a new helper function combinedResultsFn to combine Fuse.js search results with API data for each category * ✨ (filtered-data.ts): introduce a new helper function filteredDataFn to merge and return filtered data from different sources for a given category in the FlowPage component. * ✨ (normalize-string.ts): introduce a new helper function normalizeString to convert a string to lowercase, remove underscores, and spaces for better string normalization. * ✨ (ParentDisclosureComponent): Change "BETA" to "Beta" for consistency in text formatting ✨ (search-on-metadata.ts, traditional-search-metadata.ts): Add helper functions for searching in metadata and traditional search metadata to improve search functionality in the flow sidebar component.
This commit is contained in:
parent
e63cace20a
commit
f9ae9f27f3
19 changed files with 598 additions and 539 deletions
|
|
@ -125,7 +125,10 @@ export default function NodeInputField({
|
|||
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||
{
|
||||
<span>
|
||||
{getCustomParameterTitle({ title, nodeId: data.id })}
|
||||
{getCustomParameterTitle({
|
||||
title,
|
||||
isFlexView,
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
</ShadTooltip>
|
||||
|
|
@ -134,7 +137,10 @@ export default function NodeInputField({
|
|||
<span>
|
||||
{
|
||||
<span className="text-sm font-medium">
|
||||
{getCustomParameterTitle({ title, nodeId: data.id })}
|
||||
{getCustomParameterTitle({
|
||||
title,
|
||||
isFlexView,
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ export default function NodeStatus({
|
|||
size="sq"
|
||||
className="pointer-events-none mr-1 flex h-[22px] w-10 justify-center rounded-[8px] bg-accent-pink text-accent-pink-foreground"
|
||||
>
|
||||
<span className="text-[11px]">BETA</span>
|
||||
<span className="text-[11px]">Beta</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default function PageLayout({
|
|||
data-testid="mainpage_title"
|
||||
>
|
||||
{title}
|
||||
{betaIcon && <span className="store-beta-icon">BETA</span>}
|
||||
{betaIcon && <span className="store-beta-icon">Beta</span>}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ParameterRenderComponent } from "@/components/parameterRenderComponent";
|
||||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { cn } from "@/utils/utils";
|
||||
|
||||
export function CustomParameterComponent({
|
||||
handleOnNewValue,
|
||||
|
|
@ -40,18 +41,20 @@ export function CustomParameterComponent({
|
|||
|
||||
export function getCustomParameterTitle({
|
||||
title,
|
||||
nodeId,
|
||||
isFlexView,
|
||||
}: {
|
||||
title: string;
|
||||
nodeId: string;
|
||||
isFlexView: boolean;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
data-testid={`title-${title.toLocaleLowerCase()}`}
|
||||
className="text-[13px]"
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<div className={cn(isFlexView && "max-w-56 truncate")}>
|
||||
<span
|
||||
data-testid={`title-${title.toLocaleLowerCase()}`}
|
||||
className="text-[13px]"
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function ParentDisclosureComponent({
|
|||
<span className="text-sm font-medium">{title}</span>
|
||||
{beta && (
|
||||
<div className="h-fit rounded-full bg-beta-background px-2 py-1 text-xs/3 font-semibold text-beta-foreground-soft">
|
||||
BETA
|
||||
Beta
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
const NoResultsMessage = ({
|
||||
onClearSearch,
|
||||
message = "No components found.",
|
||||
clearSearchText = "Clear your search",
|
||||
additionalText = "or filter and try a different query.",
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center p-3 text-center">
|
||||
<p className="text-sm text-secondary-foreground">
|
||||
{message}{" "}
|
||||
<a
|
||||
className="cursor-pointer underline underline-offset-4"
|
||||
onClick={onClearSearch}
|
||||
>
|
||||
{clearSearchText}
|
||||
</a>{" "}
|
||||
{additionalText}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoResultsMessage;
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import React from "react";
|
||||
|
||||
const FeatureToggles = ({
|
||||
showBeta,
|
||||
setShowBeta,
|
||||
showLegacy,
|
||||
setShowLegacy,
|
||||
}) => {
|
||||
const toggles = [
|
||||
{
|
||||
label: "Beta",
|
||||
checked: showBeta,
|
||||
onChange: setShowBeta,
|
||||
badgeVariant: "pinkStatic" as const,
|
||||
testId: "sidebar-beta-switch",
|
||||
},
|
||||
{
|
||||
label: "Legacy",
|
||||
checked: showLegacy,
|
||||
onChange: setShowLegacy,
|
||||
badgeVariant: "secondaryStatic" as const,
|
||||
testId: "sidebar-legacy-switch",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-7 border-b pb-7 pt-5">
|
||||
{toggles.map((toggle) => (
|
||||
<div key={toggle.label} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="flex gap-2 text-sm font-medium">
|
||||
Show
|
||||
<Badge variant={toggle.badgeVariant} size="xq">
|
||||
{toggle.label}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={toggle.checked}
|
||||
onCheckedChange={toggle.onChange}
|
||||
data-testid={toggle.testId}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureToggles;
|
||||
|
|
@ -121,7 +121,7 @@ export const SidebarDraggableComponent = forwardRef(
|
|||
data-tooltip-id={itemName}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="rounded-md outline-none ring-ring focus-visible:ring-2"
|
||||
className="m-[1px] rounded-md outline-none ring-ring focus-visible:ring-1"
|
||||
>
|
||||
<div
|
||||
data-testid={sectionName + display_name}
|
||||
|
|
@ -150,7 +150,7 @@ export const SidebarDraggableComponent = forwardRef(
|
|||
/>
|
||||
<div className="flex flex-1 items-center overflow-hidden">
|
||||
<ShadTooltip content={display_name} styleClasses="z-50">
|
||||
<span className="truncate text-sm font-semibold">
|
||||
<span className="truncate text-sm font-normal">
|
||||
{display_name}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
|
|
@ -160,7 +160,7 @@ export const SidebarDraggableComponent = forwardRef(
|
|||
size="xq"
|
||||
className="ml-1.5 shrink-0"
|
||||
>
|
||||
BETA
|
||||
Beta
|
||||
</Badge>
|
||||
)}
|
||||
{legacy && (
|
||||
|
|
@ -169,7 +169,7 @@ export const SidebarDraggableComponent = forwardRef(
|
|||
size="xq"
|
||||
className="ml-1.5 shrink-0"
|
||||
>
|
||||
LEGACY
|
||||
Legacy
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import ForwardedIconComponent from "@/components/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SidebarMenuButton } from "@/components/ui/sidebar";
|
||||
import { CustomLink } from "@/customization/components/custom-link";
|
||||
import React from "react";
|
||||
|
||||
const SidebarMenuButtons = ({
|
||||
hasStore = false,
|
||||
customComponent,
|
||||
addComponent,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{hasStore && (
|
||||
<SidebarMenuButton asChild>
|
||||
<CustomLink
|
||||
to="/store"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group/discover"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<ForwardedIconComponent
|
||||
name="Store"
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<span className="flex-1 group-data-[state=open]/collapsible:font-semibold">
|
||||
Discover more components
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="SquareArrowOutUpRight"
|
||||
className="h-4 w-4 opacity-0 transition-all group-hover/discover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
</CustomLink>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
unstyled
|
||||
onClick={() => {
|
||||
if (customComponent) {
|
||||
addComponent(customComponent, "CustomComponent");
|
||||
}
|
||||
}}
|
||||
data-testid="sidebar-custom-component-button"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Plus"
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<span className="group-data-[state=open]/collapsible:font-semibold">
|
||||
New Custom Component
|
||||
</span>
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarMenuButtons;
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import ShadTooltip from "@/components/shadTooltipComponent";
|
||||
import { removeCountFromString } from "@/utils/utils";
|
||||
import React from "react";
|
||||
import SidebarDraggableComponent from "../sidebarDraggableComponent";
|
||||
|
||||
const SidebarItemsList = ({
|
||||
item,
|
||||
dataFilter,
|
||||
nodeColors,
|
||||
chatInputAdded,
|
||||
onDragStart,
|
||||
sensitiveSort,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 py-2">
|
||||
{Object.keys(dataFilter[item.name])
|
||||
.sort((a, b) =>
|
||||
sensitiveSort(
|
||||
dataFilter[item.name][a].display_name,
|
||||
dataFilter[item.name][b].display_name,
|
||||
),
|
||||
)
|
||||
.map((SBItemName, idx) => {
|
||||
const currentItem = dataFilter[item.name][SBItemName];
|
||||
|
||||
return (
|
||||
<ShadTooltip
|
||||
content={currentItem.display_name}
|
||||
side="right"
|
||||
key={idx}
|
||||
>
|
||||
<SidebarDraggableComponent
|
||||
sectionName={item.name}
|
||||
apiClass={currentItem}
|
||||
icon={currentItem.icon ?? item.icon ?? "Unknown"}
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, {
|
||||
type: removeCountFromString(SBItemName),
|
||||
node: currentItem,
|
||||
})
|
||||
}
|
||||
color={nodeColors[item.name]}
|
||||
itemName={SBItemName}
|
||||
error={!!currentItem.error}
|
||||
display_name={currentItem.display_name}
|
||||
official={currentItem.official === false ? false : true}
|
||||
beta={currentItem.beta ?? false}
|
||||
legacy={currentItem.legacy ?? false}
|
||||
disabled={SBItemName === "ChatInput" && chatInputAdded}
|
||||
disabledTooltip="Chat input already added"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarItemsList;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { APIDataType } from "@/types/api";
|
||||
|
||||
export const applyBetaFilter = (filteredData: APIDataType) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(filteredData).map(([category, items]) => [
|
||||
category,
|
||||
Object.fromEntries(
|
||||
Object.entries(items).filter(([_, value]) => !value.beta),
|
||||
),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { APIDataType } from "@/types/api";
|
||||
|
||||
export const applyEdgeFilter = (filteredData: APIDataType, getFilterEdge) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(filteredData).map(([family, familyData]) => {
|
||||
const edgeFilter = getFilterEdge.find((x) => x.family === family);
|
||||
if (!edgeFilter) return [family, {}];
|
||||
|
||||
const filteredTypes = edgeFilter.type
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t !== "");
|
||||
|
||||
if (filteredTypes.length === 0) return [family, familyData];
|
||||
|
||||
const filteredFamilyData = Object.fromEntries(
|
||||
Object.entries(familyData).filter(([key]) =>
|
||||
filteredTypes.includes(key),
|
||||
),
|
||||
);
|
||||
|
||||
return [family, filteredFamilyData];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { APIDataType } from "@/types/api";
|
||||
|
||||
export const applyLegacyFilter = (filteredData: APIDataType) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(filteredData).map(([category, items]) => [
|
||||
category,
|
||||
Object.fromEntries(
|
||||
Object.entries(items).filter(([_, value]) => !value.legacy),
|
||||
),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { APIDataType } from "@/types/api";
|
||||
import { FuseResult } from "fuse.js";
|
||||
|
||||
export const combinedResultsFn = (
|
||||
fuseResults: FuseResult<any>[],
|
||||
data: APIDataType,
|
||||
) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(data).map(([category]) => {
|
||||
const categoryResults = fuseResults.filter(
|
||||
(result) => result.item.category === category,
|
||||
);
|
||||
const filteredItems = Object.fromEntries(
|
||||
categoryResults.map((result) => [result.item.key, result.item]),
|
||||
);
|
||||
return [category, filteredItems];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { APIDataType } from "@/types/api";
|
||||
import { FuseResult } from "fuse.js";
|
||||
|
||||
export const filteredDataFn = (
|
||||
data: APIDataType,
|
||||
combinedResults,
|
||||
traditionalResults,
|
||||
) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(data).map(([category, _]) => {
|
||||
const fuseItems = combinedResults[category] || {};
|
||||
const traditionalItems = traditionalResults[category] || {};
|
||||
|
||||
const mergedItems = {
|
||||
...fuseItems,
|
||||
...traditionalItems,
|
||||
};
|
||||
|
||||
return [category, mergedItems];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function normalizeString(str: string): string {
|
||||
return str.toLowerCase().replace(/_/g, " ").replace(/\s+/g, "");
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { normalizeString } from "./normalize-string";
|
||||
|
||||
export function searchInMetadata(metadata: any, searchTerm: string): boolean {
|
||||
if (!metadata || typeof metadata !== "object") return false;
|
||||
|
||||
return Object.entries(metadata).some(([key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
return (
|
||||
normalizeString(key).includes(searchTerm) ||
|
||||
normalizeString(value).includes(searchTerm)
|
||||
);
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return searchInMetadata(value, searchTerm);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { APIDataType } from "@/types/api";
|
||||
import { normalizeString } from "./normalize-string";
|
||||
import { searchInMetadata } from "./search-on-metadata";
|
||||
|
||||
export const traditionalSearchMetadata = (
|
||||
data: APIDataType,
|
||||
searchTerm: string,
|
||||
) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(data).map(([category, items]) => {
|
||||
const filteredItems = Object.fromEntries(
|
||||
Object.entries(items).filter(
|
||||
([key, item]) =>
|
||||
normalizeString(key).includes(searchTerm) ||
|
||||
normalizeString(item.display_name).includes(searchTerm) ||
|
||||
normalizeString(category).includes(searchTerm) ||
|
||||
(item.metadata && searchInMetadata(item.metadata, searchTerm)),
|
||||
),
|
||||
);
|
||||
return [category, filteredItems];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -45,34 +45,26 @@ import { APIClassType } from "../../../../types/api";
|
|||
import { SidebarFilterComponent } from "../extraSidebarComponent/sidebarFilterComponent";
|
||||
import sensitiveSort from "../extraSidebarComponent/utils/sensitive-sort";
|
||||
import ShortcutDisplay from "../nodeToolbarComponent/shortcutDisplay";
|
||||
import NoResultsMessage from "./components/emptySearchComponent";
|
||||
import FeatureToggles from "./components/featureTogglesComponent";
|
||||
import SidebarDraggableComponent from "./components/sidebarDraggableComponent";
|
||||
import SidebarMenuButtons from "./components/sidebarFooterButtons";
|
||||
import SidebarItemsList from "./components/sidebarItemsList";
|
||||
import { applyBetaFilter } from "./helpers/apply-beta-filter";
|
||||
import { applyEdgeFilter } from "./helpers/apply-edge-filter";
|
||||
import { applyLegacyFilter } from "./helpers/apply-legacy-filter";
|
||||
import { combinedResultsFn } from "./helpers/combined-results";
|
||||
import { filteredDataFn } from "./helpers/filtered-data";
|
||||
import { normalizeString } from "./helpers/normalize-string";
|
||||
import { traditionalSearchMetadata } from "./helpers/traditional-search-metadata";
|
||||
|
||||
const CATEGORIES = SIDEBAR_CATEGORIES;
|
||||
const BUNDLES = SIDEBAR_BUNDLES;
|
||||
|
||||
export function FlowSidebarComponent() {
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useHotkeys("/", (event) => {
|
||||
event.preventDefault();
|
||||
searchInputRef.current?.focus();
|
||||
});
|
||||
|
||||
// Add this new useHotkeys hook
|
||||
useHotkeys(
|
||||
"esc",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
searchInputRef.current?.blur();
|
||||
},
|
||||
{
|
||||
// Only enable this hotkey when the input is focused
|
||||
enableOnFormTags: true,
|
||||
enabled: isInputFocused,
|
||||
},
|
||||
);
|
||||
|
||||
const categories = SIDEBAR_CATEGORIES;
|
||||
const bundles = SIDEBAR_BUNDLES;
|
||||
|
||||
const data = useTypesStore((state) => state.data);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
const getFilterEdge = useFlowStore((state) => state.getFilterEdge);
|
||||
|
|
@ -93,6 +85,69 @@ export function FlowSidebarComponent() {
|
|||
const [showBeta, setShowBeta] = useState(true);
|
||||
const [showLegacy, setShowLegacy] = useState(false);
|
||||
|
||||
useHotkeys("/", (event) => {
|
||||
event.preventDefault();
|
||||
searchInputRef.current?.focus();
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
"esc",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
searchInputRef.current?.blur();
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
enabled: isInputFocused,
|
||||
},
|
||||
);
|
||||
|
||||
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.3,
|
||||
};
|
||||
|
||||
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.values(dataFilter).some(
|
||||
(category) => Object.keys(category).length > 0,
|
||||
|
|
@ -100,114 +155,48 @@ export function FlowSidebarComponent() {
|
|||
}, [dataFilter]);
|
||||
const [sortedCategories, setSortedCategories] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
filterComponents();
|
||||
}, [data, search, filterType, getFilterEdge, showBeta, showLegacy]);
|
||||
function normalizeString(str: string): string {
|
||||
return str.toLowerCase().replace(/_/g, " ").replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
function searchInMetadata(metadata: any, searchTerm: string): boolean {
|
||||
if (!metadata || typeof metadata !== "object") return false;
|
||||
|
||||
return Object.entries(metadata).some(([key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
return (
|
||||
normalizeString(key).includes(searchTerm) ||
|
||||
normalizeString(value).includes(searchTerm)
|
||||
);
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return searchInMetadata(value, searchTerm);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const filterComponents = () => {
|
||||
let filteredData = cloneDeep(data);
|
||||
|
||||
// Apply search filter
|
||||
if (search && fuse) {
|
||||
const results = fuse.search(search);
|
||||
setSortedCategories(results.map((result) => result.item.category));
|
||||
filteredData = Object.fromEntries(
|
||||
Object.entries(data).map(([category, items]) => {
|
||||
const categoryResults = results.filter(
|
||||
(result) => result.item.category === category,
|
||||
);
|
||||
const filteredItems = Object.fromEntries(
|
||||
categoryResults.map((result) => [result.item.key, result.item]),
|
||||
);
|
||||
return [category, filteredItems];
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// Fallback to traditional search if Fuse.js is not available
|
||||
if (search) {
|
||||
const searchTerm = normalizeString(search);
|
||||
filteredData = Object.fromEntries(
|
||||
Object.entries(data).map(([category, items]) => {
|
||||
const filteredItems = Object.fromEntries(
|
||||
Object.entries(items).filter(
|
||||
([key, item]) =>
|
||||
normalizeString(key).includes(searchTerm) ||
|
||||
normalizeString(item.display_name).includes(searchTerm) ||
|
||||
normalizeString(category).includes(searchTerm) ||
|
||||
(item.metadata && searchInMetadata(item.metadata, searchTerm)),
|
||||
),
|
||||
);
|
||||
return [category, filteredItems];
|
||||
}),
|
||||
);
|
||||
let combinedResults = {};
|
||||
|
||||
if (fuse) {
|
||||
const fuseResults = fuse.search(search);
|
||||
setSortedCategories(fuseResults.map((result) => result.item.category));
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply edge filter
|
||||
if (getFilterEdge?.length > 0) {
|
||||
filteredData = Object.fromEntries(
|
||||
Object.entries(filteredData).map(([family, familyData]) => {
|
||||
const edgeFilter = getFilterEdge.find((x) => x.family === family);
|
||||
if (!edgeFilter) return [family, {}];
|
||||
|
||||
const filteredTypes = edgeFilter.type
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t !== "");
|
||||
|
||||
if (filteredTypes.length === 0) return [family, familyData];
|
||||
|
||||
const filteredFamilyData = Object.fromEntries(
|
||||
Object.entries(familyData).filter(([key]) =>
|
||||
filteredTypes.includes(key),
|
||||
),
|
||||
);
|
||||
|
||||
return [family, filteredFamilyData];
|
||||
}),
|
||||
);
|
||||
filteredData = applyEdgeFilter(filteredData, getFilterEdge);
|
||||
}
|
||||
|
||||
// Apply beta filter
|
||||
if (!showBeta) {
|
||||
filteredData = Object.fromEntries(
|
||||
Object.entries(filteredData).map(([category, items]) => [
|
||||
category,
|
||||
Object.fromEntries(
|
||||
Object.entries(items).filter(([_, value]) => !value.beta),
|
||||
),
|
||||
]),
|
||||
);
|
||||
filteredData = applyBetaFilter(filteredData);
|
||||
}
|
||||
|
||||
// Apply legacy filter
|
||||
if (!showLegacy) {
|
||||
filteredData = Object.fromEntries(
|
||||
Object.entries(filteredData).map(([category, items]) => [
|
||||
category,
|
||||
Object.fromEntries(
|
||||
Object.entries(items).filter(([_, value]) => !value.legacy),
|
||||
),
|
||||
]),
|
||||
);
|
||||
filteredData = applyLegacyFilter(filteredData);
|
||||
}
|
||||
|
||||
setFilterData(filteredData);
|
||||
|
|
@ -220,12 +209,6 @@ export function FlowSidebarComponent() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (search === "" && getFilterEdge.length === 0) {
|
||||
setOpenCategories([]);
|
||||
}
|
||||
}, [search, getFilterEdge]);
|
||||
|
||||
function handleSearchInput(e: string) {
|
||||
setSearch(e);
|
||||
filterComponents();
|
||||
|
|
@ -247,42 +230,6 @@ export function FlowSidebarComponent() {
|
|||
event.dataTransfer.setData("genericNode", JSON.stringify(data));
|
||||
}
|
||||
|
||||
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"],
|
||||
threshold: 0.3,
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
const customComponent = useMemo(() => {
|
||||
return data?.["custom_component"]?.["CustomComponent"] ?? null;
|
||||
}, [data]);
|
||||
|
|
@ -301,12 +248,12 @@ export function FlowSidebarComponent() {
|
|||
}
|
||||
};
|
||||
|
||||
const hasBundleItems = bundles.some(
|
||||
const hasBundleItems = BUNDLES.some(
|
||||
(item) =>
|
||||
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
|
||||
);
|
||||
|
||||
const hasCategoryItems = categories.some(
|
||||
const hasCategoryItems = CATEGORIES.some(
|
||||
(item) =>
|
||||
dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
|
||||
);
|
||||
|
|
@ -318,7 +265,6 @@ export function FlowSidebarComponent() {
|
|||
}
|
||||
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
|
||||
const chatInputAdded = checkChatInput(nodes);
|
||||
|
||||
return (
|
||||
|
|
@ -348,38 +294,12 @@ export function FlowSidebarComponent() {
|
|||
</DisclosureTrigger>
|
||||
</div>
|
||||
<DisclosureContent>
|
||||
<div className="flex flex-col gap-7 border-b pb-7 pt-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="flex gap-2 text-sm font-medium">
|
||||
Show
|
||||
<Badge variant="pinkStatic" size="xq">
|
||||
BETA
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={showBeta}
|
||||
onCheckedChange={setShowBeta}
|
||||
data-testid="sidebar-beta-switch"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="flex gap-2 text-sm font-medium">
|
||||
Show
|
||||
<Badge variant="secondaryStatic" size="xq">
|
||||
LEGACY
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={showLegacy}
|
||||
onCheckedChange={setShowLegacy}
|
||||
data-testid="sidebar-legacy-switch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FeatureToggles
|
||||
showBeta={showBeta}
|
||||
setShowBeta={setShowBeta}
|
||||
showLegacy={showLegacy}
|
||||
setShowLegacy={setShowLegacy}
|
||||
/>
|
||||
</DisclosureContent>
|
||||
</Disclosure>
|
||||
<div className="relative w-full flex-1">
|
||||
|
|
@ -399,8 +319,8 @@ export function FlowSidebarComponent() {
|
|||
onChange={(e) => handleSearchInput(e.target.value)}
|
||||
/>
|
||||
{!isInputFocused && search === "" && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-8 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm text-muted-foreground">
|
||||
Type{" "}
|
||||
<div className="pointer-events-none absolute inset-y-0 left-8 top-1/2 flex w-4/5 -translate-y-1/2 items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||
Search{" "}
|
||||
<span>
|
||||
<ShortcutDisplay sidebar shortcut="/" />
|
||||
</span>
|
||||
|
|
@ -432,151 +352,70 @@ export function FlowSidebarComponent() {
|
|||
<SidebarMenuSkeleton />
|
||||
</SidebarMenuItem>
|
||||
))
|
||||
: categories
|
||||
.toSorted(
|
||||
(a, b) =>
|
||||
(search !== ""
|
||||
? sortedCategories
|
||||
: categories
|
||||
).findIndex((value) => value === a.name) -
|
||||
(search !== ""
|
||||
? sortedCategories
|
||||
: categories
|
||||
).findIndex((value) => value === b.name),
|
||||
)
|
||||
.map(
|
||||
(item) =>
|
||||
dataFilter[item.name] &&
|
||||
Object.keys(dataFilter[item.name]).length > 0 && (
|
||||
<Disclosure
|
||||
key={item.name}
|
||||
open={openCategories.includes(item.name)}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpenCategories((prev) =>
|
||||
isOpen
|
||||
? [...prev, item.name]
|
||||
: prev.filter(
|
||||
(cat) => cat !== item.name,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<DisclosureTrigger className="group/collapsible">
|
||||
<SidebarMenuButton asChild>
|
||||
<div
|
||||
data-testid={`disclosure-${item.display_name.toLocaleLowerCase()}`}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) =>
|
||||
handleKeyDown(e, item.name)
|
||||
}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={item.icon}
|
||||
className="h-4 w-4 group-aria-expanded/collapsible:text-accent-pink-foreground"
|
||||
/>
|
||||
<span className="flex-1 group-aria-expanded/collapsible:font-semibold">
|
||||
{item.display_name}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronRight"
|
||||
className="-mr-1 h-4 w-4 text-muted-foreground transition-all group-aria-expanded/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</DisclosureTrigger>
|
||||
<DisclosureContent>
|
||||
<div className="flex flex-col gap-1 py-2">
|
||||
{Object.keys(dataFilter[item.name])
|
||||
.sort((a, b) =>
|
||||
sensitiveSort(
|
||||
dataFilter[item.name][a]
|
||||
.display_name,
|
||||
dataFilter[item.name][b]
|
||||
.display_name,
|
||||
),
|
||||
)
|
||||
.map((SBItemName: string, idx) => (
|
||||
<ShadTooltip
|
||||
content={
|
||||
dataFilter[item.name][
|
||||
SBItemName
|
||||
].display_name
|
||||
}
|
||||
side="right"
|
||||
key={idx}
|
||||
>
|
||||
<SidebarDraggableComponent
|
||||
sectionName={
|
||||
item.name as string
|
||||
}
|
||||
apiClass={
|
||||
dataFilter[item.name][
|
||||
SBItemName
|
||||
]
|
||||
}
|
||||
icon={
|
||||
dataFilter[item.name][
|
||||
SBItemName
|
||||
].icon ??
|
||||
item.icon ??
|
||||
"Unknown"
|
||||
}
|
||||
key={idx}
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, {
|
||||
type: removeCountFromString(
|
||||
SBItemName,
|
||||
),
|
||||
node: dataFilter[item.name][
|
||||
SBItemName
|
||||
],
|
||||
})
|
||||
}
|
||||
color={nodeColors[item.name]}
|
||||
itemName={SBItemName}
|
||||
error={
|
||||
!!dataFilter[item.name][
|
||||
SBItemName
|
||||
].error
|
||||
}
|
||||
display_name={
|
||||
dataFilter[item.name][
|
||||
SBItemName
|
||||
].display_name
|
||||
}
|
||||
official={
|
||||
dataFilter[item.name][
|
||||
SBItemName
|
||||
].official === false
|
||||
? false
|
||||
: true
|
||||
}
|
||||
beta={
|
||||
dataFilter[item.name][
|
||||
SBItemName
|
||||
].beta ?? false
|
||||
}
|
||||
legacy={
|
||||
dataFilter[item.name][
|
||||
SBItemName
|
||||
].legacy ?? false
|
||||
}
|
||||
disabled={
|
||||
SBItemName === "ChatInput" &&
|
||||
chatInputAdded
|
||||
}
|
||||
disabledTooltip="Chat input already added"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
))}
|
||||
: CATEGORIES.toSorted(
|
||||
(a, b) =>
|
||||
(search !== ""
|
||||
? sortedCategories
|
||||
: CATEGORIES
|
||||
).findIndex((value) => value === a.name) -
|
||||
(search !== ""
|
||||
? sortedCategories
|
||||
: CATEGORIES
|
||||
).findIndex((value) => value === b.name),
|
||||
).map(
|
||||
(item) =>
|
||||
dataFilter[item.name] &&
|
||||
Object.keys(dataFilter[item.name]).length > 0 && (
|
||||
<Disclosure
|
||||
key={item.name}
|
||||
open={openCategories.includes(item.name)}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpenCategories((prev) =>
|
||||
isOpen
|
||||
? [...prev, item.name]
|
||||
: prev.filter((cat) => cat !== item.name),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<DisclosureTrigger className="group/collapsible">
|
||||
<SidebarMenuButton asChild>
|
||||
<div
|
||||
data-testid={`disclosure-${item.display_name.toLocaleLowerCase()}`}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) =>
|
||||
handleKeyDown(e, item.name)
|
||||
}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={item.icon}
|
||||
className="h-4 w-4 group-aria-expanded/collapsible:text-accent-pink-foreground"
|
||||
/>
|
||||
<span className="flex-1 group-aria-expanded/collapsible:font-semibold">
|
||||
{item.display_name}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronRight"
|
||||
className="-mr-1 h-4 w-4 text-muted-foreground transition-all group-aria-expanded/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</DisclosureContent>
|
||||
</SidebarMenuItem>
|
||||
</Disclosure>
|
||||
),
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</DisclosureTrigger>
|
||||
<DisclosureContent>
|
||||
<SidebarItemsList
|
||||
item={item}
|
||||
dataFilter={dataFilter}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
/>
|
||||
</DisclosureContent>
|
||||
</SidebarMenuItem>
|
||||
</Disclosure>
|
||||
),
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
|
@ -586,201 +425,82 @@ export function FlowSidebarComponent() {
|
|||
<SidebarGroupLabel>Bundles</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{bundles
|
||||
.toSorted(
|
||||
(a, b) =>
|
||||
(search !== ""
|
||||
? sortedCategories
|
||||
: bundles
|
||||
).findIndex((value) => value === a.name) -
|
||||
(search !== ""
|
||||
? sortedCategories
|
||||
: bundles
|
||||
).findIndex((value) => value === b.name),
|
||||
)
|
||||
.map(
|
||||
(item) =>
|
||||
dataFilter[item.name] &&
|
||||
Object.keys(dataFilter[item.name]).length > 0 && (
|
||||
<Disclosure
|
||||
key={item.name}
|
||||
open={openCategories.includes(item.name)}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpenCategories((prev) =>
|
||||
isOpen
|
||||
? [...prev, item.name]
|
||||
: prev.filter((cat) => cat !== item.name),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<DisclosureTrigger className="group/collapsible">
|
||||
<SidebarMenuButton asChild>
|
||||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) =>
|
||||
handleKeyDown(e, item.name)
|
||||
}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={item.icon}
|
||||
className="h-4 w-4 text-muted-foreground group-aria-expanded/collapsible:text-primary"
|
||||
/>
|
||||
<span className="flex-1 group-aria-expanded/collapsible:font-semibold">
|
||||
{item.display_name}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronRight"
|
||||
className="-mr-1 h-4 w-4 text-muted-foreground transition-all group-aria-expanded/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</DisclosureTrigger>
|
||||
<DisclosureContent>
|
||||
<div className="flex flex-col gap-1 py-2">
|
||||
{Object.keys(dataFilter[item.name])
|
||||
.sort((a, b) =>
|
||||
sensitiveSort(
|
||||
dataFilter[item.name][a].display_name,
|
||||
dataFilter[item.name][b].display_name,
|
||||
),
|
||||
)
|
||||
.map((SBItemName: string, idx) => (
|
||||
<ShadTooltip
|
||||
content={
|
||||
dataFilter[item.name][SBItemName]
|
||||
.display_name
|
||||
}
|
||||
side="right"
|
||||
key={idx}
|
||||
>
|
||||
<SidebarDraggableComponent
|
||||
sectionName={item.name as string}
|
||||
apiClass={
|
||||
dataFilter[item.name][SBItemName]
|
||||
}
|
||||
icon={
|
||||
dataFilter[item.name][SBItemName]
|
||||
.icon ??
|
||||
item.icon ??
|
||||
"Unknown"
|
||||
}
|
||||
key={idx}
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, {
|
||||
type: removeCountFromString(
|
||||
SBItemName,
|
||||
),
|
||||
node: dataFilter[item.name][
|
||||
SBItemName
|
||||
],
|
||||
})
|
||||
}
|
||||
color={nodeColors[item.name]}
|
||||
itemName={SBItemName}
|
||||
error={
|
||||
!!dataFilter[item.name][
|
||||
SBItemName
|
||||
].error
|
||||
}
|
||||
display_name={
|
||||
dataFilter[item.name][SBItemName]
|
||||
.display_name
|
||||
}
|
||||
official={
|
||||
dataFilter[item.name][SBItemName]
|
||||
.official === false
|
||||
? false
|
||||
: true
|
||||
}
|
||||
beta={
|
||||
dataFilter[item.name][SBItemName]
|
||||
.beta ?? false
|
||||
}
|
||||
legacy={
|
||||
dataFilter[item.name][SBItemName]
|
||||
.legacy ?? false
|
||||
}
|
||||
disabled={
|
||||
SBItemName === "ChatInput" &&
|
||||
chatInputAdded
|
||||
}
|
||||
disabledTooltip="Chat input already added"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
))}
|
||||
{BUNDLES.toSorted(
|
||||
(a, b) =>
|
||||
(search !== "" ? sortedCategories : BUNDLES).findIndex(
|
||||
(value) => value === a.name,
|
||||
) -
|
||||
(search !== "" ? sortedCategories : BUNDLES).findIndex(
|
||||
(value) => value === b.name,
|
||||
),
|
||||
).map(
|
||||
(item) =>
|
||||
dataFilter[item.name] &&
|
||||
Object.keys(dataFilter[item.name]).length > 0 && (
|
||||
<Disclosure
|
||||
key={item.name}
|
||||
open={openCategories.includes(item.name)}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpenCategories((prev) =>
|
||||
isOpen
|
||||
? [...prev, item.name]
|
||||
: prev.filter((cat) => cat !== item.name),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<DisclosureTrigger className="group/collapsible">
|
||||
<SidebarMenuButton asChild>
|
||||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) =>
|
||||
handleKeyDown(e, item.name)
|
||||
}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={item.icon}
|
||||
className="h-4 w-4 text-muted-foreground group-aria-expanded/collapsible:text-primary"
|
||||
/>
|
||||
<span className="flex-1 group-aria-expanded/collapsible:font-semibold">
|
||||
{item.display_name}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronRight"
|
||||
className="-mr-1 h-4 w-4 text-muted-foreground transition-all group-aria-expanded/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</DisclosureContent>
|
||||
</SidebarMenuItem>
|
||||
</Disclosure>
|
||||
),
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</DisclosureTrigger>
|
||||
<DisclosureContent>
|
||||
<SidebarItemsList
|
||||
item={item}
|
||||
dataFilter={dataFilter}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
/>
|
||||
</DisclosureContent>
|
||||
</SidebarMenuItem>
|
||||
</Disclosure>
|
||||
),
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
<p className="text-sm text-secondary-foreground">
|
||||
No components found.{" "}
|
||||
<a
|
||||
className="cursor-pointer underline underline-offset-4"
|
||||
onClick={handleClearSearch}
|
||||
>
|
||||
Clear your search
|
||||
</a>{" "}
|
||||
or filter and try a different query.
|
||||
</p>
|
||||
</div>
|
||||
<NoResultsMessage onClearSearch={handleClearSearch} />
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="border-t p-4 py-3">
|
||||
{hasStore && (
|
||||
<SidebarMenuButton asChild>
|
||||
<CustomLink
|
||||
to="/store"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group/discover"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<ForwardedIconComponent
|
||||
name="Store"
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<span className="flex-1 group-data-[state=open]/collapsible:font-semibold">
|
||||
Discover more components
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="SquareArrowOutUpRight"
|
||||
className="h-4 w-4 opacity-0 transition-all group-hover/discover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
</CustomLink>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
unstyled
|
||||
onClick={() => {
|
||||
if (customComponent) {
|
||||
addComponent(customComponent, "CustomComponent");
|
||||
}
|
||||
}}
|
||||
data-testid="sidebar-custom-component-button"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Plus"
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<span className="group-data-[state=open]/collapsible:font-semibold">
|
||||
Custom Component
|
||||
</span>
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButtons
|
||||
hasStore={hasStore}
|
||||
customComponent={customComponent}
|
||||
addComponent={addComponent}
|
||||
/>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue