feat(frontend): redesign-block-menu-part-2 (#10793)

In this PR, I have added:

- a search input
- conditional rendering of the search page and the default page
- a sidebar for the default page (with the correct data)

### Screenshot

<img width="1512" height="982" alt="Screenshot 2025-09-01 at 12 28
34 PM"
src="https://github.com/user-attachments/assets/891ab99f-dde5-47b8-a980-a700845f10c2"
/>

#### Checklist:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Everything works perfectly locally.
This commit is contained in:
Abhimanyu Yadav
2025-09-01 15:34:29 +05:30
committed by GitHub
parent ae4c9897b4
commit 417ee7f0e1
11 changed files with 361 additions and 4 deletions

View File

@@ -8,6 +8,7 @@ import { ToyBrick } from "lucide-react";
import { BlockMenuContent } from "../BlockMenuContent/BlockMenuContent";
import { ControlPanelButton } from "../ControlPanelButton";
import { useBlockMenu } from "./useBlockMenu";
import { BlockMenuStateProvider } from "../block-menu-provider";
interface BlockMenuProps {
pinBlocksPopover: boolean;
@@ -44,7 +45,10 @@ export const BlockMenu: React.FC<BlockMenuProps> = ({
className="absolute h-[75vh] w-[46.625rem] overflow-hidden rounded-[1rem] border-none p-0 shadow-[0_2px_6px_0_rgba(0,0,0,0.05)]"
data-id="blocks-control-popover-content"
>
<BlockMenuContent />
<BlockMenuStateProvider>
<BlockMenuContent />
</BlockMenuStateProvider>
</PopoverContent>
</Popover>
);

View File

@@ -1,10 +1,18 @@
"use client";
import React from "react";
import { useBlockMenuContext } from "../block-menu-provider";
import { BlockMenuSearchBar } from "../BlockMenuSearchBar/BlockMenuSearchBar";
import { Separator } from "@/components/ui/separator";
import { BlockMenuDefault } from "../BlockMenuDefault/BlockMenuDefault";
import { BlockMenuSearch } from "../BlockMenuSearch/BlockMenuSearch";
export const BlockMenuContent = () => {
const { searchQuery } = useBlockMenuContext();
return (
<div className="flex h-full w-full flex-col items-center justify-center">
This is the block menu content
<div className="flex h-full w-full flex-col">
<BlockMenuSearchBar />
<Separator className="h-[1px] w-full text-zinc-300" />
{searchQuery ? <BlockMenuSearch /> : <BlockMenuDefault />}
</div>
);
};

View File

@@ -0,0 +1,15 @@
import React from "react";
import { Separator } from "@/components/ui/separator";
import { BlockMenuDefaultContent } from "../BlockMenuDefaultContent/BlockMenuDefaultContent";
import { BlockMenuSidebar } from "../BlockMenuSidebar/BlockMenuSidebar";
export const BlockMenuDefault = () => {
return (
<div className="flex flex-1 overflow-y-auto">
<BlockMenuSidebar />
<Separator className="h-full w-[1px] text-zinc-300" />
<BlockMenuDefaultContent />
</div>
);
};

View File

@@ -0,0 +1,14 @@
import { Text } from "@/components/atoms/Text/Text";
import React from "react";
export const BlockMenuDefaultContent = () => {
return (
<div className="h-full flex-1 overflow-hidden flex items-center justify-center">
{/* I have added temporary content here, will fillup it in follow up prs */}
<Text variant="body" className="text-green-300">
This is the block menu default content
</Text>
</div>
);
};

View File

@@ -0,0 +1,12 @@
import { Text } from "@/components/atoms/Text/Text";
export const BlockMenuSearch = () => {
return (
// This is just a temporary text, will content inside in it [in follow-up prs]
<div className="flex items-center justify-center h-full w-full">
<Text variant="h3" className="text-green-300">
This is the block menu search
</Text>
</div>
);
};

View File

@@ -0,0 +1,53 @@
import { cn } from "@/lib/utils";
import React from "react";
import { Input } from "@/components/ui/input";
import { useBlockMenuSearchBar } from "./useBlockMenuSearchBar";
import { Button } from "@/components/ui/button";
import { MagnifyingGlassIcon, XIcon } from "@phosphor-icons/react";
interface BlockMenuSearchBarProps {
className?: string;
}
export const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
className = "",
}) => {
const { handleClear, inputRef, localQuery, setLocalQuery, debouncedSetSearchQuery } = useBlockMenuSearchBar();
return (
<div
className={cn(
"flex min-h-[3.5625rem] items-center gap-2.5 px-4",
className,
)}
>
<div className="flex h-6 w-6 items-center justify-center">
<MagnifyingGlassIcon className="h-6 w-6 text-zinc-700" strokeWidth={2} />
</div>
<Input
ref={inputRef}
type="text"
value={localQuery}
onChange={(e) => {
setLocalQuery(e.target.value);
debouncedSetSearchQuery(e.target.value);
}}
placeholder={"Blocks, Agents, Integrations or Keywords..."}
className={cn(
"m-0 border-none p-0 font-sans text-base font-normal text-zinc-800 shadow-none outline-none",
"placeholder:text-zinc-400 focus:shadow-none focus:outline-none focus:ring-0",
)}
/>
{localQuery.length > 0 && (
<Button
variant="ghost"
size={"sm"}
onClick={handleClear}
className="p-0 hover:bg-transparent"
>
<XIcon className="h-6 w-6 text-zinc-700" strokeWidth={2} />
</Button>
)}
</div>
);
};

View File

@@ -0,0 +1,46 @@
import { debounce } from "lodash";
import { useEffect, useRef, useState } from "react";
import { useBlockMenuContext } from "../block-menu-provider";
const SEARCH_DEBOUNCE_MS = 300;
export const useBlockMenuSearchBar = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [localQuery, setLocalQuery] = useState("");
const { setSearchQuery, setSearchId, searchId } = useBlockMenuContext();
const searchIdRef = useRef(searchId);
useEffect(() => {
searchIdRef.current = searchId;
}, [searchId]);
const debouncedSetSearchQuery = debounce((value: string) => {
setSearchQuery(value);
if (value.length === 0) {
setSearchId(undefined);
} else if (!searchIdRef.current) {
setSearchId(crypto.randomUUID());
}
}, SEARCH_DEBOUNCE_MS);
useEffect(() => {
return () => {
debouncedSetSearchQuery.cancel();
};
}, [debouncedSetSearchQuery]);
const handleClear = () => {
setLocalQuery("");
setSearchQuery("");
setSearchId(undefined);
debouncedSetSearchQuery.cancel();
};
return {
handleClear,
inputRef,
localQuery,
setLocalQuery,
debouncedSetSearchQuery,
}
};

