refactor: Update component specifications to use 'program' and 'behavior' attributes, enhancing clarity and consistency across the application
This commit is contained in:
@@ -32,11 +32,18 @@ const ACTION_CONFIG: Record<string, ActionConfig> = {
|
||||
|
||||
const DEV_ACTIONS = ["build", "test", "lint", "type-check", "check"]
|
||||
|
||||
export interface ActionOutput {
|
||||
action: string
|
||||
text: string
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
interface ProgramActionsProps {
|
||||
name: string
|
||||
actions: string[]
|
||||
installed?: boolean | null
|
||||
compact?: boolean
|
||||
onOutput?: (output: ActionOutput) => void
|
||||
}
|
||||
|
||||
function visibleActions(
|
||||
@@ -69,30 +76,27 @@ function visibleActions(
|
||||
return visible
|
||||
}
|
||||
|
||||
export function ProgramActions({ name, actions, installed, compact }: ProgramActionsProps) {
|
||||
export function ProgramActions({ name, actions, installed, compact, onOutput }: ProgramActionsProps) {
|
||||
const { mutate, isPending } = useProgramAction()
|
||||
const [runningAction, setRunningAction] = useState<string | null>(null)
|
||||
const [output, setOutput] = useState<{ action: string; text: string; ok: boolean } | null>(null)
|
||||
|
||||
const visible = visibleActions(actions, installed, !!compact)
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
setRunningAction(action)
|
||||
setOutput(null)
|
||||
onOutput?.({ action: "", text: "", ok: true }) // clear previous
|
||||
mutate(
|
||||
{ name, action },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setRunningAction(null)
|
||||
// Show output for dev actions on detail page
|
||||
if (!compact && DEV_ACTIONS.includes(action) && data.output) {
|
||||
setOutput({ action, text: data.output, ok: true })
|
||||
onOutput?.({ action, text: data.output, ok: true })
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
setRunningAction(null)
|
||||
if (!compact && DEV_ACTIONS.includes(action)) {
|
||||
// Extract detail from API error JSON
|
||||
let text = String(err)
|
||||
try {
|
||||
const parsed = JSON.parse((err as Error).message)
|
||||
@@ -100,7 +104,7 @@ export function ProgramActions({ name, actions, installed, compact }: ProgramAct
|
||||
} catch {
|
||||
text = (err as Error).message ?? text
|
||||
}
|
||||
setOutput({ action, text, ok: false })
|
||||
onOutput?.({ action, text, ok: false })
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -110,58 +114,59 @@ export function ProgramActions({ name, actions, installed, compact }: ProgramAct
|
||||
if (visible.length === 0) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{visible.map((action) => {
|
||||
const config = ACTION_CONFIG[action]
|
||||
if (!config) return null
|
||||
const Icon = config.icon
|
||||
const isRunning = isPending && runningAction === action
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`p-1 rounded ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
title={config.label}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{visible.map((action) => {
|
||||
const config = ACTION_CONFIG[action]
|
||||
if (!config) return null
|
||||
const Icon = config.icon
|
||||
const isRunning = isPending && runningAction === action
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-sm rounded border ${config.borderColor} ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
className={`p-1 rounded ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
title={config.label}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
{config.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
{output && !compact && (
|
||||
<div className={`mt-3 rounded-lg border overflow-hidden ${output.ok ? "border-[var(--border)]" : "border-red-800"}`}>
|
||||
<div className={`flex items-center justify-between px-3 py-1.5 text-xs ${output.ok ? "text-green-400 bg-green-900/20" : "text-red-400 bg-red-900/20"}`}>
|
||||
<span>{ACTION_CONFIG[output.action]?.label ?? output.action} — {output.ok ? "ok" : "error"}</span>
|
||||
<button
|
||||
onClick={() => setOutput(null)}
|
||||
className="text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
>
|
||||
dismiss
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 text-xs font-mono text-gray-300 overflow-x-auto max-h-64 overflow-y-auto bg-black/40">
|
||||
{output.text}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-sm rounded border ${config.borderColor} ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
{config.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ActionOutputPanel({ output, onDismiss }: { output: ActionOutput; onDismiss: () => void }) {
|
||||
if (!output.action) return null
|
||||
return (
|
||||
<div className={`rounded-lg border overflow-hidden ${output.ok ? "border-[var(--border)]" : "border-red-800"}`}>
|
||||
<div className={`flex items-center justify-between px-3 py-1.5 text-xs ${output.ok ? "text-green-400 bg-green-900/20" : "text-red-400 bg-red-900/20"}`}>
|
||||
<span>{ACTION_CONFIG[output.action]?.label ?? output.action} — {output.ok ? "ok" : "error"}</span>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
>
|
||||
dismiss
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 text-xs font-mono text-gray-300 overflow-x-auto max-h-64 overflow-y-auto bg-black/40">
|
||||
{output.text}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user