Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions extensions/commandPalette/CommandPalette.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react";
import { Admin } from "webiny/extensions";

export const CommandPalette = () => {
return (
<>
<Admin.Extension
src={"@/extensions/commandPalette/commands/simpleCommand/SimpleCommand.tsx"}
/>
<Admin.Extension
src={"@/extensions/commandPalette/commands/formCommand/FormCommand.tsx"}
/>
</>
);
};
39 changes: 39 additions & 0 deletions extensions/commandPalette/commands/formCommand/FormCommand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import { Command, RegisterFeature, createFeature } from "webiny/admin";
import { SendMessageDetailView } from "./SendMessageDetailView.js";

interface SendMessageParams {
recipient: string;
message: string;
}

class SendMessageCommand implements Command.Interface {
name = "send-message";
label = "Send Message";
description = "Send a message to someone";
category = "Demo";
keywords = ["send", "message", "form"];
shortcut = "cmd+shift+m";
detailView = SendMessageDetailView;

execute(params?: unknown) {
const { recipient, message } = params as SendMessageParams;
alert(`Message sent to ${recipient}: "${message}"`);
}
}

const SendMessageCommandImpl = Command.createImplementation({
implementation: SendMessageCommand,
dependencies: []
});

const SendMessageCommandFeature = createFeature({
name: "SendMessageCommand",
register(container) {
container.register(SendMessageCommandImpl);
}
});