View File

@@ -0,0 +1,117 @@
import React from "react";
import { MenuItem } from "../MenuItem";
import { DefaultStateType } from "../block-menu-provider";
import { useBlockMenuSidebar } from "./useBlockMenuSidebar";
import { Skeleton } from "@/components/ui/skeleton";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
export const BlockMenuSidebar = () => {
const { blockCounts, setDefaultState, defaultState, isLoading, isError, error } = useBlockMenuSidebar();
if (isLoading) {
return (
<div className="w-fit space-y-2 px-4 pt-4">
<Skeleton className="h-12 w-[12.875rem]" />
<Skeleton className="h-12 w-[12.875rem]" />
<Skeleton className="h-12 w-[12.875rem]" />
<Skeleton className="h-12 w-[12.875rem]" />
<Skeleton className="h-12 w-[12.875rem]" />
<Skeleton className="h-12 w-[12.875rem]" />
</div>
);
}
if (isError) {
return <div className="w-fit space-y-2 px-4 pt-4">
<ErrorCard className="w-[12.875rem]" httpError={{status: 500, statusText: "Internal Server Error", message: error?.detail || 'An error occurred'}} />
</div>
}
const topLevelMenuItems = [
{
name: "Suggestion",
type: "suggestion",
},
{
name: "All blocks",
type: "all_blocks",
number: blockCounts?.all_blocks,
},
];
const subMenuItems = [
{
name: "Input blocks",
type: "input_blocks",
number: blockCounts?.input_blocks,
},
{
name: "Action blocks",
type: "action_blocks",
number: blockCounts?.action_blocks,
},
{
name: "Output blocks",
type: "output_blocks",
number: blockCounts?.output_blocks,
},
];
const bottomMenuItems = [
{
name: "Integrations",
type: "integrations",
number: blockCounts?.integrations,
onClick: () => {
setDefaultState("integrations");
},
},
{
name: "Marketplace Agents",
type: "marketplace_agents",
number: blockCounts?.marketplace_agents,
},
{
name: "My Agents",
type: "my_agents",
number: blockCounts?.my_agents,
},
];
return (
<div className="w-fit space-y-2 px-4 pt-4">
{topLevelMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
/>
))}
<div className="ml-[0.5365rem] space-y-2 border-l border-black/10 pl-[0.75rem]">
{subMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
className="max-w-[11.5339rem]"
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
/>
))}
</div>
{bottomMenuItems.map((item) => (
<MenuItem
key={item.type}
name={item.name}
number={item.number}
selected={defaultState === item.type}
onClick={
item.onClick ||
(() => setDefaultState(item.type as DefaultStateType))
}
/>
))}
</div>
);
};

