1"use client";
2
3
12import { useEffect, useState } from "react";
13import { Loader2, Plus, RefreshCw, Trash2 } from "lucide-react";
14import { workspaceApi } from "@/lib/api";
15import { inputCls } from "@/components/settings/settings-shell";
16import {
17 AUTOMATION_MAX_TRIGGERS,
18 AUTOMATION_TRIGGER_TYPES,
19 TRIGGER_LABELS,
20 type AutomationTriggerType,
21 type TriggerDraft
22} from "./lib";
23
24const SELECT_TYPES = new Set<AutomationTriggerType>(["RECORD_CREATED", "RECORD_UPDATED", "PROPERTY_CHANGED", "FORM_SUBMITTED"]);
25
26interface TriggerEditorProps {
27 triggers: TriggerDraft[];
28 onChange: (triggers: TriggerDraft[]) => void;
29 workspaceId: string;
30
31 onRegenerateWebhook?: () => void;
32 regenerating?: boolean;
33}
34
35export function TriggerEditor({ triggers, onChange, workspaceId, onRegenerateWebhook, regenerating }: TriggerEditorProps) {
36 const [databases, setDatabases] = useState<{ id: string; title: string }[]>([]);
37
38 useEffect(() => {
39 if (!workspaceId) return;
40 workspaceApi
41 .databases(workspaceId, { limit: 100 })
42 .then((dbs) => setDatabases(dbs.map((d) => ({ id: d.id, title: d.title }))))
43 .catch(() => setDatabases([]));
44 }, [workspaceId]);
45
46 const update = (i: number, patch: Partial<TriggerDraft>) => {
47 const next = [...triggers];
48 next[i] = { ...next[i]!, ...patch };
49 onChange(next);
50 };
51
52 const updateConfig = (i: number, key: string, value: string) => {
53 const t = triggers[i]!;
54 update(i, { config: { ...t.config, [key]: value } });
55 };
56
57 const add = () => {
58 if (triggers.length >= AUTOMATION_MAX_TRIGGERS) return;
59 onChange([...triggers, { type: "MANUAL", config: {} }]);
60 };
61
62 const remove = (i: number) => onChange(triggers.filter((_t, idx) => idx !== i));
63
64 return (
65 <div className="space-y-3">
66 {triggers.length === 0 && <p className="text-sm text-muted-foreground">No triggers yet — the automation only runs manually via the Run now button.</p>}
67 {triggers.map((t, i) => (
68 <div key={i} className="rounded-md border border-border p-3">
69 <div className="flex items-center gap-2">
70 <span className="text-xs font-medium text-muted-foreground">{i + 1}.</span>
71 <select
72 className="rounded-md border border-input bg-background px-2 py-1 text-sm"
73 value={t.type}
74 aria-label={`Trigger ${i + 1} type`}
75 onChange={(e) => update(i, { type: e.target.value as AutomationTriggerType, config: {} })}
76 >
77 {AUTOMATION_TRIGGER_TYPES.map((type) => (
78 <option key={type} value={type}>
79 {TRIGGER_LABELS[type]}
80 </option>
81 ))}
82 </select>
83 <span className="flex-1" />
84 <button
85 type="button"
86 className="rounded-md p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
87 onClick={() => remove(i)}
88 disabled={triggers.length <= 1}
89 aria-label={`Remove trigger ${i + 1}`}
90 >
91 <Trash2 className="size-4" />
92 </button>
93 </div>
94
95 <div className="mt-2.5 grid gap-2 sm:grid-cols-2">
96 {SELECT_TYPES.has(t.type) && (
97 <label className="block sm:col-span-2">
98 <span className="mb-1 block text-xs font-medium text-muted-foreground">Database (optional — all databases when empty)</span>
99 <input
100 className={inputCls}
101 list={`auto-db-${i}`}
102 placeholder="Database id (optional)"
103 value={String(t.config.databaseId ?? "")}
104 onChange={(e) => updateConfig(i, "databaseId", e.target.value)}
105 />
106 <datalist id={`auto-db-${i}`}>
107 {databases.map((d) => (
108 <option key={d.id} value={d.id}>
109 {d.title}
110 </option>
111 ))}
112 </datalist>
113 </label>
114 )}
115
116 {(t.type === "RECORD_UPDATED" || t.type === "PROPERTY_CHANGED") && (
117 <label className="block sm:col-span-2">
118 <span className="mb-1 block text-xs font-medium text-muted-foreground">
119 Property names {t.type === "PROPERTY_CHANGED" ? "(required, comma-separated)" : "(optional, comma-separated)"}
120 </span>
121 <input
122 className={inputCls}
123 placeholder="Status, Priority, …"
124 value={String(t.config.properties ?? "")}
125 onChange={(e) => updateConfig(i, "properties", e.target.value)}
126 />
127 </label>
128 )}
129
130 {t.type === "SCHEDULE" && (
131 <>
132 <label className="block">
133 <span className="mb-1 block text-xs font-medium text-muted-foreground">Cron expression (min interval 60 s)</span>
134 <input
135 className={inputCls}
136 placeholder="0 * * * *"
137 value={String(t.config.cron ?? "")}
138 onChange={(e) => updateConfig(i, "cron", e.target.value)}
139 />
140 </label>
141 <label className="block">
142 <span className="mb-1 block text-xs font-medium text-muted-foreground">Timezone (optional)</span>
143 <input
144 className={inputCls}
145 placeholder="UTC"
146 value={String(t.config.timezone ?? "")}
147 onChange={(e) => updateConfig(i, "timezone", e.target.value)}
148 />
149 </label>
150 </>
151 )}
152
153 {t.type === "WEBHOOK" && (
154 <div className="space-y-2 sm:col-span-2">
155 {t.config.webhookId ? (
156 <>
157 <div className="flex items-center gap-1.5">
158 <code className="min-w-0 flex-1 truncate rounded bg-muted px-2 py-1.5 text-xs">{String(t.config.url ?? "")}</code>
159 {onRegenerateWebhook && (
160 <button
161 type="button"
162 className="inline-flex shrink-0 items-center gap-1 rounded-md border px-2 py-1 text-xs disabled:opacity-50"
163 onClick={onRegenerateWebhook}
164 disabled={regenerating}
165 >
166 {regenerating ? <Loader2 className="size-3.5 animate-spin" /> : <RefreshCw className="size-3.5" />}
167 Regenerate secret
168 </button>
169 )}
170 </div>
171 <p className="text-[11px] text-muted-foreground">
172 POST to this URL with the <code>X-FS-Signature</code> header. webhookId: <code>{String(t.config.webhookId)}</code>
173 </p>
174 </>
175 ) : (
176 <p className="text-xs text-muted-foreground">The webhook URL + a one-time secret are generated when you save.</p>
177 )}
178 </div>
179 )}
180
181 {(t.type === "PAGE_CREATED" || t.type === "MANUAL") && (
182 <p className="text-xs text-muted-foreground sm:col-span-2">No extra configuration.</p>
183 )}
184 </div>
185 </div>
186 ))}
187
188 {triggers.length < AUTOMATION_MAX_TRIGGERS && (
189 <button
190 type="button"
191 data-testid="automation-trigger-add"
192 className="inline-flex items-center gap-1.5 rounded-md border border-dashed px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
193 onClick={add}
194 >
195 <Plus className="size-4" /> Add trigger
196 </button>
197 )}
198 </div>
199 );
200}
201