mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-13 01:08:13 +02:00
command palette tweaks
This commit is contained in:
@@ -13,7 +13,9 @@ import {
|
||||
CommandList,
|
||||
CommandSeparator
|
||||
} from "@app/components/ui/command";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { useMediaQuery } from "@app/hooks/useMediaQuery";
|
||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
@@ -22,7 +24,8 @@ import {
|
||||
LaptopIcon,
|
||||
PlugIcon,
|
||||
ServerIcon,
|
||||
UserIcon
|
||||
UserIcon,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -34,6 +37,7 @@ import React, {
|
||||
useMemo,
|
||||
useState
|
||||
} from "react";
|
||||
import { useCanUseCommandPalette } from "./useCanUseCommandPalette";
|
||||
import { useCommandPaletteActions } from "./useCommandPaletteActions";
|
||||
import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
|
||||
import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
|
||||
@@ -57,6 +61,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
|
||||
const router = useRouter();
|
||||
const { open, setOpen } = useCommandPalette();
|
||||
const [search, setSearch] = useState("");
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
|
||||
const isActionMode = search.startsWith(">");
|
||||
|
||||
@@ -134,6 +139,20 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
isLoading={!isActionMode && shouldSearch && isLoading}
|
||||
trailing={
|
||||
!isDesktop ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 shrink-0"
|
||||
aria-label={t("close")}
|
||||
onClick={() => handleOpenChange(false)}
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<CommandList className="max-h-118 min-h-0 h-(--cmdk-list-height) scroll-pb-4 scroll-pt-2 transition-[height] duration-250 ease-in-out">
|
||||
<CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty>
|
||||
@@ -427,13 +446,14 @@ function isEditableTarget(target: EventTarget | null) {
|
||||
);
|
||||
}
|
||||
|
||||
export function CommandPaletteProvider({
|
||||
function CommandPaletteProviderInner({
|
||||
children,
|
||||
orgId,
|
||||
orgs,
|
||||
navItems
|
||||
}: CommandPaletteProviderProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const canUseCommandPalette = useCanUseCommandPalette(orgId, orgs);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setOpen((current) => !current);
|
||||
@@ -449,6 +469,10 @@ export function CommandPaletteProvider({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canUseCommandPalette) {
|
||||
return;
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (
|
||||
event.key.toLowerCase() !== "k" ||
|
||||
@@ -467,12 +491,18 @@ export function CommandPaletteProvider({
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [open, toggle]);
|
||||
}, [canUseCommandPalette, open, toggle]);
|
||||
|
||||
return (
|
||||
<CommandPaletteContext value={contextValue}>
|
||||
{children}
|
||||
<CommandPalette orgId={orgId} orgs={orgs} navItems={navItems} />
|
||||
{canUseCommandPalette ? (
|
||||
<CommandPalette orgId={orgId} orgs={orgs} navItems={navItems} />
|
||||
) : null}
|
||||
</CommandPaletteContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function CommandPaletteProvider(props: CommandPaletteProviderProps) {
|
||||
return <CommandPaletteProviderInner {...props} />;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@ import { cn } from "@app/lib/cn";
|
||||
import { Search } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||
import { useCommandPalette } from "./CommandPalette";
|
||||
import { useCanUseCommandPalette } from "./useCanUseCommandPalette";
|
||||
|
||||
type CommandPaletteTriggerProps = {
|
||||
variant?: "header" | "mobile";
|
||||
className?: string;
|
||||
orgId?: string;
|
||||
orgs?: ListUserOrgsResponse["orgs"];
|
||||
};
|
||||
|
||||
function useIsMac() {
|
||||
@@ -25,11 +29,18 @@ function useIsMac() {
|
||||
|
||||
export function CommandPaletteTrigger({
|
||||
variant = "header",
|
||||
className
|
||||
className,
|
||||
orgId,
|
||||
orgs
|
||||
}: CommandPaletteTriggerProps) {
|
||||
const t = useTranslations();
|
||||
const { setOpen } = useCommandPalette();
|
||||
const isMac = useIsMac();
|
||||
const canUseCommandPalette = useCanUseCommandPalette(orgId, orgs);
|
||||
|
||||
if (!canUseCommandPalette) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (variant === "mobile") {
|
||||
return (
|
||||
@@ -49,13 +60,13 @@ export function CommandPaletteTrigger({
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"hidden h-9 w-56 justify-start gap-2 px-3 text-muted-foreground md:flex lg:w-64",
|
||||
"relative hidden h-9 w-56 justify-start pl-8 pr-3 text-muted-foreground md:flex lg:w-64",
|
||||
className
|
||||
)}
|
||||
aria-label={t("commandPaletteTitle")}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Search className="size-4 shrink-0 opacity-50" />
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<span className="flex-1 truncate text-left text-sm font-normal">
|
||||
{t("commandPaletteSearchPlaceholder")}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function useCanUseCommandPalette(
|
||||
orgId?: string,
|
||||
orgs?: ListUserOrgsResponse["orgs"]
|
||||
) {
|
||||
const pathname = usePathname();
|
||||
const { user } = useUserContext();
|
||||
|
||||
if (pathname?.startsWith("/admin")) {
|
||||
return user.serverAdmin;
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentOrg = orgs?.find((org) => org.orgId === orgId);
|
||||
return Boolean(currentOrg?.isAdmin || currentOrg?.isOwner);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
BellRing,
|
||||
Building2,
|
||||
Globe,
|
||||
GlobeLock,
|
||||
KeyRound,
|
||||
MonitorUp,
|
||||
Plus,
|
||||
@@ -86,6 +87,12 @@ export function useCommandPaletteActions(
|
||||
icon: <Globe className="size-4" />,
|
||||
href: `/${orgId}/settings/resources/proxy/create`
|
||||
});
|
||||
actions.push({
|
||||
id: "create-private-resource",
|
||||
label: t("commandPaletteCreatePrivateResource"),
|
||||
icon: <GlobeLock className="size-4" />,
|
||||
href: `/${orgId}/settings/resources/private/create`
|
||||
});
|
||||
actions.push({
|
||||
id: "create-machine-client",
|
||||
label: t("commandPaletteCreateMachineClient"),
|
||||
|
||||
@@ -130,7 +130,7 @@ export function useCommandPaletteSearch({
|
||||
return privateResourcesQuery.data.map((resource) => ({
|
||||
id: `site-resource-${resource.siteResourceId}`,
|
||||
name: resource.name,
|
||||
href: `/${orgId}/settings/resources/private?query=${resource.name}`
|
||||
href: `/${orgId}/settings/resources/private/${resource.niceId}`
|
||||
}));
|
||||
}, [orgId, privateResourcesQuery.data]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user