@agent-native/dispatch 0.32.0 → 0.33.0-nightly-20260831223557
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/ask_app.js +0 -1
- package/dist/actions/ask_app.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +8 -8
- package/dist/components/automation-create-dialog.d.ts +16 -0
- package/dist/components/automation-create-dialog.d.ts.map +1 -0
- package/dist/components/automation-create-dialog.js +213 -0
- package/dist/components/automation-create-dialog.js.map +1 -0
- package/dist/components/automation-details-panel.d.ts.map +1 -1
- package/dist/components/automation-details-panel.js +35 -4
- package/dist/components/automation-details-panel.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +1 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +5 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/automation-display.d.ts.map +1 -1
- package/dist/lib/automation-display.js +4 -0
- package/dist/lib/automation-display.js.map +1 -1
- package/dist/lib/automations.d.ts +2 -1
- package/dist/lib/automations.d.ts.map +1 -1
- package/dist/lib/automations.js.map +1 -1
- package/dist/routes/pages/automations.d.ts.map +1 -1
- package/dist/routes/pages/automations.js +16 -24
- package/dist/routes/pages/automations.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +27 -3
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/ask_app.ts +0 -1
- package/src/components/automation-create-dialog.tsx +759 -0
- package/src/components/automation-details-panel.tsx +80 -12
- package/src/hooks/use-navigation-state.spec.ts +13 -0
- package/src/hooks/use-navigation-state.ts +6 -0
- package/src/lib/automation-display.ts +2 -0
- package/src/lib/automations.ts +2 -1
- package/src/routes/pages/automations.spec.tsx +11 -0
- package/src/routes/pages/automations.tsx +27 -57
- package/src/server/lib/mcp-gateway.spec.ts +37 -2
- package/src/server/lib/mcp-gateway.ts +40 -3
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_AUTOMATION_SCHEDULE_DRAFT,
|
|
3
|
+
AUTOMATION_WEEKDAYS,
|
|
4
|
+
automationScheduleToCron,
|
|
5
|
+
formatAutomationTime,
|
|
6
|
+
type AutomationScheduleDraft,
|
|
7
|
+
type AutomationSchedulePreset,
|
|
8
|
+
type AutomationScheduleUnit,
|
|
9
|
+
} from "@agent-native/core/client/agent-page/automation-schedule";
|
|
10
|
+
import {
|
|
11
|
+
TimezoneSelect,
|
|
12
|
+
browserTimezone,
|
|
13
|
+
} from "@agent-native/core/client/agent-page/timezone-select";
|
|
14
|
+
import {
|
|
15
|
+
useActionMutation,
|
|
16
|
+
useActionQuery,
|
|
17
|
+
} from "@agent-native/core/client/hooks";
|
|
18
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
19
|
+
import {
|
|
20
|
+
IconBolt,
|
|
21
|
+
IconCalendarEvent,
|
|
22
|
+
IconClock,
|
|
23
|
+
IconCode,
|
|
24
|
+
IconLoader2,
|
|
25
|
+
} from "@tabler/icons-react";
|
|
26
|
+
import { useEffect, useState } from "react";
|
|
27
|
+
|
|
28
|
+
import { Button } from "./ui/button";
|
|
29
|
+
import {
|
|
30
|
+
Dialog,
|
|
31
|
+
DialogContent,
|
|
32
|
+
DialogDescription,
|
|
33
|
+
DialogFooter,
|
|
34
|
+
DialogHeader,
|
|
35
|
+
DialogTitle,
|
|
36
|
+
} from "./ui/dialog";
|
|
37
|
+
import { Input } from "./ui/input";
|
|
38
|
+
import {
|
|
39
|
+
Select,
|
|
40
|
+
SelectContent,
|
|
41
|
+
SelectItem,
|
|
42
|
+
SelectTrigger,
|
|
43
|
+
SelectValue,
|
|
44
|
+
} from "./ui/select";
|
|
45
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
|
46
|
+
import { Textarea } from "./ui/textarea";
|
|
47
|
+
|
|
48
|
+
type TriggerType = "schedule" | "event" | "webhook";
|
|
49
|
+
|
|
50
|
+
interface AutomationEvent {
|
|
51
|
+
name: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CreateAutomationInput {
|
|
56
|
+
operation: "create";
|
|
57
|
+
name: string;
|
|
58
|
+
scope: "personal" | "organization";
|
|
59
|
+
triggerType: TriggerType;
|
|
60
|
+
body: string;
|
|
61
|
+
schedule?: string;
|
|
62
|
+
timezone?: string;
|
|
63
|
+
event?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface CreateAutomationResult {
|
|
67
|
+
created: true;
|
|
68
|
+
name: string;
|
|
69
|
+
triggerType: TriggerType;
|
|
70
|
+
webhookPath: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PRESETS: {
|
|
74
|
+
value: AutomationSchedulePreset;
|
|
75
|
+
labelKey: string;
|
|
76
|
+
detailKey: string;
|
|
77
|
+
label: string;
|
|
78
|
+
detail: string;
|
|
79
|
+
draft: Partial<AutomationScheduleDraft>;
|
|
80
|
+
}[] = [
|
|
81
|
+
{
|
|
82
|
+
value: "hourly",
|
|
83
|
+
labelKey: "jobs.schedulePreset.hourly",
|
|
84
|
+
detailKey: "jobs.schedulePreset.hourlyDetail",
|
|
85
|
+
label: "Every hour",
|
|
86
|
+
detail: "At the top of the hour",
|
|
87
|
+
draft: { preset: "hourly" },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
value: "daily-midnight",
|
|
91
|
+
labelKey: "jobs.schedulePreset.dailyMidnight",
|
|
92
|
+
detailKey: "jobs.schedulePreset.dailyMidnightDetail",
|
|
93
|
+
label: "Every day at midnight",
|
|
94
|
+
detail: "12:00 AM",
|
|
95
|
+
draft: { preset: "daily-midnight" },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
value: "daily-noon",
|
|
99
|
+
labelKey: "jobs.schedulePreset.dailyNoon",
|
|
100
|
+
detailKey: "jobs.schedulePreset.dailyNoonDetail",
|
|
101
|
+
label: "Every day at noon",
|
|
102
|
+
detail: "12:00 PM",
|
|
103
|
+
draft: { preset: "daily-noon" },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
value: "weekdays",
|
|
107
|
+
labelKey: "jobs.schedulePreset.weekdays",
|
|
108
|
+
detailKey: "jobs.schedulePreset.weekdaysDetail",
|
|
109
|
+
label: "Every weekday",
|
|
110
|
+
detail: "Monday to Friday at 9:00 AM",
|
|
111
|
+
draft: { preset: "weekdays", time: "09:00" },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
value: "weekly",
|
|
115
|
+
labelKey: "jobs.schedulePreset.weekly",
|
|
116
|
+
detailKey: "jobs.schedulePreset.weeklyDetail",
|
|
117
|
+
label: "Every week",
|
|
118
|
+
detail: "Sunday at 9:00 AM",
|
|
119
|
+
draft: { preset: "weekly", weekday: 0, time: "09:00" },
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
function presetClass(selected: boolean): string {
|
|
124
|
+
return (
|
|
125
|
+
"rounded-lg border px-3 py-2 text-left transition-colors " +
|
|
126
|
+
(selected
|
|
127
|
+
? "border-primary bg-primary/5 text-foreground"
|
|
128
|
+
: "border-border/70 bg-background hover:bg-muted/50")
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function scheduleIsValid(value: string): boolean {
|
|
133
|
+
return value.trim().split(/\s+/).length === 5;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ScheduleFields({
|
|
137
|
+
draft,
|
|
138
|
+
saving,
|
|
139
|
+
onChange,
|
|
140
|
+
}: {
|
|
141
|
+
draft: AutomationScheduleDraft;
|
|
142
|
+
saving: boolean;
|
|
143
|
+
onChange: (patch: Partial<AutomationScheduleDraft>) => void;
|
|
144
|
+
}) {
|
|
145
|
+
const t = useT();
|
|
146
|
+
const customMinute = Number(draft.time.split(":")[1] ?? 0);
|
|
147
|
+
return (
|
|
148
|
+
<div className="space-y-3 rounded-lg border border-border/70 bg-muted/20 p-3">
|
|
149
|
+
<div className="grid gap-3 sm:grid-cols-[1fr_1fr]">
|
|
150
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
151
|
+
<span>{t("jobs.repeatEvery", { defaultValue: "Repeat every" })}</span>
|
|
152
|
+
<Input
|
|
153
|
+
type="number"
|
|
154
|
+
min={1}
|
|
155
|
+
max={31}
|
|
156
|
+
value={draft.interval}
|
|
157
|
+
disabled={saving}
|
|
158
|
+
onChange={(event) =>
|
|
159
|
+
onChange({
|
|
160
|
+
interval: Math.min(
|
|
161
|
+
draft.unit === "day" ? 1 : 31,
|
|
162
|
+
Math.max(1, Number(event.target.value) || 1),
|
|
163
|
+
),
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
className="h-9 bg-background text-sm"
|
|
167
|
+
/>
|
|
168
|
+
</label>
|
|
169
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
170
|
+
<span>{t("jobs.scheduleUnit", { defaultValue: "Unit" })}</span>
|
|
171
|
+
<Select
|
|
172
|
+
value={draft.unit}
|
|
173
|
+
disabled={saving}
|
|
174
|
+
onValueChange={(value) =>
|
|
175
|
+
onChange({
|
|
176
|
+
unit: value as AutomationScheduleUnit,
|
|
177
|
+
...(value === "day" ? { interval: 1 } : {}),
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
>
|
|
181
|
+
<SelectTrigger className="h-9 bg-background text-sm">
|
|
182
|
+
<SelectValue />
|
|
183
|
+
</SelectTrigger>
|
|
184
|
+
<SelectContent>
|
|
185
|
+
<SelectItem value="hour">
|
|
186
|
+
{t("jobs.hours", { defaultValue: "hour(s)" })}
|
|
187
|
+
</SelectItem>
|
|
188
|
+
<SelectItem value="day">
|
|
189
|
+
{t("jobs.day", { defaultValue: "day" })}
|
|
190
|
+
</SelectItem>
|
|
191
|
+
<SelectItem value="week">
|
|
192
|
+
{t("jobs.weeks", { defaultValue: "week(s)" })}
|
|
193
|
+
</SelectItem>
|
|
194
|
+
<SelectItem value="month">
|
|
195
|
+
{t("jobs.months", { defaultValue: "month(s)" })}
|
|
196
|
+
</SelectItem>
|
|
197
|
+
</SelectContent>
|
|
198
|
+
</Select>
|
|
199
|
+
</label>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{draft.unit === "hour" ? (
|
|
203
|
+
<label className="block space-y-1.5 text-xs text-muted-foreground">
|
|
204
|
+
<span>{t("jobs.atMinute", { defaultValue: "At minute" })}</span>
|
|
205
|
+
<Input
|
|
206
|
+
type="number"
|
|
207
|
+
min={0}
|
|
208
|
+
max={59}
|
|
209
|
+
value={customMinute}
|
|
210
|
+
disabled={saving}
|
|
211
|
+
onChange={(event) =>
|
|
212
|
+
onChange({
|
|
213
|
+
time:
|
|
214
|
+
"00:" +
|
|
215
|
+
String(
|
|
216
|
+
Math.min(59, Math.max(0, Number(event.target.value) || 0)),
|
|
217
|
+
).padStart(2, "0"),
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
className="h-9 bg-background text-sm"
|
|
221
|
+
/>
|
|
222
|
+
</label>
|
|
223
|
+
) : null}
|
|
224
|
+
|
|
225
|
+
{draft.unit === "week" ? (
|
|
226
|
+
<label className="block space-y-1.5 text-xs text-muted-foreground">
|
|
227
|
+
<span>{t("jobs.onDay", { defaultValue: "On" })}</span>
|
|
228
|
+
<Select
|
|
229
|
+
value={String(draft.weekday)}
|
|
230
|
+
disabled={saving}
|
|
231
|
+
onValueChange={(value) => onChange({ weekday: Number(value) })}
|
|
232
|
+
>
|
|
233
|
+
<SelectTrigger className="h-9 bg-background text-sm">
|
|
234
|
+
<SelectValue />
|
|
235
|
+
</SelectTrigger>
|
|
236
|
+
<SelectContent>
|
|
237
|
+
{AUTOMATION_WEEKDAYS.map((day) => (
|
|
238
|
+
<SelectItem key={day.value} value={String(day.value)}>
|
|
239
|
+
{day.short}
|
|
240
|
+
</SelectItem>
|
|
241
|
+
))}
|
|
242
|
+
</SelectContent>
|
|
243
|
+
</Select>
|
|
244
|
+
</label>
|
|
245
|
+
) : null}
|
|
246
|
+
|
|
247
|
+
{draft.unit === "month" ? (
|
|
248
|
+
<label className="block space-y-1.5 text-xs text-muted-foreground">
|
|
249
|
+
<span>{t("jobs.dayOfMonth", { defaultValue: "Day of month" })}</span>
|
|
250
|
+
<Input
|
|
251
|
+
type="number"
|
|
252
|
+
min={1}
|
|
253
|
+
max={31}
|
|
254
|
+
value={draft.monthDay}
|
|
255
|
+
disabled={saving}
|
|
256
|
+
onChange={(event) =>
|
|
257
|
+
onChange({
|
|
258
|
+
monthDay: Math.min(
|
|
259
|
+
31,
|
|
260
|
+
Math.max(1, Number(event.target.value) || 1),
|
|
261
|
+
),
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
className="h-9 bg-background text-sm"
|
|
265
|
+
/>
|
|
266
|
+
</label>
|
|
267
|
+
) : null}
|
|
268
|
+
|
|
269
|
+
{draft.unit !== "hour" ? (
|
|
270
|
+
<label className="block space-y-1.5 text-xs text-muted-foreground">
|
|
271
|
+
<span>{t("jobs.atTime", { defaultValue: "At" })}</span>
|
|
272
|
+
<Input
|
|
273
|
+
type="time"
|
|
274
|
+
value={draft.time}
|
|
275
|
+
disabled={saving}
|
|
276
|
+
onChange={(event) => onChange({ time: event.target.value })}
|
|
277
|
+
className="h-9 bg-background text-sm"
|
|
278
|
+
/>
|
|
279
|
+
</label>
|
|
280
|
+
) : null}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function ScheduleBuilder({
|
|
286
|
+
saving,
|
|
287
|
+
timezone,
|
|
288
|
+
onTimezoneChange,
|
|
289
|
+
onChange,
|
|
290
|
+
}: {
|
|
291
|
+
saving: boolean;
|
|
292
|
+
timezone: string;
|
|
293
|
+
onTimezoneChange: (value: string) => void;
|
|
294
|
+
onChange: (value: string) => void;
|
|
295
|
+
}) {
|
|
296
|
+
const t = useT();
|
|
297
|
+
const [draft, setDraft] = useState<AutomationScheduleDraft>(
|
|
298
|
+
DEFAULT_AUTOMATION_SCHEDULE_DRAFT,
|
|
299
|
+
);
|
|
300
|
+
const [advanced, setAdvanced] = useState(false);
|
|
301
|
+
const [advancedValue, setAdvancedValue] = useState("0 * * * *");
|
|
302
|
+
const friendly = automationScheduleToCron(draft);
|
|
303
|
+
const activeSchedule = advanced
|
|
304
|
+
? advancedValue.trim()
|
|
305
|
+
: (friendly.schedule ?? "");
|
|
306
|
+
const valid =
|
|
307
|
+
scheduleIsValid(activeSchedule) && (!friendly.error || advanced);
|
|
308
|
+
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
onChange(valid ? activeSchedule : "");
|
|
311
|
+
}, [activeSchedule, onChange, valid]);
|
|
312
|
+
|
|
313
|
+
function choosePreset(
|
|
314
|
+
value: AutomationSchedulePreset,
|
|
315
|
+
presetDraft: Partial<AutomationScheduleDraft>,
|
|
316
|
+
) {
|
|
317
|
+
setDraft((current) => ({ ...current, ...presetDraft, preset: value }));
|
|
318
|
+
setAdvanced(false);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className="space-y-3">
|
|
323
|
+
<div className="space-y-2">
|
|
324
|
+
<div className="text-xs font-medium text-foreground">
|
|
325
|
+
{t("jobs.repeat", { defaultValue: "Repeat" })}
|
|
326
|
+
</div>
|
|
327
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
328
|
+
{PRESETS.map((preset) => (
|
|
329
|
+
<button
|
|
330
|
+
key={preset.value}
|
|
331
|
+
type="button"
|
|
332
|
+
aria-pressed={!advanced && draft.preset === preset.value}
|
|
333
|
+
disabled={saving}
|
|
334
|
+
className={presetClass(
|
|
335
|
+
!advanced && draft.preset === preset.value,
|
|
336
|
+
)}
|
|
337
|
+
onClick={() => choosePreset(preset.value, preset.draft)}
|
|
338
|
+
>
|
|
339
|
+
<span className="block text-sm font-medium">
|
|
340
|
+
{t(preset.labelKey, { defaultValue: preset.label })}
|
|
341
|
+
</span>
|
|
342
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
343
|
+
{t(preset.detailKey, { defaultValue: preset.detail })}
|
|
344
|
+
</span>
|
|
345
|
+
</button>
|
|
346
|
+
))}
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
aria-pressed={!advanced && draft.preset === "custom"}
|
|
350
|
+
disabled={saving}
|
|
351
|
+
className={presetClass(!advanced && draft.preset === "custom")}
|
|
352
|
+
onClick={() =>
|
|
353
|
+
choosePreset("custom", {
|
|
354
|
+
preset: "custom",
|
|
355
|
+
unit: draft.unit || "day",
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
>
|
|
359
|
+
<span className="block text-sm font-medium">
|
|
360
|
+
{t("jobs.schedulePreset.custom", { defaultValue: "Custom" })}
|
|
361
|
+
</span>
|
|
362
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
363
|
+
{t("jobs.schedulePreset.customDetail", {
|
|
364
|
+
defaultValue: "Set your own repeat pattern",
|
|
365
|
+
})}
|
|
366
|
+
</span>
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{!advanced && draft.preset === "custom" ? (
|
|
372
|
+
<ScheduleFields
|
|
373
|
+
draft={draft}
|
|
374
|
+
saving={saving}
|
|
375
|
+
onChange={(patch) =>
|
|
376
|
+
setDraft((current) => ({ ...current, ...patch }))
|
|
377
|
+
}
|
|
378
|
+
/>
|
|
379
|
+
) : null}
|
|
380
|
+
|
|
381
|
+
{!advanced && friendly.schedule ? (
|
|
382
|
+
<p className="text-xs text-muted-foreground">
|
|
383
|
+
{t("jobs.schedulePreview", {
|
|
384
|
+
defaultValue: "Runs {{time}}.",
|
|
385
|
+
time:
|
|
386
|
+
draft.preset === "hourly"
|
|
387
|
+
? t("jobs.schedulePreview.hourly", {
|
|
388
|
+
defaultValue: "at the start of every hour",
|
|
389
|
+
})
|
|
390
|
+
: draft.preset === "daily-midnight"
|
|
391
|
+
? t("jobs.schedulePreview.dailyMidnight", {
|
|
392
|
+
defaultValue: "every day at midnight",
|
|
393
|
+
})
|
|
394
|
+
: draft.preset === "daily-noon"
|
|
395
|
+
? t("jobs.schedulePreview.dailyNoon", {
|
|
396
|
+
defaultValue: "every day at noon",
|
|
397
|
+
})
|
|
398
|
+
: t("jobs.schedulePreview.atTime", {
|
|
399
|
+
defaultValue: "at {{time}}",
|
|
400
|
+
time: formatAutomationTime(draft.time),
|
|
401
|
+
}),
|
|
402
|
+
})}
|
|
403
|
+
</p>
|
|
404
|
+
) : null}
|
|
405
|
+
{!advanced && friendly.error ? (
|
|
406
|
+
<p className="text-xs text-destructive">
|
|
407
|
+
{friendly.error === "daily-interval"
|
|
408
|
+
? t("jobs.dailyIntervalAdvanced", {
|
|
409
|
+
defaultValue:
|
|
410
|
+
"Every few days needs the Advanced cron editor below.",
|
|
411
|
+
})
|
|
412
|
+
: t("jobs.weeklyIntervalAdvanced", {
|
|
413
|
+
defaultValue:
|
|
414
|
+
"Custom weekly intervals beyond one week are available under Advanced.",
|
|
415
|
+
})}
|
|
416
|
+
</p>
|
|
417
|
+
) : null}
|
|
418
|
+
|
|
419
|
+
<div>
|
|
420
|
+
<label className="text-xs font-medium text-foreground">
|
|
421
|
+
{t("jobs.timezone", { defaultValue: "Timezone" })}
|
|
422
|
+
</label>
|
|
423
|
+
<div className="mt-1">
|
|
424
|
+
<TimezoneSelect
|
|
425
|
+
value={timezone}
|
|
426
|
+
disabled={saving}
|
|
427
|
+
onChange={onTimezoneChange}
|
|
428
|
+
suggested={[browserTimezone()]}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<details
|
|
434
|
+
className="group rounded-lg border border-border/70 bg-muted/20"
|
|
435
|
+
open={advanced}
|
|
436
|
+
onToggle={(event) => {
|
|
437
|
+
const next = event.currentTarget.open;
|
|
438
|
+
if (next && !advanced && friendly.schedule) {
|
|
439
|
+
setAdvancedValue(friendly.schedule);
|
|
440
|
+
}
|
|
441
|
+
setAdvanced(next);
|
|
442
|
+
}}
|
|
443
|
+
>
|
|
444
|
+
<summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2.5 text-xs font-medium text-foreground [&::-webkit-details-marker]:hidden">
|
|
445
|
+
<IconCode className="size-4 text-muted-foreground" />
|
|
446
|
+
{t("jobs.advancedSchedule", {
|
|
447
|
+
defaultValue: "Advanced - cron expression",
|
|
448
|
+
})}
|
|
449
|
+
<span className="ms-auto text-muted-foreground group-open:hidden">
|
|
450
|
+
{t("jobs.show", { defaultValue: "Show" })}
|
|
451
|
+
</span>
|
|
452
|
+
<span className="ms-auto hidden text-muted-foreground group-open:inline">
|
|
453
|
+
{t("jobs.hide", { defaultValue: "Hide" })}
|
|
454
|
+
</span>
|
|
455
|
+
</summary>
|
|
456
|
+
<div className="border-t border-border/60 px-3 pb-3 pt-2.5">
|
|
457
|
+
<Input
|
|
458
|
+
className="font-mono text-sm"
|
|
459
|
+
value={advancedValue}
|
|
460
|
+
spellCheck={false}
|
|
461
|
+
autoComplete="off"
|
|
462
|
+
disabled={saving}
|
|
463
|
+
onFocus={() => setAdvanced(true)}
|
|
464
|
+
onChange={(event) => {
|
|
465
|
+
setAdvanced(true);
|
|
466
|
+
setAdvancedValue(event.target.value);
|
|
467
|
+
}}
|
|
468
|
+
/>
|
|
469
|
+
<p className="mt-1 text-[11px] text-muted-foreground">
|
|
470
|
+
{t("jobs.cronFormatHint", {
|
|
471
|
+
defaultValue: "minute hour day-of-month month day-of-week",
|
|
472
|
+
})}
|
|
473
|
+
</p>
|
|
474
|
+
</div>
|
|
475
|
+
</details>
|
|
476
|
+
|
|
477
|
+
{activeSchedule && !valid ? (
|
|
478
|
+
<p className="text-xs text-destructive">
|
|
479
|
+
{t("jobs.cronFieldCount", {
|
|
480
|
+
defaultValue: "A cron expression needs exactly 5 fields.",
|
|
481
|
+
})}
|
|
482
|
+
</p>
|
|
483
|
+
) : null}
|
|
484
|
+
</div>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function EventFields({
|
|
489
|
+
open,
|
|
490
|
+
event,
|
|
491
|
+
onChange,
|
|
492
|
+
}: {
|
|
493
|
+
open: boolean;
|
|
494
|
+
event: string;
|
|
495
|
+
onChange: (value: string) => void;
|
|
496
|
+
}) {
|
|
497
|
+
const t = useT();
|
|
498
|
+
const eventsQuery = useActionQuery<AutomationEvent[]>(
|
|
499
|
+
"list-automation-events",
|
|
500
|
+
{},
|
|
501
|
+
{ enabled: open, staleTime: 60_000 },
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
<>
|
|
506
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
507
|
+
<span>{t("jobs.startWhen", { defaultValue: "Start when" })}</span>
|
|
508
|
+
<Select value={event} onValueChange={onChange}>
|
|
509
|
+
<SelectTrigger>
|
|
510
|
+
<SelectValue
|
|
511
|
+
placeholder={
|
|
512
|
+
eventsQuery.isLoading
|
|
513
|
+
? t("jobs.loadingEventTypes", {
|
|
514
|
+
defaultValue: "Loading event types...",
|
|
515
|
+
})
|
|
516
|
+
: t("jobs.chooseAppEvent", {
|
|
517
|
+
defaultValue: "Choose an app event",
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
/>
|
|
521
|
+
</SelectTrigger>
|
|
522
|
+
<SelectContent>
|
|
523
|
+
{(eventsQuery.data ?? []).map((item) => (
|
|
524
|
+
<SelectItem key={item.name} value={item.name}>
|
|
525
|
+
{item.name}
|
|
526
|
+
</SelectItem>
|
|
527
|
+
))}
|
|
528
|
+
</SelectContent>
|
|
529
|
+
</Select>
|
|
530
|
+
</label>
|
|
531
|
+
{eventsQuery.data?.find((item) => item.name === event)?.description ? (
|
|
532
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
533
|
+
{eventsQuery.data.find((item) => item.name === event)?.description}
|
|
534
|
+
</p>
|
|
535
|
+
) : null}
|
|
536
|
+
{eventsQuery.error ? (
|
|
537
|
+
<p className="mt-2 text-xs text-destructive">
|
|
538
|
+
{t("jobs.loadEventTypesError", {
|
|
539
|
+
defaultValue: "Could not load event types.",
|
|
540
|
+
})}
|
|
541
|
+
</p>
|
|
542
|
+
) : null}
|
|
543
|
+
</>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export interface AutomationCreateDialogProps {
|
|
548
|
+
open: boolean;
|
|
549
|
+
initialScope?: "personal" | "organization";
|
|
550
|
+
onOpenChange: (open: boolean) => void;
|
|
551
|
+
onCreated: (result: CreateAutomationResult) => void;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function AutomationCreateDialog({
|
|
555
|
+
open,
|
|
556
|
+
initialScope = "personal",
|
|
557
|
+
onOpenChange,
|
|
558
|
+
onCreated,
|
|
559
|
+
}: AutomationCreateDialogProps) {
|
|
560
|
+
const t = useT();
|
|
561
|
+
const [name, setName] = useState("");
|
|
562
|
+
const [scope, setScope] = useState<"personal" | "organization">(initialScope);
|
|
563
|
+
const [triggerType, setTriggerType] = useState<TriggerType>("schedule");
|
|
564
|
+
const [schedule, setSchedule] = useState("0 * * * *");
|
|
565
|
+
const [timezone, setTimezone] = useState(browserTimezone());
|
|
566
|
+
const [event, setEvent] = useState("");
|
|
567
|
+
const [body, setBody] = useState("");
|
|
568
|
+
const create = useActionMutation<
|
|
569
|
+
CreateAutomationResult,
|
|
570
|
+
CreateAutomationInput
|
|
571
|
+
>("manage-automation", {
|
|
572
|
+
onSuccess: onCreated,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const resetCreate = create.reset;
|
|
576
|
+
|
|
577
|
+
useEffect(() => {
|
|
578
|
+
if (!open) return;
|
|
579
|
+
setName("");
|
|
580
|
+
setScope(initialScope);
|
|
581
|
+
setTriggerType("schedule");
|
|
582
|
+
setSchedule("0 * * * *");
|
|
583
|
+
setTimezone(browserTimezone());
|
|
584
|
+
setEvent("");
|
|
585
|
+
setBody("");
|
|
586
|
+
resetCreate();
|
|
587
|
+
}, [initialScope, open, resetCreate]);
|
|
588
|
+
|
|
589
|
+
const canSubmit =
|
|
590
|
+
name.trim().length > 0 &&
|
|
591
|
+
body.trim().length > 0 &&
|
|
592
|
+
(triggerType !== "event" || event.length > 0) &&
|
|
593
|
+
(triggerType !== "schedule" || scheduleIsValid(schedule));
|
|
594
|
+
|
|
595
|
+
function submit() {
|
|
596
|
+
if (!canSubmit) return;
|
|
597
|
+
create.mutate({
|
|
598
|
+
operation: "create",
|
|
599
|
+
name: name.trim(),
|
|
600
|
+
scope,
|
|
601
|
+
triggerType,
|
|
602
|
+
body: body.trim(),
|
|
603
|
+
...(triggerType === "schedule"
|
|
604
|
+
? { schedule, timezone }
|
|
605
|
+
: triggerType === "event"
|
|
606
|
+
? { event }
|
|
607
|
+
: {}),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
613
|
+
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
|
614
|
+
<DialogHeader>
|
|
615
|
+
<DialogTitle>
|
|
616
|
+
{t("jobs.newAutomation", { defaultValue: "New automation" })}
|
|
617
|
+
</DialogTitle>
|
|
618
|
+
<DialogDescription>
|
|
619
|
+
{t("jobs.createAutomationDescription", {
|
|
620
|
+
defaultValue:
|
|
621
|
+
"Choose what starts it, then describe the work in plain language.",
|
|
622
|
+
})}
|
|
623
|
+
</DialogDescription>
|
|
624
|
+
</DialogHeader>
|
|
625
|
+
|
|
626
|
+
<div className="space-y-4">
|
|
627
|
+
<div className="grid gap-3 sm:grid-cols-[1fr_12rem]">
|
|
628
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
629
|
+
<span>{t("jobs.name", { defaultValue: "Name" })}</span>
|
|
630
|
+
<Input
|
|
631
|
+
value={name}
|
|
632
|
+
disabled={create.isPending}
|
|
633
|
+
placeholder={t("jobs.automationNamePlaceholder", {
|
|
634
|
+
defaultValue: "morning-digest",
|
|
635
|
+
})}
|
|
636
|
+
onChange={(event) => setName(event.target.value)}
|
|
637
|
+
/>
|
|
638
|
+
</label>
|
|
639
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
640
|
+
<span>{t("jobs.scope", { defaultValue: "Scope" })}</span>
|
|
641
|
+
<Select
|
|
642
|
+
value={scope}
|
|
643
|
+
disabled={create.isPending}
|
|
644
|
+
onValueChange={(value) =>
|
|
645
|
+
setScope(value as "personal" | "organization")
|
|
646
|
+
}
|
|
647
|
+
>
|
|
648
|
+
<SelectTrigger>
|
|
649
|
+
<SelectValue />
|
|
650
|
+
</SelectTrigger>
|
|
651
|
+
<SelectContent>
|
|
652
|
+
<SelectItem value="personal">
|
|
653
|
+
{t("jobs.personal", { defaultValue: "Personal" })}
|
|
654
|
+
</SelectItem>
|
|
655
|
+
<SelectItem value="organization">
|
|
656
|
+
{t("jobs.organization", { defaultValue: "Organization" })}
|
|
657
|
+
</SelectItem>
|
|
658
|
+
</SelectContent>
|
|
659
|
+
</Select>
|
|
660
|
+
</label>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<Tabs
|
|
664
|
+
value={triggerType}
|
|
665
|
+
onValueChange={(value) => setTriggerType(value as TriggerType)}
|
|
666
|
+
>
|
|
667
|
+
<TabsList className="grid h-auto w-full grid-cols-3">
|
|
668
|
+
<TabsTrigger value="schedule" className="gap-2 py-2 text-xs">
|
|
669
|
+
<IconClock className="size-3.5" />
|
|
670
|
+
{t("jobs.schedule", { defaultValue: "Schedule" })}
|
|
671
|
+
</TabsTrigger>
|
|
672
|
+
<TabsTrigger value="webhook" className="gap-2 py-2 text-xs">
|
|
673
|
+
<IconBolt className="size-3.5" />
|
|
674
|
+
{t("jobs.webhook", { defaultValue: "Webhook" })}
|
|
675
|
+
</TabsTrigger>
|
|
676
|
+
<TabsTrigger value="event" className="gap-2 py-2 text-xs">
|
|
677
|
+
<IconCalendarEvent className="size-3.5" />
|
|
678
|
+
{t("jobs.appEvent", { defaultValue: "App event" })}
|
|
679
|
+
</TabsTrigger>
|
|
680
|
+
</TabsList>
|
|
681
|
+
<TabsContent value="schedule" className="mt-3">
|
|
682
|
+
<ScheduleBuilder
|
|
683
|
+
key={open ? "schedule-open" : "schedule-closed"}
|
|
684
|
+
saving={create.isPending}
|
|
685
|
+
timezone={timezone}
|
|
686
|
+
onTimezoneChange={setTimezone}
|
|
687
|
+
onChange={setSchedule}
|
|
688
|
+
/>
|
|
689
|
+
</TabsContent>
|
|
690
|
+
<TabsContent value="webhook" className="mt-3">
|
|
691
|
+
<div className="rounded-lg border border-primary/20 bg-primary/5 p-3">
|
|
692
|
+
<div className="flex items-start gap-2">
|
|
693
|
+
<IconBolt className="mt-0.5 size-4 shrink-0 text-primary" />
|
|
694
|
+
<div>
|
|
695
|
+
<p className="text-sm font-medium text-foreground">
|
|
696
|
+
{t("jobs.webhookTriggerDescription", {
|
|
697
|
+
defaultValue: "Run when a service sends an HTTP POST",
|
|
698
|
+
})}
|
|
699
|
+
</p>
|
|
700
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
701
|
+
{t("jobs.webhookSetupDescription", {
|
|
702
|
+
defaultValue:
|
|
703
|
+
"After you save, Dispatch gives you a private URL to paste into GitHub, Stripe, or another webhook provider.",
|
|
704
|
+
})}
|
|
705
|
+
</p>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
</TabsContent>
|
|
710
|
+
<TabsContent value="event" className="mt-3">
|
|
711
|
+
<EventFields open={open} event={event} onChange={setEvent} />
|
|
712
|
+
</TabsContent>
|
|
713
|
+
</Tabs>
|
|
714
|
+
|
|
715
|
+
<label className="space-y-1.5 text-xs text-muted-foreground">
|
|
716
|
+
<span>
|
|
717
|
+
{t("jobs.whatShouldItDo", { defaultValue: "What should it do?" })}
|
|
718
|
+
</span>
|
|
719
|
+
<Textarea
|
|
720
|
+
value={body}
|
|
721
|
+
disabled={create.isPending}
|
|
722
|
+
placeholder={t("jobs.automationBodyPlaceholder", {
|
|
723
|
+
defaultValue:
|
|
724
|
+
"Review new support requests and summarize anything urgent in a shared note.",
|
|
725
|
+
})}
|
|
726
|
+
rows={5}
|
|
727
|
+
onChange={(event) => setBody(event.target.value)}
|
|
728
|
+
/>
|
|
729
|
+
</label>
|
|
730
|
+
|
|
731
|
+
{create.error ? (
|
|
732
|
+
<p className="text-xs text-destructive">{create.error.message}</p>
|
|
733
|
+
) : null}
|
|
734
|
+
</div>
|
|
735
|
+
|
|
736
|
+
<DialogFooter>
|
|
737
|
+
<Button
|
|
738
|
+
type="button"
|
|
739
|
+
variant="outline"
|
|
740
|
+
disabled={create.isPending}
|
|
741
|
+
onClick={() => onOpenChange(false)}
|
|
742
|
+
>
|
|
743
|
+
{t("jobs.cancel", { defaultValue: "Cancel" })}
|
|
744
|
+
</Button>
|
|
745
|
+
<Button
|
|
746
|
+
type="button"
|
|
747
|
+
disabled={!canSubmit || create.isPending}
|
|
748
|
+
onClick={submit}
|
|
749
|
+
>
|
|
750
|
+
{create.isPending ? (
|
|
751
|
+
<IconLoader2 className="size-4 animate-spin" />
|
|
752
|
+
) : null}
|
|
753
|
+
{t("jobs.createAutomation", { defaultValue: "Create automation" })}
|
|
754
|
+
</Button>
|
|
755
|
+
</DialogFooter>
|
|
756
|
+
</DialogContent>
|
|
757
|
+
</Dialog>
|
|
758
|
+
);
|
|
759
|
+
}
|