update
This commit is contained in:
1 parent
65e85135c1
commit
6f1de8047e
64 files changed
+1269
-515
No files matched your search
@@ -15,8 +15,8 @@ function DeleteConfirmDialog({ open, entityName, onConfirm, onCancel }: { open:
|
||||
if (!open) return null
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onCancel}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[380px] rounded-lg border bg-card p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="mb-1 flex items-center gap-2"><div className="flex h-8 w-8 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/50"><Trash size={16} className="text-destructive" /></div><h3 className="text-[15px] font-bold text-foreground">Delete</h3></div>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[min(380px,90vw)] rounded-lg border bg-card p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="mb-1 flex items-center gap-2"><div className="flex h-8 w-8 items-center justify-center rounded-full bg-destructive-muted"><Trash size={16} className="text-destructive" /></div><h3 className="text-[15px] font-bold text-foreground">Delete</h3></div>
|
||||
<p className="mt-2 text-[13px] leading-relaxed text-muted-foreground">Are you sure you want to delete <strong className="text-foreground">{entityName}</strong>? This action cannot be undone.</p>
|
||||
<div className="mt-5 flex justify-end gap-3"><button type="button" onClick={onCancel} className="rounded-lg px-4 py-2 text-[12px] font-medium text-muted-foreground transition hover:bg-muted">Cancel</button><ActionButton label="Delete" onClick={onConfirm} icon={Trash} tone="danger" /></div>
|
||||
</motion.div>
|
||||
@@ -41,15 +41,15 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
|
||||
|
||||
useEffect(() => { void window.agentCompany.getAutomationLog(companyId).then((r) => { if (r.ok) setActLog(r.data) }) }, [companyId, rules])
|
||||
const filtered = useMemo(() => filterStatus === "all" ? rules : rules.filter((r) => r.status === filterStatus), [rules, filterStatus])
|
||||
const counts = useMemo(() => ({ total: rules.length, active: rules.filter((r) => r.status === "active").length, paused: rules.filter((r) => r.status === "paused").length, error: rules.filter((r) => r.status === "error").length }), [rules])
|
||||
const counts = useMemo(() => { let active = 0, paused = 0, error = 0; for (const r of rules) { if (r.status === "active") active++; else if (r.status === "paused") paused++; else if (r.status === "error") error++; } return { total: rules.length, active, paused, error } }, [rules])
|
||||
const handleToggle = useCallback(async (rule: AutomationRuleRecord) => { setActionError(null); try { const r = await window.agentCompany.toggleAutomationRule({ id: rule.id, companyId, status: (rule.status === "active" ? "paused" : "active") as "active"|"paused" }); if (!r.ok) { setActionError(r.error.message); return }; onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed") } }, [companyId, onRefresh])
|
||||
const handleDelete = useCallback(async () => { if (!delTarget) return; setActionError(null); try { const r = await window.agentCompany.deleteAutomationRule({ id: delTarget.id, companyId }); if (!r.ok) { setActionError(r.error.message); setDelTarget(null); return }; setDelTarget(null); onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed"); setDelTarget(null) } }, [delTarget, companyId, onRefresh])
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{actionError ? <div className="flex items-center justify-between rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
|
||||
{actionError ? <div className="flex items-center justify-between rounded-lg bg-destructive-muted px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-foreground">Automation Rules</h2><p className="text-[12px] text-muted-foreground">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
|
||||
{rules.length > 0 ? <div className="grid grid-cols-3 gap-2">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--primary)" }].map((s) => <div key={s.l} className="rounded-lg border bg-card px-4 py-3.5" style={{ borderColor: LINE }}><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{s.l}</div><div className="text-[26px] font-bold tabular-nums" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
|
||||
{rules.length > 0 ? <div className="grid grid-cols-1 gap-2 sm:grid-cols-3">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--primary)" }].map((s) => <div key={s.l} className="rounded-lg border bg-card px-4 py-3.5" style={{ borderColor: LINE }}><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{s.l}</div><div className="text-[26px] font-bold tabular-nums" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
|
||||
<div className="flex gap-1 rounded-lg bg-muted p-1"><button type="button" onClick={() => setActiveTab("rules")} className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "rules" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}>Rules ({rules.length})</button><button type="button" onClick={() => setActiveTab("activity")} className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "activity" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}>Activity Log ({actLog.length})</button></div>
|
||||
{activeTab === "rules" ? <>
|
||||
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
|
||||
@@ -71,7 +71,7 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl
|
||||
const counts = useMemo(() => ({ total: workflows.length, active: workflows.filter((w) => w.status === "active").length, draft: workflows.filter((w) => w.status === "draft").length }), [workflows])
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{actionError ? <div className="flex items-center justify-between rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
|
||||
{actionError ? <div className="flex items-center justify-between rounded-lg bg-destructive-muted px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-foreground">Workflow Pipelines</h2><p className="text-[12px] text-muted-foreground">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
|
||||
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-muted-foreground" /><div className="text-[14px] font-medium text-muted-foreground">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
|
||||
: <div className="space-y-2"><AnimatePresence>{workflows.map((wf) => <WorkflowCard key={wf.id} workflow={wf} isRunning={runningId === wf.id} onDelete={() => setDelTarget(wf)} onEdit={() => setEditWf(wf)} onRun={() => void handleRun(wf)} />)}</AnimatePresence></div>}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: Automatio
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button type="button" onClick={onToggle} aria-label={rule.status === "active" ? "Pause rule" : "Activate rule"} title={rule.status === "active" ? "Pause rule" : "Activate rule"} className="rounded-lg p-2 text-muted-foreground transition hover:bg-muted hover:text-foreground">{rule.status === "active" ? <Pause size={15} /> : <Play size={15} />}</button>
|
||||
<button type="button" onClick={onDelete} aria-label="Delete rule" title="Delete rule" className="rounded-lg p-2 text-muted-foreground opacity-0 transition hover:bg-red-100 dark:bg-red-900/50 hover:text-destructive group-hover:opacity-100"><Trash size={15} /></button>
|
||||
<button type="button" onClick={onDelete} aria-label="Delete rule" title="Delete rule" className="rounded-lg p-2 text-muted-foreground opacity-0 transition hover:bg-destructive-muted hover:text-destructive group-hover:opacity-100"><Trash size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { Gear, Lightning, X } from "@phosphor-icons/react"
|
||||
import type { AutomationAction, AutomationRuleRecord, AutomationTrigger, DepartmentId } from "@shared/types"
|
||||
import { ActionButton, Select } from "../ui"
|
||||
import { ActionButton, Select, useFocusTrap } from "../ui"
|
||||
import { ACCENT, LINE, triggerConfig, actionConfig, departmentOptions, safeJsonStringify } from "./automation-config"
|
||||
|
||||
interface RuleFormState { name: string; description: string; trigger: AutomationTrigger; conditionsJson: string; action: AutomationAction; actionConfigJson: string; sourceDepartment: string; targetDepartment: string; priority: number }
|
||||
@@ -20,18 +20,20 @@ export function RuleFormModal({ open, editingRule, companyId, onClose, onSaved }
|
||||
setSaving(true); setError(null)
|
||||
try { const result = await window.agentCompany.saveAutomationRule({ id: editingRule?.id ?? null, companyId, name: form.name.trim(), description: form.description.trim(), trigger: form.trigger, conditionsJson: form.conditionsJson, action: form.action, actionConfigJson: form.actionConfigJson, sourceDepartment: (form.sourceDepartment || null) as DepartmentId | null, targetDepartment: (form.targetDepartment || null) as DepartmentId | null, priority: form.priority, status: editingRule?.status ?? "active" }); if (!result.ok) { setError(result.error.message); return }; onSaved(); onClose() } catch (err) { setError(err instanceof Error ? err.message : "Failed to save rule") } finally { setSaving(false) }
|
||||
}, [form, editingRule, companyId, onSaved, onClose])
|
||||
const trapRef = useFocusTrap(open)
|
||||
useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey) }, [open, onClose])
|
||||
if (!open) return null
|
||||
const triggerOptions = Object.entries(triggerConfig).map(([key, c]) => ({ value: key, label: c.label })); const actionOptions = Object.entries(actionConfig).map(([key, c]) => ({ value: key, label: c.label }))
|
||||
const selectedTriggerInfo = triggerConfig[form.trigger]; const selectedActionInfo = actionConfig[form.action]
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[580px] flex-col rounded-lg border bg-card shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<motion.div ref={trapRef} role="dialog" aria-modal="true" aria-label={isEditing ? "Edit Automation Rule" : "New Automation Rule"} initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[min(580px,90vw)] flex-col rounded-lg border bg-card shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}>
|
||||
<div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-foreground">{isEditing ? "Edit Automation Rule" : "New Automation Rule"}</h2></div>
|
||||
<button type="button" onClick={onClose} className="rounded-lg p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground" aria-label="Close"><X size={16} /></button>
|
||||
</div>
|
||||
<div className="overflow-y-auto px-6 py-5"><div className="space-y-5">
|
||||
{error ? <div className="rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive">{error}</div> : null}
|
||||
{error ? <div className="rounded-lg bg-destructive-muted px-3.5 py-2.5 text-[12px] font-medium text-destructive">{error}</div> : null}
|
||||
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Rule Name</label><input type="text" value={form.name} onChange={(e) => updateField("name", e.target.value)} placeholder="e.g., Auto-assign blocked tasks" className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
|
||||
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Description</label><textarea value={form.description} onChange={(e) => updateField("description", e.target.value)} placeholder="What does this rule do?" rows={2} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
|
||||
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Trigger</label><Select value={form.trigger} onChange={(v) => updateField("trigger", v as AutomationTrigger)} options={triggerOptions} />{selectedTriggerInfo ? <div className="mt-1.5 text-[11px] text-muted-foreground">{selectedTriggerInfo.description}</div> : null}</div>
|
||||
@@ -39,7 +41,7 @@ export function RuleFormModal({ open, editingRule, companyId, onClose, onSaved }
|
||||
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Action</label><Select value={form.action} onChange={(v) => updateField("action", v as AutomationAction)} options={actionOptions} />{selectedActionInfo ? <div className="mt-1.5 text-[11px] text-muted-foreground">{selectedActionInfo.description}</div> : null}</div>
|
||||
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Action Configuration (JSON)</label><textarea value={form.actionConfigJson} onChange={(e) => updateField("actionConfigJson", e.target.value)} rows={3} spellCheck={false} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 font-mono text-[12px] text-foreground" style={{ borderColor: LINE }} placeholder='{"targetAgentId": "..."}' /></div>
|
||||
<div className="grid grid-cols-2 gap-3"><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Source Department</label><Select value={form.sourceDepartment} onChange={(v) => updateField("sourceDepartment", v)} options={departmentOptions} /></div><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Target Department</label><Select value={form.targetDepartment} onChange={(v) => updateField("targetDepartment", v)} options={departmentOptions} /></div></div>
|
||||
<div><div className="mb-1.5 flex items-center justify-between"><label className="text-[12px] font-medium text-muted-foreground">Priority</label><span className="text-[12px] font-semibold" style={{ color: ACCENT }}>{form.priority}</span></div><input type="range" min={0} max={100} value={form.priority} onChange={(e) => updateField("priority", parseInt(e.target.value, 10))} className="w-full accent-[#d97745]" /><div className="mt-1 flex justify-between text-[10px] text-muted-foreground"><span>Low priority</span><span>High priority</span></div></div>
|
||||
<div><div className="mb-1.5 flex items-center justify-between"><label className="text-[12px] font-medium text-muted-foreground">Priority</label><span className="text-[12px] font-semibold" style={{ color: ACCENT }}>{form.priority}</span></div><input type="range" min={0} max={100} value={form.priority} onChange={(e) => updateField("priority", parseInt(e.target.value, 10))} className="w-full accent-[color:var(--primary)]" /><div className="mt-1 flex justify-between text-[10px] text-muted-foreground"><span>Low priority</span><span>High priority</span></div></div>
|
||||
</div></div>
|
||||
<div className="flex shrink-0 justify-end gap-3 border-t px-6 py-4" style={{ borderColor: LINE }}>
|
||||
<button type="button" onClick={onClose} className="rounded-lg px-4 py-2 text-[12px] font-medium text-muted-foreground transition hover:bg-muted hover:text-foreground">Cancel</button>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { ArrowsClockwise, Gear, X } from "@phosphor-icons/react"
|
||||
import type { AutomationTrigger, WorkflowPipelineRecord, WorkflowStatus } from "@shared/types"
|
||||
import { ActionButton, Select } from "../ui"
|
||||
import { ActionButton, Select, useFocusTrap } from "../ui"
|
||||
import { ACCENT, LINE, triggerConfig, safeJsonStringify } from "./automation-config"
|
||||
|
||||
interface WorkflowFormState { name: string; description: string; triggerType: AutomationTrigger; triggerConfigJson: string; status: WorkflowStatus; stepsJson: string }
|
||||
@@ -20,15 +20,17 @@ export function WorkflowFormModal({ open, editingWorkflow, companyId, onClose, o
|
||||
setSaving(true); setError(null)
|
||||
try { const result = await window.agentCompany.saveWorkflow({ id: editingWorkflow?.id ?? null, companyId, name: form.name.trim(), description: form.description.trim(), triggerType: form.triggerType, triggerConfigJson: form.triggerConfigJson, status: form.status, stepsJson: form.stepsJson }); if (!result.ok) { setError(result.error.message); return }; onSaved(); onClose() } catch (err) { setError(err instanceof Error ? err.message : "Failed to save workflow") } finally { setSaving(false) }
|
||||
}, [form, editingWorkflow, companyId, onSaved, onClose])
|
||||
const trapRef = useFocusTrap(open)
|
||||
useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey) }, [open, onClose])
|
||||
if (!open) return null
|
||||
const triggerOptions = Object.entries(triggerConfig).map(([key, c]) => ({ value: key, label: c.label }))
|
||||
const statusOptions = [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }, { value: "paused", label: "Paused" }]
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[540px] flex-col rounded-lg border bg-card shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<motion.div ref={trapRef} role="dialog" aria-modal="true" aria-label={isEditing ? "Edit Workflow" : "New Workflow"} initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[min(540px,90vw)] flex-col rounded-lg border bg-card shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}><div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><ArrowsClockwise size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-foreground">{isEditing ? "Edit Workflow" : "New Workflow"}</h2></div><button type="button" onClick={onClose} className="rounded-lg p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground" aria-label="Close"><X size={16} /></button></div>
|
||||
<div className="overflow-y-auto px-6 py-5"><div className="space-y-5">
|
||||
{error ? <div className="rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive">{error}</div> : null}
|
||||
{error ? <div className="rounded-lg bg-destructive-muted px-3.5 py-2.5 text-[12px] font-medium text-destructive">{error}</div> : null}
|
||||
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Workflow Name</label><input type="text" value={form.name} onChange={(e) => updateField("name", e.target.value)} placeholder="e.g., Bug Fix Pipeline" className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
|
||||
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Description</label><textarea value={form.description} onChange={(e) => updateField("description", e.target.value)} placeholder="Describe this workflow pipeline" rows={2} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
|
||||
<div className="grid grid-cols-2 gap-3"><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Trigger</label><Select value={form.triggerType} onChange={(v) => updateField("triggerType", v as AutomationTrigger)} options={triggerOptions} /></div><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Status</label><Select value={form.status} onChange={(v) => updateField("status", v as WorkflowStatus)} options={statusOptions} /></div></div>
|
||||
|
||||
@@ -19,7 +19,7 @@ function StepTimeline({ steps }: { steps: WorkflowStep[] }) {
|
||||
<div className="absolute -left-5 top-1.5 z-10 h-[10px] w-[10px] rounded-full border-2" style={{ backgroundColor: colors.bg, borderColor: colors.border }} />
|
||||
<div className="flex-1 rounded-lg border px-3 py-2.5" style={{ borderColor: step.status === "running" ? ACCENT : LINE }}>
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2"><span className="text-[11px] font-bold" style={{ color: colors.text }}>{idx + 1}</span><span className="text-[12px] font-medium text-foreground">{step.name}</span></div><span className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}>{step.status}</span></div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground"><span>{info?.label ?? step.action}</span>{step.assigneeDepartment ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{step.assigneeDepartment}</span></> : null}{step.requiresApproval ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span className="text-yellow-600 dark:text-yellow-400">Requires approval</span></> : null}{step.timeoutSec > 0 ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{step.timeoutSec}s timeout</span></> : null}</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground"><span>{info?.label ?? step.action}</span>{step.assigneeDepartment ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{step.assigneeDepartment}</span></> : null}{step.requiresApproval ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span className="text-warning">Requires approval</span></> : null}{step.timeoutSec > 0 ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{step.timeoutSec}s timeout</span></> : null}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -44,9 +44,9 @@ function WorkflowCard({ workflow, isRunning, onDelete, onEdit, onRun }: { workfl
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<motion.div animate={{ rotate: expanded ? 90 : 0 }} transition={{ duration: 0.15 }}><button type="button" onClick={() => setExpanded((p) => !p)} aria-label={expanded ? "Collapse" : "Expand"} className="rounded-lg p-2 text-muted-foreground transition hover:bg-muted hover:text-foreground"><CaretRight size={14} /></button></motion.div>
|
||||
<button type="button" onClick={onRun} aria-label="Run workflow" disabled={isRunning} className="rounded-lg p-2 text-muted-foreground transition hover:bg-green-100 dark:bg-green-900/50 hover:text-green-600 dark:text-green-400 disabled:pointer-events-none disabled:opacity-50">{isRunning ? <ArrowsClockwise size={15} className="animate-spin" /> : <Play size={15} />}</button>
|
||||
<button type="button" onClick={onRun} aria-label="Run workflow" disabled={isRunning} className="rounded-lg p-2 text-muted-foreground transition hover:bg-success-muted hover:text-success disabled:pointer-events-none disabled:opacity-50">{isRunning ? <ArrowsClockwise size={15} className="animate-spin" /> : <Play size={15} />}</button>
|
||||
<button type="button" onClick={onEdit} aria-label="Edit workflow" className="rounded-lg p-2 text-muted-foreground transition hover:bg-muted hover:text-foreground"><Gear size={15} /></button>
|
||||
<button type="button" onClick={onDelete} aria-label="Delete workflow" className="rounded-lg p-2 text-muted-foreground opacity-0 transition hover:bg-red-100 dark:bg-red-900/50 hover:text-destructive group-hover:opacity-100"><Trash size={15} /></button>
|
||||
<button type="button" onClick={onDelete} aria-label="Delete workflow" className="rounded-lg p-2 text-muted-foreground opacity-0 transition hover:bg-destructive-muted hover:text-destructive group-hover:opacity-100"><Trash size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
|
||||
Reference in new issue
Block a user