Showing the changes to apps/web/src/components/ProjectScriptsControl.tsx (1 of 11 files). Show all
Unified · Split
| @@ -1,61 +1,28 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | ProjectScript, | 2 | ProjectScript, |
| 3 | ProjectScriptIcon, | ||
| 4 | ResolvedKeybindingsConfig, | 3 | ResolvedKeybindingsConfig, |
| 5 | T3ProjectFileScript, | 4 | T3ProjectFileScript, |
| 6 | } from "@t3tools/contracts"; | 5 | } from "@t3tools/contracts"; |
| 7 | import { | 6 | import { |
| 8 | isAtomCommandInterrupted, | 7 | isAtomCommandInterrupted, |
| 9 | squashAtomCommandFailure, | 8 | squashAtomCommandFailure, |
| 10 | type AtomCommandResult, | ||
| 11 | } from "@t3tools/client-runtime/state/runtime"; | 9 | } from "@t3tools/client-runtime/state/runtime"; |
| 12 | import { | 10 | import { ChevronDownIcon, DownloadIcon, PlusIcon, SettingsIcon } from "lucide-react"; |
| 13 | BugIcon, | 11 | import { useCallback, useMemo, useState } from "react"; |
| 14 | ChevronDownIcon, | ||
| 15 | DownloadIcon, | ||
| 16 | FlaskConicalIcon, | ||
| 17 | HammerIcon, | ||
| 18 | ListChecksIcon, | ||
| 19 | PlayIcon, | ||
| 20 | PlusIcon, | ||
| 21 | SettingsIcon, | ||
| 22 | WrenchIcon, | ||
| 23 | } from "lucide-react"; | ||
| 24 | import React, { type FormEvent, type KeyboardEvent, useCallback, useMemo, useState } from "react"; | ||
| 25 | 12 | ||
| 26 | import { | 13 | import { commandForProjectScript, primaryProjectScript } from "~/projectScripts"; |
| 27 | keybindingValueForCommand, | ||
| 28 | decodeProjectScriptKeybindingRule, | ||
| 29 | } from "~/lib/projectScriptKeybindings"; | ||
| 30 | import { keybindingFromKeyboardEvent } from "~/components/settings/KeybindingsSettings.logic"; | ||
| 31 | import { | ||
| 32 | commandForProjectScript, | ||
| 33 | nextProjectScriptId, | ||
| 34 | primaryProjectScript, | ||
| 35 | } from "~/projectScripts"; | ||
| 36 | import { shortcutLabelForCommand } from "~/keybindings"; | 14 | import { shortcutLabelForCommand } from "~/keybindings"; |
| 37 | import { | 15 | import { |
| 38 | AlertDialog, | 16 | EMPTY_PROJECT_SCRIPT_INPUT, |
| 39 | AlertDialogClose, | 17 | editorRequestForScript, |
| 40 | AlertDialogDescription, | 18 | ProjectScriptEditorDialog, |
| 41 | AlertDialogFooter, | 19 | ScriptIcon, |
| 42 | AlertDialogHeader, | 20 | type NewProjectScriptInput, |
| 43 | AlertDialogPopup, | 21 | type ProjectScriptActionResult, |
| 44 | AlertDialogTitle, | 22 | type ProjectScriptEditorRequest, |
| 45 | } from "./ui/alert-dialog"; | 23 | } from "./projectScriptEditor"; |
| 46 | import { Button } from "./ui/button"; | 24 | import { Button } from "./ui/button"; |
| 47 | import { | ||
| 48 | Dialog, | ||
| 49 | DialogDescription, | ||
| 50 | DialogFooter, | ||
| 51 | DialogHeader, | ||
| 52 | DialogPanel, | ||
| 53 | DialogPopup, | ||
| 54 | DialogTitle, | ||
| 55 | } from "./ui/dialog"; | ||
| 56 | import { Group, GroupSeparator } from "./ui/group"; | 25 | import { Group, GroupSeparator } from "./ui/group"; |
| 57 | import { Input } from "./ui/input"; | ||
| 58 | import { Label } from "./ui/label"; | ||
| 59 | import { | 26 | import { |
| 60 | Menu, | 27 | Menu, |
| 61 | MenuGroup, | 28 | MenuGroup, |
| @@ -66,48 +33,9 @@ import { · 4 unmodified lines | |||
| 66 | MenuShortcut, | 33 | MenuShortcut, |
| 67 | MenuTrigger, | 34 | MenuTrigger, |
| 68 | } from "./ui/menu"; | 35 | } from "./ui/menu"; |
| 69 | import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover"; | ||
| 70 | import { Switch } from "./ui/switch"; | ||
| 71 | import { Textarea } from "./ui/textarea"; | ||
| 72 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 36 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 73 | 37 | ||
| 74 | const SCRIPT_ICONS: Array<{ id: ProjectScriptIcon; label: string }> = [ | 38 | export type { NewProjectScriptInput, ProjectScriptActionResult }; |
| 75 | { id: "play", label: "Play" }, | ||
| 76 | { id: "test", label: "Test" }, | ||
| 77 | { id: "lint", label: "Lint" }, | ||
| 78 | { id: "configure", label: "Configure" }, | ||
| 79 | { id: "build", label: "Build" }, | ||
| 80 | { id: "debug", label: "Debug" }, | ||
| 81 | ]; | ||
| 82 | |||
| 83 | function ScriptIcon({ | ||
| 84 | icon, | ||
| 85 | className = "size-3.5", | ||
| 86 | }: { | ||
| 87 | icon: ProjectScriptIcon; | ||
| 88 | className?: string; | ||
| 89 | }) { | ||
| 90 | if (icon === "test") return <FlaskConicalIcon className={className} />; | ||
| 91 | if (icon === "lint") return <ListChecksIcon className={className} />; | ||
| 92 | if (icon === "configure") return <WrenchIcon className={className} />; | ||
| 93 | if (icon === "build") return <HammerIcon className={className} />; | ||
| 94 | if (icon === "debug") return <BugIcon className={className} />; | ||
| 95 | return <PlayIcon className={className} />; | ||
| 96 | } | ||
| 97 | |||
| 98 | export interface NewProjectScriptInput { | ||
| 99 | name: string; | ||
| 100 | command: string; | ||
| 101 | icon: ProjectScriptIcon; | ||
| 102 | runOnWorktreeCreate: boolean; | ||
| 103 | keybinding: string | null; | ||
| 104 | /** Optional URL to open in the in-app preview when this script runs. */ | ||
| 105 | previewUrl: string | null; | ||
| 106 | /** When true, automatically open the preview panel pointed at `previewUrl`. */ | ||
| 107 | autoOpenPreview: boolean; | ||
| 108 | } | ||
| 109 | |||
| 110 | export type ProjectScriptActionResult = AtomCommandResult<void, unknown>; | ||
| 111 | 39 | ||
| 112 | const NO_FILE_SCRIPTS: ReadonlyArray<T3ProjectFileScript> = []; | 40 | const NO_FILE_SCRIPTS: ReadonlyArray<T3ProjectFileScript> = []; |
| 113 | 41 | ||
| @@ -136,23 +64,11 @@ export default function ProjectScriptsControl({ · 22 unmodified lines | |||
| 136 | onUpdateScript, | 64 | onUpdateScript, |
| 137 | onDeleteScript, | 65 | onDeleteScript, |
| 138 | }: ProjectScriptsControlProps) { | 66 | }: ProjectScriptsControlProps) { |
| 139 | const addScriptFormId = React.useId(); | ||
| 140 | const [editingScriptId, setEditingScriptId] = useState<string | null>(null); | ||
| 141 | const [actionsMenuOpen, setActionsMenuOpen] = useState({ | 67 | const [actionsMenuOpen, setActionsMenuOpen] = useState({ |
| 142 | scripts: false, | 68 | scripts: false, |
| 143 | imports: false, | 69 | imports: false, |
| 144 | }); | 70 | }); |
| 145 | const [dialogOpen, setDialogOpen] = useState(false); | 71 | const [editorRequest, setEditorRequest] = useState<ProjectScriptEditorRequest | null>(null); |
| 146 | const [name, setName] = useState(""); | ||
| 147 | const [command, setCommand] = useState(""); | ||
| 148 | const [icon, setIcon] = useState<ProjectScriptIcon>("play"); | ||
| 149 | const [iconPickerOpen, setIconPickerOpen] = useState(false); | ||
| 150 | const [runOnWorktreeCreate, setRunOnWorktreeCreate] = useState(false); | ||
| 151 | const [keybinding, setKeybinding] = useState(""); | ||
| 152 | const [previewUrl, setPreviewUrl] = useState(""); | ||
| 153 | const [autoOpenPreview, setAutoOpenPreview] = useState(false); | ||
| 154 | const [validationError, setValidationError] = useState<string | null>(null); | ||
| 155 | const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); | ||
| 156 | 72 | ||
| 157 | const primaryScript = useMemo(() => { | 73 | const primaryScript = useMemo(() => { |
| 158 | if (preferredScriptId) { | 74 | if (preferredScriptId) { |
| @@ -173,112 +89,23 @@ export default function ProjectScriptsControl({ · 14 unmodified lines | |||
| 173 | ), | 89 | ), |
| 174 | [fileScripts, scripts], | 90 | [fileScripts, scripts], |
| 175 | ); | 91 | ); |
| 176 | const isEditing = editingScriptId !== null; | ||
| 177 | const dropdownItemClassName = | 92 | const dropdownItemClassName = |
| 178 | "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"; | 93 | "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"; |
| 179 | 94 | ||
| 180 | const captureKeybinding = (event: KeyboardEvent<HTMLInputElement>) => { | ||
| 181 | if (event.key === "Tab") return; | ||
| 182 | event.preventDefault(); | ||
| 183 | if (event.key === "Backspace" || event.key === "Delete") { | ||
| 184 | setKeybinding(""); | ||
| 185 | return; | ||
| 186 | } | ||
| 187 | const next = keybindingFromKeyboardEvent(event, navigator.platform); | ||
| 188 | if (!next) return; | ||
| 189 | setKeybinding(next); | ||
| 190 | }; | ||
| 191 | |||
| 192 | const submitAddScript = async (event: FormEvent) => { | ||
| 193 | event.preventDefault(); | ||
| 194 | const trimmedName = name.trim(); | ||
| 195 | const trimmedCommand = command.trim(); | ||
| 196 | if (trimmedName.length === 0) { | ||
| 197 | setValidationError("Name is required."); | ||
| 198 | return; | ||
| 199 | } | ||
| 200 | if (trimmedCommand.length === 0) { | ||
| 201 | setValidationError("Command is required."); | ||
| 202 | return; | ||
| 203 | } | ||
| 204 | |||
| 205 | setValidationError(null); | ||
| 206 | let payload: NewProjectScriptInput; | ||
| 207 | try { | ||
| 208 | const scriptIdForValidation = | ||
| 209 | editingScriptId ?? | ||
| 210 | nextProjectScriptId( | ||
| 211 | trimmedName, | ||
| 212 | scripts.map((script) => script.id), | ||
| 213 | ); | ||
| 214 | const keybindingRule = decodeProjectScriptKeybindingRule({ | ||
| 215 | keybinding, | ||
| 216 | command: commandForProjectScript(scriptIdForValidation), | ||
| 217 | }); | ||
| 218 | const trimmedPreviewUrl = previewUrl.trim(); | ||
| 219 | payload = { | ||
| 220 | name: trimmedName, | ||
| 221 | command: trimmedCommand, | ||
| 222 | icon, | ||
| 223 | runOnWorktreeCreate, | ||
| 224 | keybinding: keybindingRule?.key ?? null, | ||
| 225 | previewUrl: trimmedPreviewUrl.length > 0 ? trimmedPreviewUrl : null, | ||
| 226 | autoOpenPreview: trimmedPreviewUrl.length > 0 ? autoOpenPreview : false, | ||
| 227 | } satisfies NewProjectScriptInput; | ||
| 228 | } catch (error) { | ||
| 229 | setValidationError(error instanceof Error ? error.message : "Failed to save action."); | ||
| 230 | return; | ||
| 231 | } | ||
| 232 | |||
| 233 | const result = editingScriptId | ||
| 234 | ? await onUpdateScript(editingScriptId, payload) | ||
| 235 | : await onAddScript(payload); | ||
| 236 | if (result._tag === "Failure") { | ||
| 237 | if (!isAtomCommandInterrupted(result)) { | ||
| 238 | const error = squashAtomCommandFailure(result); | ||
| 239 | setValidationError(error instanceof Error ? error.message : "Failed to save action."); | ||
| 240 | } | ||
| 241 | return; | ||
| 242 | } | ||
| 243 | setDialogOpen(false); | ||
| 244 | setIconPickerOpen(false); | ||
| 245 | }; | ||
| 246 | |||
| 247 | const openAddDialog = () => { | 95 | const openAddDialog = () => { |
| 248 | setEditingScriptId(null); | 96 | setEditorRequest({ scriptId: null, initial: EMPTY_PROJECT_SCRIPT_INPUT }); |
| 249 | setName(""); | ||
| 250 | setCommand(""); | ||
| 251 | setIcon("play"); | ||
| 252 | setIconPickerOpen(false); | ||
| 253 | setRunOnWorktreeCreate(false); | ||
| 254 | setKeybinding(""); | ||
| 255 | setPreviewUrl(""); | ||
| 256 | setAutoOpenPreview(false); | ||
| 257 | setValidationError(null); | ||
| 258 | setDialogOpen(true); | ||
| 259 | }; | 97 | }; |
| 260 | 98 | ||
| 261 | const openEditDialog = (script: ProjectScript) => { | 99 | const openEditDialog = (script: ProjectScript) => { |
| 262 | setActionsMenuOpen({ scripts: false, imports: false }); | 100 | setActionsMenuOpen({ scripts: false, imports: false }); |
| 263 | setEditingScriptId(script.id); | 101 | setEditorRequest(editorRequestForScript(script, keybindings)); |
| 264 | setName(script.name); | ||
| 265 | setCommand(script.command); | ||
| 266 | setIcon(script.icon); | ||
| 267 | setIconPickerOpen(false); | ||
| 268 | setRunOnWorktreeCreate(script.runOnWorktreeCreate); | ||
| 269 | setKeybinding(keybindingValueForCommand(keybindings, commandForProjectScript(script.id)) ?? ""); | ||
| 270 | setPreviewUrl(script.previewUrl ?? ""); | ||
| 271 | setAutoOpenPreview(script.autoOpenPreview ?? false); | ||
| 272 | setValidationError(null); | ||
| 273 | setDialogOpen(true); | ||
| 274 | }; | 102 | }; |
| 275 | 103 | ||
| 276 | const confirmDeleteScript = useCallback(() => { | 104 | const submitScript = useCallback( |
| 277 | if (!editingScriptId) return; | 105 | (scriptId: string | null, input: NewProjectScriptInput) => |
| 278 | setDeleteConfirmOpen(false); | 106 | scriptId === null ? onAddScript(input) : onUpdateScript(scriptId, input), |
| 279 | setDialogOpen(false); | 107 | [onAddScript, onUpdateScript], |
| 280 | void onDeleteScript(editingScriptId); | 108 | ); |
| 281 | }, [editingScriptId, onDeleteScript]); | ||
| 282 | 109 | ||
| 283 | const importFileScript = async (fileScript: T3ProjectFileScript) => { | 110 | const importFileScript = async (fileScript: T3ProjectFileScript) => { |
| 284 | const payload: NewProjectScriptInput = { | 111 | const payload: NewProjectScriptInput = { |
| @@ -295,17 +122,11 @@ export default function ProjectScriptsControl({ · 10 unmodified lines | |||
| 295 | // Surface the failure through the regular add dialog, prefilled so the | 122 | // Surface the failure through the regular add dialog, prefilled so the |
| 296 | // user can adjust and retry. | 123 | // user can adjust and retry. |
| 297 | const error = squashAtomCommandFailure(result); | 124 | const error = squashAtomCommandFailure(result); |
| 298 | setEditingScriptId(null); | 125 | setEditorRequest({ |
| 299 | setName(payload.name); | 126 | scriptId: null, |
| 300 | setCommand(payload.command); | 127 | initial: payload, |
| 301 | setIcon(payload.icon); | 128 | error: error instanceof Error ? error.message : "Failed to import action.", |
| 302 | setIconPickerOpen(false); | 129 | }); |
| 303 | setRunOnWorktreeCreate(payload.runOnWorktreeCreate); | ||
| 304 | setKeybinding(""); | ||
| 305 | setPreviewUrl(payload.previewUrl ?? ""); | ||
| 306 | setAutoOpenPreview(payload.autoOpenPreview); | ||
| 307 | setValidationError(error instanceof Error ? error.message : "Failed to import action."); | ||
| 308 | setDialogOpen(true); | ||
| 309 | } | 130 | } |
| 310 | }; | 131 | }; |
| 311 | 132 | ||
| @@ -466,184 +287,13 @@ export default function ProjectScriptsControl({ · 154 unmodified lines | |||
| 466 | </Tooltip> | 287 | </Tooltip> |
| 467 | )} | 288 | )} |
| 468 | 289 | ||
| 469 | <Dialog | 290 | <ProjectScriptEditorDialog |
| 470 | onOpenChange={(open) => { | 291 | request={editorRequest} |
| 471 | setDialogOpen(open); | 292 | scripts={scripts} |
| 472 | if (!open) { | 293 | onSubmit={submitScript} |
| 473 | setIconPickerOpen(false); | 294 | onDelete={(scriptId) => void onDeleteScript(scriptId)} |
| 474 | } | 295 | onClose={() => setEditorRequest(null)} |
| 475 | }} | 296 | /> |
| 476 | onOpenChangeComplete={(open) => { | ||
| 477 | if (open) return; | ||
| 478 | setEditingScriptId(null); | ||
| 479 | setName(""); | ||
| 480 | setCommand(""); | ||
| 481 | setIcon("play"); | ||
| 482 | setRunOnWorktreeCreate(false); | ||
| 483 | setKeybinding(""); | ||
| 484 | setPreviewUrl(""); | ||
| 485 | setAutoOpenPreview(false); | ||
| 486 | setValidationError(null); | ||
| 487 | }} | ||
| 488 | open={dialogOpen} | ||
| 489 | > | ||
| 490 | <DialogPopup> | ||
| 491 | <DialogHeader> | ||
| 492 | <DialogTitle>{isEditing ? "Edit Action" : "Add Action"}</DialogTitle> | ||
| 493 | <DialogDescription> | ||
| 494 | Actions are project-scoped commands you can run from the top bar or keybindings. | ||
| 495 | </DialogDescription> | ||
| 496 | </DialogHeader> | ||
| 497 | <DialogPanel> | ||
| 498 | <form id={addScriptFormId} className="space-y-4" onSubmit={submitAddScript}> | ||
| 499 | <div className="space-y-1.5"> | ||
| 500 | <Label htmlFor="script-name">Name</Label> | ||
| 501 | <div className="flex items-center gap-2"> | ||
| 502 | <Popover onOpenChange={setIconPickerOpen} open={iconPickerOpen}> | ||
| 503 | <PopoverTrigger | ||
| 504 | render={ | ||
| 505 | <Button | ||
| 506 | type="button" | ||
| 507 | variant="outline" | ||
| 508 | className="size-9 shrink-0 hover:bg-popover active:bg-popover data-pressed:bg-popover data-pressed:shadow-xs/5 data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:border-transparent dark:bg-white/[0.035] dark:data-pressed:before:shadow-none" | ||
| 509 | aria-label="Choose icon" | ||
| 510 | /> | ||
| 511 | } | ||
| 512 | > | ||
| 513 | <ScriptIcon icon={icon} className="size-4.5" /> | ||
| 514 | </PopoverTrigger> | ||
| 515 | <PopoverPopup align="start"> | ||
| 516 | <div className="grid grid-cols-3 gap-2"> | ||
| 517 | {SCRIPT_ICONS.map((entry) => { | ||
| 518 | const isSelected = entry.id === icon; | ||
| 519 | return ( | ||
| 520 | <button | ||
| 521 | key={entry.id} | ||
| 522 | type="button" | ||
| 523 | className={`relative flex flex-col items-center gap-2 rounded-md border px-2 py-2 text-xs dark:border-transparent ${ | ||
| 524 | isSelected | ||
| 525 | ? "border-primary/70 bg-primary/10 dark:ring-1 dark:ring-primary/30" | ||
| 526 | : "border-border/70 hover:bg-accent/60 dark:bg-white/[0.035]" | ||
| 527 | }`} | ||
| 528 | onClick={() => { | ||
| 529 | setIcon(entry.id); | ||
| 530 | setIconPickerOpen(false); | ||
| 531 | }} | ||
| 532 | > | ||
| 533 | <ScriptIcon icon={entry.id} className="size-4" /> | ||
| 534 | <span>{entry.label}</span> | ||
| 535 | </button> | ||
| 536 | ); | ||
| 537 | })} | ||
| 538 | </div> | ||
| 539 | </PopoverPopup> | ||
| 540 | </Popover> | ||
| 541 | <Input | ||
| 542 | id="script-name" | ||
| 543 | autoFocus | ||
| 544 | placeholder="Test" | ||
| 545 | value={name} | ||
| 546 | onChange={(event) => setName(event.target.value)} | ||
| 547 | /> | ||
| 548 | </div> | ||
| 549 | </div> | ||
| 550 | <div className="space-y-1.5"> | ||
| 551 | <Label htmlFor="script-keybinding">Keybinding</Label> | ||
| 552 | <Input | ||
| 553 | id="script-keybinding" | ||
| 554 | placeholder="Press shortcut" | ||
| 555 | value={keybinding} | ||
| 556 | readOnly | ||
| 557 | onKeyDown={captureKeybinding} | ||
| 558 | /> | ||
| 559 | <p className="text-xs text-muted-foreground"> | ||
| 560 | Press a shortcut. Use <code>Backspace</code> to clear. | ||
| 561 | </p> | ||
| 562 | </div> | ||
| 563 | <div className="space-y-1.5"> | ||
| 564 | <Label htmlFor="script-command">Command</Label> | ||
| 565 | <Textarea | ||
| 566 | id="script-command" | ||
| 567 | placeholder="bun test" | ||
| 568 | value={command} | ||
| 569 | onChange={(event) => setCommand(event.target.value)} | ||
| 570 | /> | ||
| 571 | </div> | ||
| 572 | <div className="space-y-1.5"> | ||
| 573 | <Label htmlFor="script-preview-url">Preview URL (optional)</Label> | ||
| 574 | <Input | ||
| 575 | id="script-preview-url" | ||
| 576 | placeholder="http://localhost:5173" | ||
| 577 | value={previewUrl} | ||
| 578 | onChange={(event) => setPreviewUrl(event.target.value)} | ||
| 579 | /> | ||
| 580 | <p className="text-xs text-muted-foreground"> | ||
| 581 | Open this URL in the in-app preview when this action runs. | ||
| 582 | </p> | ||
| 583 | </div> | ||
| 584 | <label className="flex items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2 text-sm dark:border-transparent dark:bg-white/[0.035]"> | ||
| 585 | <span>Run automatically on worktree creation</span> | ||
| 586 | <Switch | ||
| 587 | checked={runOnWorktreeCreate} | ||
| 588 | onCheckedChange={(checked) => setRunOnWorktreeCreate(Boolean(checked))} | ||
| 589 | /> | ||
| 590 | </label> | ||
| 591 | <label | ||
| 592 | className={`flex items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2 text-sm dark:border-transparent dark:bg-white/[0.035] ${ | ||
| 593 | previewUrl.trim().length === 0 ? "opacity-60" : "" | ||
| 594 | }`} | ||
| 595 | > | ||
| 596 | <span>Open preview automatically when this action runs</span> | ||
| 597 | <Switch | ||
| 598 | checked={autoOpenPreview} | ||
| 599 | disabled={previewUrl.trim().length === 0} | ||
| 600 | onCheckedChange={(checked) => setAutoOpenPreview(Boolean(checked))} | ||
| 601 | /> | ||
| 602 | </label> | ||
| 603 | {validationError && <p className="text-sm text-destructive">{validationError}</p>} | ||
| 604 | </form> | ||
| 605 | </DialogPanel> | ||
| 606 | <DialogFooter className="dark:border-transparent dark:bg-transparent"> | ||
| 607 | {isEditing && ( | ||
| 608 | <Button | ||
| 609 | type="button" | ||
| 610 | variant="destructive-outline" | ||
| 611 | className="mr-auto" | ||
| 612 | onClick={() => setDeleteConfirmOpen(true)} | ||
| 613 | > | ||
| 614 | Delete | ||
| 615 | </Button> | ||
| 616 | )} | ||
| 617 | <Button | ||
| 618 | type="button" | ||
| 619 | variant="outline" | ||
| 620 | onClick={() => { | ||
| 621 | setDialogOpen(false); | ||
| 622 | }} | ||
| 623 | > | ||
| 624 | Cancel | ||
| 625 | </Button> | ||
| 626 | <Button form={addScriptFormId} type="submit"> | ||
| 627 | {isEditing ? "Save changes" : "Save action"} | ||
| 628 | </Button> | ||
| 629 | </DialogFooter> | ||
| 630 | </DialogPopup> | ||
| 631 | </Dialog> | ||
| 632 | |||
| 633 | <AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}> | ||
| 634 | <AlertDialogPopup> | ||
| 635 | <AlertDialogHeader> | ||
| 636 | <AlertDialogTitle>Delete action "{name}"?</AlertDialogTitle> | ||
| 637 | <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription> | ||
| 638 | </AlertDialogHeader> | ||
| 639 | <AlertDialogFooter> | ||
| 640 | <AlertDialogClose render={<Button variant="outline" />}>Cancel</AlertDialogClose> | ||
| 641 | <Button variant="destructive" onClick={confirmDeleteScript}> | ||
| 642 | Delete action | ||
| 643 | </Button> | ||
| 644 | </AlertDialogFooter> | ||
| 645 | </AlertDialogPopup> | ||
| 646 | </AlertDialog> | ||
| 647 | </> | 297 | </> |
| 648 | ); | 298 | ); |
| 649 | } | 299 | } |