export default () => {
return <RegisterFeature feature={SendMessageCommandFeature} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react";
import { Form, Bind } from "webiny/admin/form";
import { Input, Button } from "webiny/admin/ui";
import { Command } from "webiny/admin";

interface FormData {
recipient: string;
message: string;
}

export const SendMessageDetailView = ({ command, onClose }: Command.DetailProps) => {
return (
<Form<FormData>
data={{ recipient: "", message: "" }}
onSubmit={data => {
command.execute(data);
onClose();
}}
>
{({ submit }) => (
<div className="p-md space-y-md">
<Bind name="recipient">
<Input
label="Recipient"
placeholder="Enter recipient name"
autoFocus={true}
/>
</Bind>
<Bind name="message">
<Input label="Message" placeholder="Enter your message" />
</Bind>
<div className="flex justify-end gap-sm pt-sm">
<Button variant="primary" onClick={submit}>
Send
</Button>
</div>
</div>
)}
</Form>
);
};
30 changes: 30 additions & 0 deletions extensions/commandPalette/commands/simpleCommand/SimpleCommand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import { Command, RegisterFeature, createFeature } from "webiny/admin";

class SayHelloCommand implements Command.Interface {
name = "say-hello";
label = "Say Hello";
description = "Displays a greeting in the console";
category = "Demo";
keywords = ["hello", "greet", "demo"];

execute() {
alert("Hello from the Command Palette!");
}
}

const SayHelloCommandImpl = Command.createImplementation({
implementation: SayHelloCommand,
dependencies: []
});

const SayHelloCommandFeature = createFeature({
name: "SayHelloCommand",
register(container) {
container.register(SayHelloCommandImpl);
}
});

export default () => {
return <RegisterFeature feature={SayHelloCommandFeature} />;
};
78 changes: 78 additions & 0 deletions packages/admin-ui/src/CommandPalette/CommandPalette.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { ReactComponent as ArrowLeftIcon } from "@webiny/icons/arrow_back.svg";
import { makeDecoratable, cn, withStaticProps } from "~/utils.js";
import type { CommandPaletteProps } from "./types.js";
import { CommandPaletteContent } from "./components/CommandPaletteContent.js";
import { CommandPaletteSearch } from "./components/CommandPaletteSearch.js";
import { CommandPaletteList } from "./components/CommandPaletteList.js";

const CommandPaletteBase = ({
open,
onOpenChange,
commands,
detailView,
onSelectCommand,
onCancelCommand,
placeholder
}: CommandPaletteProps) => {
const handleOpenChange = React.useCallback(
(nextOpen: boolean) => {
if (!nextOpen) {
onCancelCommand();
}
onOpenChange(nextOpen);
},
[onOpenChange, onCancelCommand]
);

return (
<CommandPaletteContent open={open} onOpenChange={handleOpenChange}>
{detailView ? (
<div>
<div
className={cn(
"flex items-center gap-sm px-md py-sm border-b border-neutral-muted"
)}
>
<button
type="button"
onClick={onCancelCommand}
className={cn(
"flex items-center justify-center w-lg h-lg rounded",
"hover:bg-neutral-dimmed text-neutral-secondary",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-default"
)}
>
<ArrowLeftIcon className="w-md h-md fill-current" />
</button>
<div className="flex items-center gap-sm-extra min-w-0">
{detailView.icon && (
<span className="flex items-center justify-center w-md-plus h-md-plus shrink-0 fill-neutral-xstrong">
{detailView.icon}
</span>
)}
<span className="text-md font-semibold text-neutral-primary truncate">
{detailView.label}
</span>
</div>
</div>
{detailView.element}
</div>
) : (
<CommandPrimitive className="flex flex-col outline-none">
<CommandPaletteSearch placeholder={placeholder} />
<CommandPaletteList commands={commands} onSelect={onSelectCommand} />
</CommandPrimitive>
)}
</CommandPaletteContent>
);
};

CommandPaletteBase.displayName = "CommandPalette";

const DecoratableCommandPalette = makeDecoratable("CommandPalette", CommandPaletteBase);

const CommandPalette = withStaticProps(DecoratableCommandPalette, {});

export { CommandPalette, type CommandPaletteProps };
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from "react";
import { Dialog as DialogPrimitive, VisuallyHidden } from "radix-ui";
import { cn } from "~/utils.js";

interface CommandPaletteContentProps {
open: boolean;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
}

const CommandPaletteContent = ({ open, onOpenChange, children }: CommandPaletteContentProps) => {
return (
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className={cn(
"z-overlay",
"fixed inset-0 bg-neutral-dark/50",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
)}
/>
<DialogPrimitive.Content
className={cn(
"fixed left-[50%] top-[20%] translate-x-[-50%] w-[640px]",
"max-w-[calc(100vw-var(--spacing-lg))]",
"bg-neutral-base rounded-xl shadow-lg border-none",
"overflow-hidden focus:outline-none",
"z-overlay",
"duration-200",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
)}
aria-describedby={undefined}
>
<VisuallyHidden.Root>
<DialogPrimitive.Title></DialogPrimitive.Title>
</VisuallyHidden.Root>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
};

export { CommandPaletteContent };
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "~/utils.js";
import type { CommandPaletteCommand } from "../types.js";

interface CommandPaletteItemProps {
command: CommandPaletteCommand;
onSelect: () => void;
}

const CommandPaletteItem = ({ command, onSelect }: CommandPaletteItemProps) => {
return (
<CommandPrimitive.Item
value={command.name}
keywords={command.keywords}
onSelect={onSelect}
className={cn(
"flex items-center gap-sm-extra px-lg py-sm-extra cursor-default select-none",
"text-md text-neutral-primary",
"data-[selected=true]:bg-neutral-dimmed",
"outline-none rounded-md mx-sm-extra"
)}
>
{command.icon && (
<span className="flex items-center justify-center w-lg h-lg shrink-0 fill-neutral-xstrong">
{command.icon}
</span>
)}
<div className="flex flex-col flex-1 min-w-0">
<span className="truncate">{command.label}</span>
{command.description && (
<span className="text-sm text-neutral-secondary truncate">
{command.description}
</span>
)}
</div>
{command.shortcut && (
<kbd
className={cn(
"ml-auto shrink-0 text-xs text-neutral-secondary",
"bg-neutral-dimmed px-xs-plus py-xs rounded"
)}
>
{command.shortcut}
</kbd>
)}
</CommandPrimitive.Item>
);
};

export { CommandPaletteItem };
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "~/utils.js";
import type { CommandPaletteCommand } from "../types.js";
import { CommandPaletteItem } from "./CommandPaletteItem.js";

interface CommandPaletteListProps {
commands: CommandPaletteCommand[];
onSelect: (name: string) => void;
}

const CommandPaletteList = ({ commands, onSelect }: CommandPaletteListProps) => {
const grouped = React.useMemo(() => {
const groups = new Map<string, CommandPaletteCommand[]>();
for (const cmd of commands) {
const category = cmd.category ?? "";
const list = groups.get(category) ?? [];
list.push(cmd);
groups.set(category, list);
}
return groups;
}, [commands]);

return (
<CommandPrimitive.List
className={cn(
"max-h-[360px] overflow-y-auto overflow-x-hidden py-sm-extra",
"bg-neutral-base text-neutral-strong"
)}
>
<CommandPrimitive.Empty className="py-xl text-center text-sm text-neutral-secondary">
No commands found.
</CommandPrimitive.Empty>
{[...grouped.entries()].map(([category, cmds]) => {
if (category === "") {
return cmds.map(cmd => (
<CommandPaletteItem
key={cmd.name}
command={cmd}
onSelect={() => onSelect(cmd.name)}
/>
));
}
return (
<CommandPrimitive.Group
key={category}
heading={category}
className={cn(
"[&_[cmdk-group-heading]]:px-lg [&_[cmdk-group-heading]]:py-sm-extra",
"[&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold",
"[&_[cmdk-group-heading]]:text-neutral-secondary [&_[cmdk-group-heading]]:uppercase",
"[&_[cmdk-group-heading]]:tracking-wide"
)}
>
{cmds.map(cmd => (
<CommandPaletteItem
key={cmd.name}
command={cmd}
onSelect={() => onSelect(cmd.name)}
/>
))}
</CommandPrimitive.Group>
);
})}
</CommandPrimitive.List>
);
};

export { CommandPaletteList };
Loading
Loading