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:
Cristhian Zanforlin Lousa 2024-11-07 15:45:26 -03:00 committed by GitHub
commit f9ae9f27f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 598 additions and 539 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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),
),
]),
);
};

View file

@ -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];
}),
);
};

View file

@ -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),
),
]),
);
};

View file

@ -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];
}),
);
};

View file

@ -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];
}),
);
};

View file

@ -0,0 +1,3 @@
export function normalizeString(str: string): string {
return str.toLowerCase().replace(/_/g, " ").replace(/\s+/g, "");
}

View file

@ -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;
});
}

View file

@ -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];
}),
);
};

View file

@ -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>
);