import type { ProjectScript, ResolvedKeybindingsConfig, T3ProjectFileScript, } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { ChevronDownIcon, DownloadIcon, PlusIcon, SettingsIcon } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { commandForProjectScript, primaryProjectScript } from "~/projectScripts"; import { shortcutLabelForCommand } from "~/keybindings"; import { EMPTY_PROJECT_SCRIPT_INPUT, editorRequestForScript, ProjectScriptEditorDialog, ScriptIcon, type NewProjectScriptInput, type ProjectScriptActionResult, type ProjectScriptEditorRequest, } from "./projectScriptEditor"; import { Button } from "./ui/button"; import { Group, GroupSeparator } from "./ui/group"; import { Menu, MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, MenuSeparator, MenuShortcut, MenuTrigger, } from "./ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; export type { NewProjectScriptInput, ProjectScriptActionResult }; const NO_FILE_SCRIPTS: ReadonlyArray = []; interface ProjectScriptsControlProps { scripts: ReadonlyArray; /** Scripts declared in the project's checked-in t3.json, offered for import. */ fileScripts?: ReadonlyArray; keybindings: ResolvedKeybindingsConfig; preferredScriptId?: string | null; onRunScript: (script: ProjectScript) => void; onAddScript: (input: NewProjectScriptInput) => Promise; onUpdateScript: ( scriptId: string, input: NewProjectScriptInput, ) => Promise; onDeleteScript: (scriptId: string) => Promise; } export default function ProjectScriptsControl({ scripts, fileScripts = NO_FILE_SCRIPTS, keybindings, preferredScriptId = null, onRunScript, onAddScript, onUpdateScript, onDeleteScript, }: ProjectScriptsControlProps) { const [actionsMenuOpen, setActionsMenuOpen] = useState({ scripts: false, imports: false, }); const [editorRequest, setEditorRequest] = useState(null); const primaryScript = useMemo(() => { if (preferredScriptId) { const preferred = scripts.find((script) => script.id === preferredScriptId); if (preferred) return preferred; } return primaryProjectScript(scripts); }, [preferredScriptId, scripts]); const importableScripts = useMemo( () => fileScripts.filter( (fileScript) => !scripts.some( (script) => script.command === fileScript.command || script.name.toLowerCase() === fileScript.name.toLowerCase(), ), ), [fileScripts, scripts], ); const dropdownItemClassName = "data-highlighted:bg-transparent data-highlighted:text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground data-highlighted:hover:bg-accent data-highlighted:hover:text-accent-foreground data-highlighted:focus-visible:bg-accent data-highlighted:focus-visible:text-accent-foreground"; const openAddDialog = () => { setEditorRequest({ scriptId: null, initial: EMPTY_PROJECT_SCRIPT_INPUT }); }; const openEditDialog = (script: ProjectScript) => { setActionsMenuOpen({ scripts: false, imports: false }); setEditorRequest(editorRequestForScript(script, keybindings)); }; const submitScript = useCallback( (scriptId: string | null, input: NewProjectScriptInput) => scriptId === null ? onAddScript(input) : onUpdateScript(scriptId, input), [onAddScript, onUpdateScript], ); const importFileScript = async (fileScript: T3ProjectFileScript) => { const payload: NewProjectScriptInput = { name: fileScript.name, command: fileScript.command, icon: fileScript.icon ?? "play", runOnWorktreeCreate: fileScript.runOnWorktreeCreate ?? false, keybinding: null, previewUrl: fileScript.previewUrl ?? null, autoOpenPreview: fileScript.previewUrl ? (fileScript.autoOpenPreview ?? false) : false, }; const result = await onAddScript(payload); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { // Surface the failure through the regular add dialog, prefilled so the // user can adjust and retry. const error = squashAtomCommandFailure(result); setEditorRequest({ scriptId: null, initial: payload, error: error instanceof Error ? error.message : "Failed to import action.", }); } }; const importMenuItems = importableScripts.length > 0 && ( <> {primaryScript && } From t3.json {importableScripts.map((fileScript) => ( void importFileScript(fileScript)} > {fileScript.name} ))} ); return ( <> {primaryScript ? ( onRunScript(primaryScript)} /> } > {primaryScript.name} Run {primaryScript.name} setActionsMenuOpen({ scripts: open, imports: false })} > } > {scripts.map((script) => { const shortcutLabel = shortcutLabelForCommand( keybindings, commandForProjectScript(script.id), ); return ( onRunScript(script)} > {script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name} {shortcutLabel && ( {shortcutLabel} )} ); })} {importMenuItems} Add action ) : importableScripts.length > 0 ? ( setActionsMenuOpen({ scripts: false, imports: open })} > }> Add action {importMenuItems} Add action ) : ( } > Add action Add action )} void onDeleteScript(scriptId)} onClose={() => setEditorRequest(null)} /> ); }