"use client"; import { createElement, useMemo, useState, useCallback } from "react"; import { IconPlus, IconTrash, IconRefresh } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { CardContent } from "@kandev/ui/card"; import { Input } from "@kandev/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@kandev/ui/tabs"; import { useToast } from "@/components/toast-provider"; import { SettingsSection } from "@/components/settings/settings-section"; import { SettingsCard } from "@/components/settings/settings-card"; import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider"; import { useGitHubActionPresets } from "@/hooks/domains/github/use-github-action-presets"; import { DEFAULT_ISSUE_PRESETS, DEFAULT_PR_PRESETS, PRESET_ICON_CHOICES, iconForPresetKey, } from "@/components/github/my-github/action-presets"; import type { GitHubActionPreset } from "@/lib/types/github"; import { ScriptEditor, computeEditorHeight, } from "@/components/settings/profile-edit/script-editor"; import type { ScriptPlaceholder } from "@/components/settings/profile-edit/script-editor-completions"; const ACTION_PROMPT_PLACEHOLDERS: ScriptPlaceholder[] = [ { key: "url", description: "URL of the PR or issue", example: "https://github.com/org/repo/pull/42", executor_types: [], }, { key: "title", description: "Title of the PR or issue", example: "Fix login page crash", executor_types: [], }, ]; function newPreset(): GitHubActionPreset { return { id: `preset_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, label: "New action", hint: "", icon: "sparkle", prompt_template: "", }; } function PresetIconSelect({ value, isDirty, onChange, }: { value: string; isDirty: boolean; onChange: (v: string) => void; }) { return ( {createElement(iconForPresetKey(value), { className: "h-4 w-4" })} {PRESET_ICON_CHOICES.map((choice) => { const ChoiceIcon = choice.icon; return ( {choice.label} ); })} ); } function PresetRow({ preset, baseline, expanded, onToggle, onPatch, onRemove, }: { preset: GitHubActionPreset; baseline?: GitHubActionPreset; expanded: boolean; onToggle: () => void; onPatch: (patch: Partial) => void; onRemove: () => void; }) { return ( Icon onPatch({ icon: v })} /> Label onPatch({ label: e.target.value })} /> Hint onPatch({ hint: e.target.value })} /> {expanded ? "Hide prompt" : "Edit prompt"} {expanded && ( onPatch({ prompt_template: v })} language="markdown" height={computeEditorHeight(preset.prompt_template)} lineNumbers="off" placeholders={ACTION_PROMPT_PLACEHOLDERS} /> Type {"{{"} to see available placeholders.{" "} {"{{url}}"} and{" "} {"{{title}}"} are substituted when the action runs. )} ); } function PresetEditor({ presets, baseline, onChange, addLabel, }: { presets: GitHubActionPreset[]; baseline: GitHubActionPreset[]; onChange: (presets: GitHubActionPreset[]) => void; addLabel: string; }) { const [expandedId, setExpandedId] = useState(null); const patch = useCallback( (index: number, change: Partial) => { onChange(presets.map((p, i) => (i === index ? { ...p, ...change } : p))); }, [presets, onChange], ); const remove = useCallback( (index: number) => { onChange(presets.filter((_, i) => i !== index)); }, [presets, onChange], ); const add = useCallback(() => { const created = newPreset(); onChange([...presets, created]); setExpandedId(created.id); }, [presets, onChange]); return ( {presets.map((preset, index) => ( candidate.id === preset.id)} expanded={expandedId === preset.id} onToggle={() => setExpandedId((id) => (id === preset.id ? null : preset.id))} onPatch={(p) => patch(index, p)} onRemove={() => remove(index)} /> ))} {addLabel} ); } function usePresetDrafts(workspaceId: string): { prDraft: GitHubActionPreset[]; issueDraft: GitHubActionPreset[]; setPrDraft: (next: GitHubActionPreset[]) => void; setIssueDraft: (next: GitHubActionPreset[]) => void; dirty: boolean; prBaseline: GitHubActionPreset[]; issueBaseline: GitHubActionPreset[]; save: () => Promise; reset: () => void; discard: () => void; loading: boolean; } { const { presets, save, loading } = useGitHubActionPresets(workspaceId); const [prDraft, setPrDraft] = useState(() => presets?.pr?.length ? presets.pr : DEFAULT_PR_PRESETS, ); const [issueDraft, setIssueDraft] = useState(() => presets?.issue?.length ? presets.issue : DEFAULT_ISSUE_PRESETS, ); const [prBaseline, setPrBaseline] = useState(prDraft); const [issueBaseline, setIssueBaseline] = useState(issueDraft); const dirty = useMemo( () => JSON.stringify(prBaseline) !== JSON.stringify(prDraft) || JSON.stringify(issueBaseline) !== JSON.stringify(issueDraft), [prBaseline, issueBaseline, prDraft, issueDraft], ); const [syncedPresets, setSyncedPresets] = useState(presets); if (presets && presets !== syncedPresets && !dirty) { const nextPr = presets.pr?.length ? presets.pr : DEFAULT_PR_PRESETS; const nextIssue = presets.issue?.length ? presets.issue : DEFAULT_ISSUE_PRESETS; setSyncedPresets(presets); setPrBaseline(nextPr); setIssueBaseline(nextIssue); setPrDraft(nextPr); setIssueDraft(nextIssue); } const persist = useCallback(async () => { const submittedPr = prDraft; const submittedIssue = issueDraft; const response = await save({ pr: submittedPr, issue: submittedIssue }); const savedPr = response?.pr?.length ? response.pr : submittedPr; const savedIssue = response?.issue?.length ? response.issue : submittedIssue; setPrBaseline(savedPr); setIssueBaseline(savedIssue); setPrDraft((current) => JSON.stringify(current) === JSON.stringify(submittedPr) ? savedPr : current, ); setIssueDraft((current) => JSON.stringify(current) === JSON.stringify(submittedIssue) ? savedIssue : current, ); }, [save, prDraft, issueDraft]); const doReset = useCallback(() => { setPrDraft(DEFAULT_PR_PRESETS); setIssueDraft(DEFAULT_ISSUE_PRESETS); }, []); const discard = useCallback(() => { setPrDraft(prBaseline); setIssueDraft(issueBaseline); }, [prBaseline, issueBaseline]); return { prDraft, issueDraft, setPrDraft, setIssueDraft, dirty, prBaseline, issueBaseline, save: persist, reset: doReset, discard, loading, }; } export function ActionPresetsSection({ workspaceId }: { workspaceId: string }) { const { toast } = useToast(); const { prDraft, issueDraft, prBaseline, issueBaseline, setPrDraft, setIssueDraft, dirty, save, reset, discard, loading, } = usePresetDrafts(workspaceId); const handleSave = useCallback(async () => { try { await save(); toast({ description: "Quick actions saved", variant: "success" }); } catch { toast({ description: "Failed to save quick actions", variant: "error" }); throw new Error("Failed to save quick actions"); } }, [save, toast]); useSettingsSaveContributor({ id: `github-action-presets:${workspaceId}`, revision: JSON.stringify([prDraft, issueDraft]), isDirty: dirty, canSave: !loading, invalidReason: loading ? "Quick actions are still loading." : undefined, save: handleSave, discard, }); return ( Reset } > Pull requests Issues ); }
Type {"{{"} to see available placeholders.{" "} {"{{url}}"} and{" "} {"{{title}}"} are substituted when the action runs.
{"{{url}}"}
{"{{title}}"}