View File

@@ -0,0 +1,24 @@
import { useGetV2GetBuilderItemCounts } from "@/app/api/__generated__/endpoints/default/default";
import { useBlockMenuContext } from "../block-menu-provider";
import { CountResponse } from "@/app/api/__generated__/models/countResponse";
export const useBlockMenuSidebar = () => {
const { defaultState, setDefaultState } = useBlockMenuContext();
const { data: blockCounts, isLoading, isError, error} = useGetV2GetBuilderItemCounts({
query : {
select : (x) =>{
return x.data as CountResponse
}
}
});
return {
blockCounts,
setDefaultState,
defaultState,
isLoading,
isError,
error,
}
};

View File

@@ -0,0 +1,64 @@
"use client";
import { createContext, ReactNode, useContext, useState } from "react";
export type DefaultStateType =
| "suggestion"
| "all_blocks"
| "input_blocks"
| "action_blocks"
| "output_blocks"
| "integrations"
| "marketplace_agents"
| "my_agents";
interface BlockMenuContextType {
searchQuery: string;
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
searchId: string | undefined;
setSearchId: React.Dispatch<React.SetStateAction<string | undefined>>;
defaultState: DefaultStateType;
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
}
export const BlockMenuContext = createContext<BlockMenuContextType>(
{} as BlockMenuContextType,
);
interface BlockMenuStateProviderProps {
children: ReactNode;
}
export function BlockMenuStateProvider({
children,
}: BlockMenuStateProviderProps) {
const [searchQuery, setSearchQuery] = useState("");
const [searchId, setSearchId] = useState<string | undefined>(undefined);
const [defaultState, setDefaultState] = useState<DefaultStateType>("suggestion");
return (
<BlockMenuContext.Provider
value={{
searchQuery,
setSearchQuery,
searchId,
setSearchId,
defaultState,
setDefaultState,
}}
>
{children}
</BlockMenuContext.Provider>
);
}
export function useBlockMenuContext(): BlockMenuContextType {
const context = useContext(BlockMenuContext);
if (!context) {
throw new Error(
"useBlockMenuContext must be used within a BlockMenuStateProvider",
);
}
return context;
}

View File

@@ -18,7 +18,7 @@ export function ActionButtons({
context,
}: ActionButtonsProps) {
return (
<div className="flex flex-col gap-3 pt-2 sm:flex-row">
<div className="flex flex-col flex-wrap gap-3 pt-2 sm:flex-row">
{onRetry && (
<Button onClick={onRetry} variant="outline" size="small">
<ArrowClockwise size={16} weight="bold" />