Merge pull request #2515 from divaltor/filter-projects-shortcut

feat(input): Add focus by Cmd + K shortcut to search input
This commit is contained in:
Mauricio Siu
2025-09-06 14:32:52 -06:00
committed by GitHub
4 changed files with 99 additions and 24 deletions

View File

@@ -44,7 +44,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -144,12 +144,13 @@ export const ShowProjects = () => {
<> <>
<div className="flex max-sm:flex-col gap-4 items-center w-full"> <div className="flex max-sm:flex-col gap-4 items-center w-full">
<div className="flex-1 relative max-sm:w-full"> <div className="flex-1 relative max-sm:w-full">
<Input <FocusShortcutInput
placeholder="Filter projects..." placeholder="Filter projects..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10" className="pr-10"
/> />
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" /> <Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div> </div>
<div className="flex items-center gap-2 min-w-48 max-sm:w-full"> <div className="flex items-center gap-2 min-w-48 max-sm:w-full">

View File

@@ -3,6 +3,10 @@
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react"; import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React from "react"; import React from "react";
import {
extractServices,
type Services,
} from "@/components/dashboard/settings/users/add-permissions";
import { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -20,13 +24,34 @@ import {
CommandSeparator, CommandSeparator,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
// import {
// extractServices,
// type Services,
// } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { StatusTooltip } from "../shared/status-tooltip"; import { StatusTooltip } from "../shared/status-tooltip";
// Extended Services type to include environmentId and environmentName for search navigation
type SearchServices = Services & {
environmentId: string;
environmentName: string;
};
const extractAllServicesFromProject = (project: any): SearchServices[] => {
const allServices: SearchServices[] = [];
// Iterate through all environments in the project
project.environments?.forEach((environment: any) => {
const environmentServices = extractServices(environment);
const servicesWithEnvironmentId: SearchServices[] = environmentServices.map(
(service) => ({
...service,
environmentId: environment.environmentId,
environmentName: environment.name,
}),
);
allServices.push(...servicesWithEnvironmentId);
});
return allServices;
};
export const SearchCommand = () => { export const SearchCommand = () => {
const router = useRouter(); const router = useRouter();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -51,7 +76,7 @@ export const SearchCommand = () => {
return ( return (
<div> <div>
{/* <CommandDialog open={open} onOpenChange={setOpen}> <CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput <CommandInput
placeholder={"Search projects or settings"} placeholder={"Search projects or settings"}
value={search} value={search}
@@ -63,25 +88,37 @@ export const SearchCommand = () => {
</CommandEmpty> </CommandEmpty>
<CommandGroup heading={"Projects"}> <CommandGroup heading={"Projects"}>
<CommandList> <CommandList>
{data?.map((project) => ( {data?.map((project) => {
<CommandItem console.log("project", project);
key={project.projectId} const productionEnvironment = project.environments.find(
onSelect={() => { (environment) => environment.name === "production",
router.push(`/dashboard/project/${project.projectId}`); );
setOpen(false);
}} if (!productionEnvironment) return null;
>
<BookIcon className="size-4 text-muted-foreground mr-2" /> return (
{project.name} <CommandItem
</CommandItem> key={project.projectId}
))} onSelect={() => {
router.push(
`/dashboard/project/${project.projectId}/environment/${productionEnvironment!.environmentId}`,
);
setOpen(false);
}}
>
<BookIcon className="size-4 text-muted-foreground mr-2" />
{project.name} / {productionEnvironment!.name}
</CommandItem>
);
})}
</CommandList> </CommandList>
</CommandGroup> </CommandGroup>
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading={"Services"}> <CommandGroup heading={"Services"}>
<CommandList> <CommandList>
{data?.map((project) => { {data?.map((project) => {
const applications: Services[] = extractServices(project); const applications: SearchServices[] =
extractAllServicesFromProject(project);
return applications.map((application) => ( return applications.map((application) => (
<CommandItem <CommandItem
key={application.id} key={application.id}
@@ -114,7 +151,8 @@ export const SearchCommand = () => {
<CircuitBoard className="h-6 w-6 mr-2" /> <CircuitBoard className="h-6 w-6 mr-2" />
)} )}
<span className="flex-grow"> <span className="flex-grow">
{project.name} / {application.name}{" "} {project.name} / {application.environmentName} /{" "}
{application.name}{" "}
<div style={{ display: "none" }}>{application.id}</div> <div style={{ display: "none" }}>{application.id}</div>
</span> </span>
<div> <div>
@@ -181,7 +219,7 @@ export const SearchCommand = () => {
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</CommandDialog> */} </CommandDialog>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,36 @@
import { useEffect, useRef } from "react";
import { Input } from "@/components/ui/input";
type Props = React.ComponentPropsWithoutRef<typeof Input>;
export const FocusShortcutInput = (props: Props) => {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
const isMod = e.metaKey || e.ctrlKey;
if (!isMod || e.key.toLowerCase() !== "k") return;
const target = e.target as HTMLElement | null;
if (target) {
const tag = target.tagName;
if (
target.isContentEditable ||
tag === "INPUT" ||
tag === "TEXTAREA" ||
tag === "SELECT" ||
target.getAttribute("role") === "textbox"
)
return;
}
e.preventDefault();
inputRef.current?.focus();
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
return <Input {...props} ref={inputRef} />;
};

View File

@@ -80,7 +80,6 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -96,6 +95,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
export type Services = { export type Services = {
appName: string; appName: string;
@@ -1197,7 +1197,7 @@ const EnvironmentPage = (
<div className="flex flex-col gap-2 lg:flex-row lg:gap-4 lg:items-center"> <div className="flex flex-col gap-2 lg:flex-row lg:gap-4 lg:items-center">
<div className="w-full relative"> <div className="w-full relative">
<Input <FocusShortcutInput
placeholder="Filter services..." placeholder="Filter services..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}