@checkstack/automation-frontend 0.2.0 → 0.3.0
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/CHANGELOG.md +352 -0
- package/package.json +13 -9
- package/src/components/AutomationGroupCombobox.tsx +133 -0
- package/src/editor/ActionEditor.tsx +180 -90
- package/src/editor/ActionListEditor.tsx +27 -1
- package/src/editor/AddActionDialog.tsx +15 -45
- package/src/editor/AddConditionDialog.tsx +86 -0
- package/src/editor/AddTriggerDialog.tsx +97 -0
- package/src/editor/AutomationDefinitionEditor.tsx +41 -2
- package/src/editor/ConditionEditor.tsx +359 -70
- package/src/editor/ConditionsEditor.tsx +113 -44
- package/src/editor/ItemSheet.tsx +51 -0
- package/src/editor/RunReplayPicker.tsx +97 -0
- package/src/editor/ScriptServicesBooter.tsx +53 -0
- package/src/editor/ScriptTestRenderer.tsx +150 -0
- package/src/editor/SystemEntityPicker.test.ts +37 -0
- package/src/editor/SystemEntityPicker.tsx +109 -0
- package/src/editor/TriggersEditor.tsx +345 -137
- package/src/editor/action-helpers.test.ts +107 -0
- package/src/editor/action-helpers.ts +72 -0
- package/src/editor/action-leaf-cards.tsx +98 -1
- package/src/editor/condition-kind.test.ts +126 -0
- package/src/editor/condition-kind.ts +130 -0
- package/src/editor/item-summary.test.ts +171 -0
- package/src/editor/item-summary.ts +210 -0
- package/src/editor/picker-dialog.tsx +156 -0
- package/src/editor/registry-context.tsx +9 -2
- package/src/editor/script-actions.test.ts +184 -0
- package/src/editor/script-actions.ts +146 -0
- package/src/editor/system-entity-picker.logic.ts +23 -0
- package/src/editor/template-completion.test.ts +22 -3
- package/src/editor/template-completion.ts +16 -8
- package/src/editor/template-helpers.ts +4 -0
- package/src/editor/trigger-helpers.test.ts +28 -0
- package/src/editor/trigger-helpers.ts +17 -0
- package/src/editor/useScriptDiagnostics.ts +108 -0
- package/src/index.tsx +2 -0
- package/src/pages/AutomationEditPage.tsx +95 -47
- package/src/pages/AutomationListPage.tsx +172 -123
- package/src/pages/automation-grouping.test.ts +86 -0
- package/src/pages/automation-grouping.ts +65 -0
- package/src/script-context.test.ts +142 -1
- package/src/script-context.ts +115 -0
- package/tsconfig.json +12 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {
|
|
3
|
+
Accordion,
|
|
4
|
+
AccordionContent,
|
|
5
|
+
AccordionItem,
|
|
6
|
+
AccordionTrigger,
|
|
3
7
|
ActionCard,
|
|
8
|
+
type ActionCardMenuItem,
|
|
4
9
|
Checkbox,
|
|
5
10
|
cn,
|
|
6
11
|
Input,
|
|
@@ -20,6 +25,7 @@ import type {
|
|
|
20
25
|
StopInput,
|
|
21
26
|
VariablesInput,
|
|
22
27
|
WaitForTriggerInput,
|
|
28
|
+
WaitUntilInput,
|
|
23
29
|
} from "@checkstack/automation-common";
|
|
24
30
|
import {
|
|
25
31
|
ACTION_KIND_META,
|
|
@@ -28,6 +34,8 @@ import {
|
|
|
28
34
|
collectActionIds,
|
|
29
35
|
defaultActionId,
|
|
30
36
|
} from "./action-helpers";
|
|
37
|
+
import { summarizeAction } from "./item-summary";
|
|
38
|
+
import { ItemSheet } from "./ItemSheet";
|
|
31
39
|
import { useAutomationRegistry, useVariableScope } from "./registry-context";
|
|
32
40
|
import { useActionIssues } from "./editor-validation";
|
|
33
41
|
import {
|
|
@@ -37,6 +45,7 @@ import {
|
|
|
37
45
|
StopActionBody,
|
|
38
46
|
VariablesActionBody,
|
|
39
47
|
WaitForTriggerActionBody,
|
|
48
|
+
WaitUntilActionBody,
|
|
40
49
|
} from "./action-leaf-cards";
|
|
41
50
|
import {
|
|
42
51
|
ChooseActionBody,
|
|
@@ -49,6 +58,8 @@ export interface ActionEditorProps {
|
|
|
49
58
|
value: ActionInput;
|
|
50
59
|
onChange: (next: ActionInput) => void;
|
|
51
60
|
onDelete: () => void;
|
|
61
|
+
/** Clone this action (fresh ids) directly after itself. Omit to hide. */
|
|
62
|
+
onDuplicate?: () => void;
|
|
52
63
|
path: ActionPath;
|
|
53
64
|
definition: AutomationDefinition;
|
|
54
65
|
dragHandleProps?: React.HTMLAttributes<HTMLButtonElement>;
|
|
@@ -91,6 +102,7 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
91
102
|
value,
|
|
92
103
|
onChange,
|
|
93
104
|
onDelete,
|
|
105
|
+
onDuplicate,
|
|
94
106
|
path,
|
|
95
107
|
definition,
|
|
96
108
|
dragHandleProps,
|
|
@@ -100,6 +112,7 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
100
112
|
const { actions } = useAutomationRegistry();
|
|
101
113
|
const kind = actionKindOf(value);
|
|
102
114
|
const meta = ACTION_KIND_META[kind];
|
|
115
|
+
const [sheetOpen, setSheetOpen] = React.useState(false);
|
|
103
116
|
const {
|
|
104
117
|
templateProperties,
|
|
105
118
|
variableNodes,
|
|
@@ -116,101 +129,73 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
116
129
|
actions.find((a) => a.qualifiedId === qualified)?.displayName,
|
|
117
130
|
);
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
// Collapsed-row summary derived from the config; falls back to the
|
|
133
|
+
// operator's description/id note when there's no derivable summary.
|
|
134
|
+
const summary =
|
|
135
|
+
summarizeAction(value) ??
|
|
136
|
+
value.description ??
|
|
137
|
+
(value.id ? `id: ${value.id}` : undefined);
|
|
121
138
|
|
|
122
139
|
const enabledValue = value.enabled !== false;
|
|
123
140
|
const issues = useActionIssues(path);
|
|
141
|
+
// `id` auto-fills on blur, so it isn't a meaningful "advanced was
|
|
142
|
+
// customised" signal; open the metadata disclosure only when the operator
|
|
143
|
+
// set a description or flipped the failure behaviour.
|
|
144
|
+
const hasAdvancedMeta =
|
|
145
|
+
value.description !== undefined || value.continue_on_error === true;
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
category={meta.label}
|
|
131
|
-
icon={meta.icon}
|
|
132
|
-
enabled={enabledValue}
|
|
133
|
-
onEnabledChange={
|
|
134
|
-
disabled
|
|
135
|
-
? undefined
|
|
136
|
-
: (next) => onChange({ ...value, enabled: next })
|
|
137
|
-
}
|
|
138
|
-
onDelete={disabled ? undefined : onDelete}
|
|
139
|
-
dragHandleProps={disabled ? undefined : dragHandleProps}
|
|
140
|
-
errors={issues}
|
|
141
|
-
>
|
|
142
|
-
<div className="space-y-4">
|
|
143
|
-
{/* Action settings — identity and failure behaviour. Grouped in a
|
|
144
|
-
quiet panel and labelled with small uppercase eyebrows so the
|
|
145
|
-
action's own configuration below reads as the primary content of
|
|
146
|
-
the card rather than competing with it. The action's kind is
|
|
147
|
-
fixed at creation; to change it, add a new step and delete this
|
|
148
|
-
one. */}
|
|
149
|
-
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3">
|
|
150
|
-
<div className="grid gap-x-4 gap-y-3 sm:grid-cols-2">
|
|
151
|
-
<MetaField label="Id" htmlFor={`${stableId}-id`}>
|
|
152
|
-
<Input
|
|
153
|
-
id={`${stableId}-id`}
|
|
154
|
-
value={value.id ?? ""}
|
|
155
|
-
onChange={(event) =>
|
|
156
|
-
onChange({ ...value, id: event.target.value || undefined })
|
|
157
|
-
}
|
|
158
|
-
onBlur={() => {
|
|
159
|
-
// Never leave the id blank: re-fill a unique, log-friendly
|
|
160
|
-
// default so the action stays referenceable
|
|
161
|
-
// (artifacts.<id>.<name>) and parseable in run logs.
|
|
162
|
-
if (value.id) return;
|
|
163
|
-
const taken = collectActionIds(definition.actions);
|
|
164
|
-
onChange({ ...value, id: defaultActionId(value, taken) });
|
|
165
|
-
}}
|
|
166
|
-
placeholder="Generated on blur"
|
|
167
|
-
disabled={disabled}
|
|
168
|
-
className="h-8 text-xs"
|
|
169
|
-
/>
|
|
170
|
-
</MetaField>
|
|
147
|
+
// Validation issues (structural + inline-script type errors) surface only as
|
|
148
|
+
// the card's error badge - never auto-open the sheet. Auto-opening would pop
|
|
149
|
+
// multiple sheets at once when several cards have errors (e.g. every script
|
|
150
|
+
// action after a trigger change), so the badge is the single, calm signal and
|
|
151
|
+
// the operator clicks in for detail.
|
|
171
152
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
!disabled &&
|
|
197
|
-
onChange({
|
|
198
|
-
...value,
|
|
199
|
-
continue_on_error: value.continue_on_error !== true,
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
>
|
|
203
|
-
<Checkbox
|
|
204
|
-
checked={value.continue_on_error === true}
|
|
205
|
-
disabled={disabled}
|
|
206
|
-
/>
|
|
207
|
-
<Label className="text-xs font-normal text-muted-foreground cursor-pointer">
|
|
208
|
-
Continue on error
|
|
209
|
-
</Label>
|
|
210
|
-
</div>
|
|
211
|
-
</MetaField>
|
|
212
|
-
</div>
|
|
153
|
+
const menuItems: ActionCardMenuItem[] = disabled
|
|
154
|
+
? []
|
|
155
|
+
: [
|
|
156
|
+
{
|
|
157
|
+
label: enabledValue ? "Disable" : "Enable",
|
|
158
|
+
icon: enabledValue ? "PowerOff" : "Power",
|
|
159
|
+
onClick: () => onChange({ ...value, enabled: !enabledValue }),
|
|
160
|
+
},
|
|
161
|
+
...(onDuplicate
|
|
162
|
+
? [
|
|
163
|
+
{
|
|
164
|
+
label: "Duplicate",
|
|
165
|
+
icon: "Copy" as const,
|
|
166
|
+
onClick: onDuplicate,
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
: []),
|
|
170
|
+
{
|
|
171
|
+
label: "Delete",
|
|
172
|
+
icon: "Trash2",
|
|
173
|
+
onClick: onDelete,
|
|
174
|
+
variant: "destructive",
|
|
175
|
+
},
|
|
176
|
+
];
|
|
213
177
|
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<ActionCard
|
|
181
|
+
id={stableId}
|
|
182
|
+
title={title}
|
|
183
|
+
summary={summary}
|
|
184
|
+
category={meta.label}
|
|
185
|
+
icon={meta.icon}
|
|
186
|
+
enabled={enabledValue}
|
|
187
|
+
onOpenSheet={() => setSheetOpen(true)}
|
|
188
|
+
actions={menuItems}
|
|
189
|
+
dragHandleProps={disabled ? undefined : dragHandleProps}
|
|
190
|
+
errors={issues}
|
|
191
|
+
/>
|
|
192
|
+
<ItemSheet
|
|
193
|
+
open={sheetOpen}
|
|
194
|
+
onOpenChange={setSheetOpen}
|
|
195
|
+
title={title}
|
|
196
|
+
description={meta.label}
|
|
197
|
+
>
|
|
198
|
+
<div className="space-y-4">
|
|
214
199
|
<ActionBody
|
|
215
200
|
kind={kind}
|
|
216
201
|
value={value}
|
|
@@ -224,8 +209,102 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
224
209
|
shellEnvVars={shellEnvVars}
|
|
225
210
|
disabled={disabled}
|
|
226
211
|
/>
|
|
212
|
+
|
|
213
|
+
{/* Per-action metadata — id, description, failure behaviour — tucked
|
|
214
|
+
behind a collapsed disclosure so the action's own configuration
|
|
215
|
+
above reads as the primary content of the card. The action's kind
|
|
216
|
+
is fixed at creation; to change it, add a new step and delete this
|
|
217
|
+
one. Enable/disable lives on the card header, not here. */}
|
|
218
|
+
<Accordion
|
|
219
|
+
type="single"
|
|
220
|
+
collapsible
|
|
221
|
+
defaultValue={hasAdvancedMeta ? "advanced" : undefined}
|
|
222
|
+
className="w-full"
|
|
223
|
+
>
|
|
224
|
+
<AccordionItem value="advanced" className="border-b-0">
|
|
225
|
+
<AccordionTrigger className="py-2 text-xs hover:no-underline">
|
|
226
|
+
<span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
227
|
+
Advanced
|
|
228
|
+
</span>
|
|
229
|
+
</AccordionTrigger>
|
|
230
|
+
<AccordionContent className="pb-0">
|
|
231
|
+
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3">
|
|
232
|
+
<div className="grid gap-x-4 gap-y-3 sm:grid-cols-2">
|
|
233
|
+
<MetaField label="Id" htmlFor={`${stableId}-id`}>
|
|
234
|
+
<Input
|
|
235
|
+
id={`${stableId}-id`}
|
|
236
|
+
value={value.id ?? ""}
|
|
237
|
+
onChange={(event) =>
|
|
238
|
+
onChange({
|
|
239
|
+
...value,
|
|
240
|
+
id: event.target.value || undefined,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
onBlur={() => {
|
|
244
|
+
// Never leave the id blank: re-fill a unique,
|
|
245
|
+
// log-friendly default so the action stays
|
|
246
|
+
// referenceable (artifacts.<id>.<name>) and parseable
|
|
247
|
+
// in run logs.
|
|
248
|
+
if (value.id) return;
|
|
249
|
+
const taken = collectActionIds(definition.actions);
|
|
250
|
+
onChange({
|
|
251
|
+
...value,
|
|
252
|
+
id: defaultActionId(value, taken),
|
|
253
|
+
});
|
|
254
|
+
}}
|
|
255
|
+
placeholder="Generated on blur"
|
|
256
|
+
disabled={disabled}
|
|
257
|
+
className="h-8 text-xs"
|
|
258
|
+
/>
|
|
259
|
+
</MetaField>
|
|
260
|
+
|
|
261
|
+
<MetaField label="Description" htmlFor={`${stableId}-desc`}>
|
|
262
|
+
<Input
|
|
263
|
+
id={`${stableId}-desc`}
|
|
264
|
+
value={value.description ?? ""}
|
|
265
|
+
onChange={(event) =>
|
|
266
|
+
onChange({
|
|
267
|
+
...value,
|
|
268
|
+
description: event.target.value || undefined,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
placeholder="Optional note"
|
|
272
|
+
disabled={disabled}
|
|
273
|
+
className="h-8 text-xs"
|
|
274
|
+
/>
|
|
275
|
+
</MetaField>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<MetaField label="On failure">
|
|
279
|
+
<div
|
|
280
|
+
className={cn(
|
|
281
|
+
"flex h-8 items-center gap-2 select-none",
|
|
282
|
+
disabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
283
|
+
)}
|
|
284
|
+
onClick={() =>
|
|
285
|
+
!disabled &&
|
|
286
|
+
onChange({
|
|
287
|
+
...value,
|
|
288
|
+
continue_on_error: value.continue_on_error !== true,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
>
|
|
292
|
+
<Checkbox
|
|
293
|
+
checked={value.continue_on_error === true}
|
|
294
|
+
disabled={disabled}
|
|
295
|
+
/>
|
|
296
|
+
<Label className="text-xs font-normal text-muted-foreground cursor-pointer">
|
|
297
|
+
Continue on error
|
|
298
|
+
</Label>
|
|
299
|
+
</div>
|
|
300
|
+
</MetaField>
|
|
301
|
+
</div>
|
|
302
|
+
</AccordionContent>
|
|
303
|
+
</AccordionItem>
|
|
304
|
+
</Accordion>
|
|
227
305
|
</div>
|
|
228
|
-
|
|
306
|
+
</ItemSheet>
|
|
307
|
+
</>
|
|
229
308
|
);
|
|
230
309
|
};
|
|
231
310
|
|
|
@@ -343,6 +422,17 @@ const ActionBody: React.FC<{
|
|
|
343
422
|
/>
|
|
344
423
|
);
|
|
345
424
|
}
|
|
425
|
+
case "wait_until": {
|
|
426
|
+
return (
|
|
427
|
+
<WaitUntilActionBody
|
|
428
|
+
value={value as WaitUntilInput}
|
|
429
|
+
onChange={(next) => onChange(next)}
|
|
430
|
+
variableNodes={variableNodes}
|
|
431
|
+
completionProvider={expressionCompletion}
|
|
432
|
+
disabled={disabled}
|
|
433
|
+
/>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
346
436
|
case "sequence": {
|
|
347
437
|
return (
|
|
348
438
|
<SequenceActionBody
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import {
|
|
23
23
|
assignDefaultIds,
|
|
24
24
|
collectActionIds,
|
|
25
|
+
duplicateAction,
|
|
25
26
|
makeEmptyAction,
|
|
26
27
|
makeProviderAction,
|
|
27
28
|
} from "./action-helpers";
|
|
@@ -128,6 +129,28 @@ export const ActionListEditor: React.FC<ActionListEditorProps> = ({
|
|
|
128
129
|
setIds((current) => [...current, nextId()]);
|
|
129
130
|
};
|
|
130
131
|
|
|
132
|
+
const duplicateStep = (index: number): void => {
|
|
133
|
+
const original = value[index];
|
|
134
|
+
if (!original) return;
|
|
135
|
+
// Clone with FRESH ids deduped against every id in the automation so the
|
|
136
|
+
// copy never collides with the original (or any sibling/nested step).
|
|
137
|
+
const taken = collectActionIds(definition.actions);
|
|
138
|
+
const clone = duplicateAction(original, taken);
|
|
139
|
+
// Insert directly after the original, keeping the parallel `ids` array in
|
|
140
|
+
// lockstep so each card's stable React key / drag id stays aligned.
|
|
141
|
+
const nextValue = [
|
|
142
|
+
...value.slice(0, index + 1),
|
|
143
|
+
clone,
|
|
144
|
+
...value.slice(index + 1),
|
|
145
|
+
];
|
|
146
|
+
onChange(nextValue);
|
|
147
|
+
setIds((current) => [
|
|
148
|
+
...current.slice(0, index + 1),
|
|
149
|
+
nextId(),
|
|
150
|
+
...current.slice(index + 1),
|
|
151
|
+
]);
|
|
152
|
+
};
|
|
153
|
+
|
|
131
154
|
return (
|
|
132
155
|
<div className="space-y-2">
|
|
133
156
|
<DndContext
|
|
@@ -150,6 +173,7 @@ export const ActionListEditor: React.FC<ActionListEditorProps> = ({
|
|
|
150
173
|
onChange(value.filter((_, i) => i !== index));
|
|
151
174
|
setIds((current) => current.filter((_, i) => i !== index));
|
|
152
175
|
}}
|
|
176
|
+
onDuplicate={disabled ? undefined : () => duplicateStep(index)}
|
|
153
177
|
path={childPathAt(index)}
|
|
154
178
|
definition={definition}
|
|
155
179
|
disabled={disabled}
|
|
@@ -174,10 +198,11 @@ const SortableActionItem: React.FC<{
|
|
|
174
198
|
value: ActionInput;
|
|
175
199
|
onChange: (next: ActionInput) => void;
|
|
176
200
|
onDelete: () => void;
|
|
201
|
+
onDuplicate?: () => void;
|
|
177
202
|
path: ActionPath;
|
|
178
203
|
definition: Parameters<typeof ActionEditor>[0]["definition"];
|
|
179
204
|
disabled?: boolean;
|
|
180
|
-
}> = ({ id, value, onChange, onDelete, path, definition, disabled }) => {
|
|
205
|
+
}> = ({ id, value, onChange, onDelete, onDuplicate, path, definition, disabled }) => {
|
|
181
206
|
const sortable = useSortable({ id });
|
|
182
207
|
const style: React.CSSProperties = {
|
|
183
208
|
transform: CSS.Transform.toString(sortable.transform),
|
|
@@ -189,6 +214,7 @@ const SortableActionItem: React.FC<{
|
|
|
189
214
|
value={value}
|
|
190
215
|
onChange={onChange}
|
|
191
216
|
onDelete={onDelete}
|
|
217
|
+
onDuplicate={onDuplicate}
|
|
192
218
|
path={path}
|
|
193
219
|
definition={definition}
|
|
194
220
|
stableId={id}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Zap } from "lucide-react";
|
|
3
3
|
import {
|
|
4
|
-
Button,
|
|
5
4
|
Dialog,
|
|
6
5
|
DialogContent,
|
|
7
6
|
DialogHeader,
|
|
8
7
|
DialogTitle,
|
|
9
8
|
DynamicIcon,
|
|
10
|
-
Input,
|
|
11
9
|
TabPanel,
|
|
12
10
|
Tabs,
|
|
13
11
|
} from "@checkstack/ui";
|
|
14
12
|
import { ACTION_KIND_META, type ActionKind } from "./action-helpers";
|
|
13
|
+
import {
|
|
14
|
+
PickerAddButton,
|
|
15
|
+
PickerRow,
|
|
16
|
+
PickerSearchInput,
|
|
17
|
+
} from "./picker-dialog";
|
|
15
18
|
import { useAutomationRegistry } from "./registry-context";
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -30,6 +33,7 @@ const BLOCK_KINDS: ActionKind[] = [
|
|
|
30
33
|
"stop",
|
|
31
34
|
"variables",
|
|
32
35
|
"wait_for_trigger",
|
|
36
|
+
"wait_until",
|
|
33
37
|
];
|
|
34
38
|
|
|
35
39
|
const TAB_ITEMS = [
|
|
@@ -37,29 +41,6 @@ const TAB_ITEMS = [
|
|
|
37
41
|
{ id: "blocks", label: "Blocks" },
|
|
38
42
|
];
|
|
39
43
|
|
|
40
|
-
/** Shared row used by both tabs — icon, name, description, and a `+` affordance. */
|
|
41
|
-
const PickerRow: React.FC<{
|
|
42
|
-
icon: React.ReactNode;
|
|
43
|
-
title: string;
|
|
44
|
-
description?: string;
|
|
45
|
-
onClick: () => void;
|
|
46
|
-
}> = ({ icon, title, description, onClick }) => (
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
onClick={onClick}
|
|
50
|
-
className="flex w-full items-start gap-3 rounded-md border border-border/60 bg-card px-3 py-2.5 text-left transition-colors hover:border-border hover:bg-accent/50"
|
|
51
|
-
>
|
|
52
|
-
<span className="mt-0.5 shrink-0 text-muted-foreground">{icon}</span>
|
|
53
|
-
<div className="min-w-0 flex-1">
|
|
54
|
-
<div className="text-sm font-medium">{title}</div>
|
|
55
|
-
{description && (
|
|
56
|
-
<div className="text-xs text-muted-foreground">{description}</div>
|
|
57
|
-
)}
|
|
58
|
-
</div>
|
|
59
|
-
<Plus className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
60
|
-
</button>
|
|
61
|
-
);
|
|
62
|
-
|
|
63
44
|
export interface AddActionDialogProps {
|
|
64
45
|
/** Insert a building-block step of the given kind. */
|
|
65
46
|
onAddKind: (kind: ActionKind) => void;
|
|
@@ -128,17 +109,11 @@ export const AddActionDialog: React.FC<AddActionDialogProps> = ({
|
|
|
128
109
|
|
|
129
110
|
return (
|
|
130
111
|
<>
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
variant="outline"
|
|
134
|
-
size="sm"
|
|
112
|
+
<PickerAddButton
|
|
113
|
+
label="Add step"
|
|
135
114
|
disabled={disabled}
|
|
136
|
-
className="h-7 text-xs"
|
|
137
115
|
onClick={() => setOpen(true)}
|
|
138
|
-
|
|
139
|
-
<Plus className="mr-1 h-3 w-3" />
|
|
140
|
-
Add step
|
|
141
|
-
</Button>
|
|
116
|
+
/>
|
|
142
117
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
143
118
|
<DialogContent size="lg">
|
|
144
119
|
<DialogHeader>
|
|
@@ -146,16 +121,11 @@ export const AddActionDialog: React.FC<AddActionDialogProps> = ({
|
|
|
146
121
|
</DialogHeader>
|
|
147
122
|
|
|
148
123
|
<div className="space-y-3">
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
onChange={(event) => setQuery(event.target.value)}
|
|
155
|
-
placeholder="Search actions and blocks…"
|
|
156
|
-
className="pl-9"
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
124
|
+
<PickerSearchInput
|
|
125
|
+
value={query}
|
|
126
|
+
onChange={setQuery}
|
|
127
|
+
placeholder="Search actions and blocks…"
|
|
128
|
+
/>
|
|
159
129
|
|
|
160
130
|
<Tabs items={TAB_ITEMS} activeTab={tab} onTabChange={setTab} />
|
|
161
131
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DynamicIcon } from "@checkstack/ui";
|
|
3
|
+
import {
|
|
4
|
+
CONDITION_KIND_META,
|
|
5
|
+
type ConditionKind,
|
|
6
|
+
type ConditionKindGroup,
|
|
7
|
+
} from "./condition-kind";
|
|
8
|
+
import { PickerDialogShell, PickerRow } from "./picker-dialog";
|
|
9
|
+
|
|
10
|
+
export interface AddConditionDialogProps {
|
|
11
|
+
/** Append a condition of the picked kind (seeded via `defaultForKind`). */
|
|
12
|
+
onAdd: (kind: ConditionKind) => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Picker group order — structured first (common case), advanced last. */
|
|
17
|
+
const GROUP_ORDER: ConditionKindGroup[] = ["Structured", "Logical", "Advanced"];
|
|
18
|
+
|
|
19
|
+
const ALL_KINDS = Object.values(CONDITION_KIND_META);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Home-Assistant-style condition type picker. The operator picks the condition
|
|
23
|
+
* kind up front (grouped by `CONDITION_KIND_META.group`, searchable on label /
|
|
24
|
+
* description); on pick the consumer (`ConditionsEditor`) appends a seeded
|
|
25
|
+
* default for that kind. Mirrors the Actions "Add step" picker but is
|
|
26
|
+
* single-list.
|
|
27
|
+
*/
|
|
28
|
+
export const AddConditionDialog: React.FC<AddConditionDialogProps> = ({
|
|
29
|
+
onAdd,
|
|
30
|
+
disabled,
|
|
31
|
+
}) => (
|
|
32
|
+
<PickerDialogShell
|
|
33
|
+
addLabel="Add condition"
|
|
34
|
+
title="Add condition"
|
|
35
|
+
searchPlaceholder="Search conditions…"
|
|
36
|
+
disabled={disabled}
|
|
37
|
+
>
|
|
38
|
+
{({ query, close }) => {
|
|
39
|
+
const filtered = query
|
|
40
|
+
? ALL_KINDS.filter(
|
|
41
|
+
(meta) =>
|
|
42
|
+
meta.label.toLowerCase().includes(query) ||
|
|
43
|
+
meta.description.toLowerCase().includes(query) ||
|
|
44
|
+
meta.kind.toLowerCase().includes(query),
|
|
45
|
+
)
|
|
46
|
+
: ALL_KINDS;
|
|
47
|
+
|
|
48
|
+
const grouped = GROUP_ORDER.map((group) => ({
|
|
49
|
+
group,
|
|
50
|
+
items: filtered.filter((meta) => meta.group === group),
|
|
51
|
+
})).filter(({ items }) => items.length > 0);
|
|
52
|
+
|
|
53
|
+
if (grouped.length === 0) {
|
|
54
|
+
return (
|
|
55
|
+
<p className="px-1 py-6 text-center text-xs italic text-muted-foreground">
|
|
56
|
+
No matching conditions.
|
|
57
|
+
</p>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="space-y-3">
|
|
63
|
+
{grouped.map(({ group, items }) => (
|
|
64
|
+
<div key={group} className="space-y-1">
|
|
65
|
+
<p className="px-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
66
|
+
{group}
|
|
67
|
+
</p>
|
|
68
|
+
{items.map((meta) => (
|
|
69
|
+
<PickerRow
|
|
70
|
+
key={meta.kind}
|
|
71
|
+
icon={<DynamicIcon name={meta.icon} className="h-4 w-4" />}
|
|
72
|
+
title={meta.label}
|
|
73
|
+
description={meta.description}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
onAdd(meta.kind);
|
|
76
|
+
close();
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}}
|
|
85
|
+
</PickerDialogShell>
|
|
86
|
+
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Zap } from "lucide-react";
|
|
3
|
+
import { useAutomationRegistry } from "./registry-context";
|
|
4
|
+
import { PickerDialogShell, PickerRow } from "./picker-dialog";
|
|
5
|
+
|
|
6
|
+
export interface AddTriggerDialogProps {
|
|
7
|
+
/** Append a trigger subscribed to the picked event's qualified id. */
|
|
8
|
+
onAdd: (event: string) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Home-Assistant-style trigger type picker. The operator picks a concrete
|
|
14
|
+
* trigger event (grouped by category, searchable) up front; on pick the
|
|
15
|
+
* consumer (`TriggersEditor`) creates the trigger with a unique default id and
|
|
16
|
+
* appends it. Mirrors the Actions "Add step" picker but is single-list.
|
|
17
|
+
*/
|
|
18
|
+
export const AddTriggerDialog: React.FC<AddTriggerDialogProps> = ({
|
|
19
|
+
onAdd,
|
|
20
|
+
disabled,
|
|
21
|
+
}) => {
|
|
22
|
+
const { triggers } = useAutomationRegistry();
|
|
23
|
+
|
|
24
|
+
const items = React.useMemo(
|
|
25
|
+
() =>
|
|
26
|
+
triggers.map((t) => ({
|
|
27
|
+
id: t.qualifiedId,
|
|
28
|
+
label: t.displayName,
|
|
29
|
+
description: t.description,
|
|
30
|
+
category: t.category,
|
|
31
|
+
})),
|
|
32
|
+
[triggers],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<PickerDialogShell
|
|
37
|
+
addLabel="Add trigger"
|
|
38
|
+
title="Add trigger"
|
|
39
|
+
searchPlaceholder="Search triggers…"
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
>
|
|
42
|
+
{({ query, close }) => {
|
|
43
|
+
const filtered = query
|
|
44
|
+
? items.filter(
|
|
45
|
+
(item) =>
|
|
46
|
+
item.label.toLowerCase().includes(query) ||
|
|
47
|
+
item.id.toLowerCase().includes(query) ||
|
|
48
|
+
item.description?.toLowerCase().includes(query) ||
|
|
49
|
+
item.category.toLowerCase().includes(query),
|
|
50
|
+
)
|
|
51
|
+
: items;
|
|
52
|
+
|
|
53
|
+
const groups = new Map<string, typeof filtered>();
|
|
54
|
+
for (const item of filtered) {
|
|
55
|
+
const list = groups.get(item.category) ?? [];
|
|
56
|
+
list.push(item);
|
|
57
|
+
groups.set(item.category, list);
|
|
58
|
+
}
|
|
59
|
+
const grouped = [...groups.entries()].toSorted(([a], [b]) =>
|
|
60
|
+
a.localeCompare(b),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (grouped.length === 0) {
|
|
64
|
+
return (
|
|
65
|
+
<p className="px-1 py-6 text-center text-xs italic text-muted-foreground">
|
|
66
|
+
No matching triggers.
|
|
67
|
+
</p>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="space-y-3">
|
|
73
|
+
{grouped.map(([category, list]) => (
|
|
74
|
+
<div key={category} className="space-y-1">
|
|
75
|
+
<p className="px-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
76
|
+
{category}
|
|
77
|
+
</p>
|
|
78
|
+
{list.map((item) => (
|
|
79
|
+
<PickerRow
|
|
80
|
+
key={item.id}
|
|
81
|
+
icon={<Zap className="h-4 w-4" />}
|
|
82
|
+
title={item.label}
|
|
83
|
+
description={item.description}
|
|
84
|
+
onClick={() => {
|
|
85
|
+
onAdd(item.id);
|
|
86
|
+
close();
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}}
|
|
95
|
+
</PickerDialogShell>
|
|
96
|
+
);
|
|
97
|
+
};
|