@checkstack/ui 1.10.0 → 1.12.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/.storybook/main.ts +43 -0
- package/CHANGELOG.md +565 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +25 -2
- package/src/components/ActionCard.tsx +309 -0
- package/src/components/CodeEditor/CodeEditor.tsx +132 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -0
- package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
- package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
- package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
- package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
- package/src/components/CodeEditor/importSpecifiers.ts +267 -0
- package/src/components/CodeEditor/index.ts +26 -0
- package/src/components/CodeEditor/monacoTsService.ts +217 -0
- package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
- package/src/components/CodeEditor/popoutTitle.ts +31 -0
- package/src/components/CodeEditor/scriptContext.test.ts +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +168 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
- package/src/components/CodeEditor/validateScripts.ts +132 -0
- package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
- package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
- package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
- package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
- package/src/components/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +27 -1
- package/src/components/DynamicForm/FormField.tsx +138 -10
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +6 -0
- package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
- package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
- package/src/components/DynamicForm/types.ts +83 -1
- package/src/components/DynamicForm/utils.ts +32 -0
- package/src/components/Popover.tsx +6 -1
- package/src/components/ScriptTestPanel.logic.test.ts +139 -0
- package/src/components/ScriptTestPanel.logic.ts +137 -0
- package/src/components/ScriptTestPanel.tsx +394 -0
- package/src/components/Sheet.tsx +21 -6
- package/src/components/TemplateInput.tsx +104 -0
- package/src/components/TemplateInputToggle.tsx +111 -0
- package/src/components/TemplateValueInput.test.ts +98 -0
- package/src/components/TemplateValueInput.tsx +470 -0
- package/src/components/TimeOfDayInput.tsx +116 -0
- package/src/components/VariablePicker.tsx +271 -0
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +9 -0
- package/stories/ActionCard.stories.tsx +122 -0
- package/stories/Alert.stories.tsx +5 -5
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/tsconfig.json +1 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
- package/src/components/CodeEditor/monacoStdlib.ts +0 -62
- package/src/components/CodeEditor/monacoWorkers.ts +0 -118
|
@@ -13,13 +13,21 @@ import {
|
|
|
13
13
|
Textarea,
|
|
14
14
|
Toggle,
|
|
15
15
|
ColorPicker,
|
|
16
|
+
TemplateValueInput,
|
|
17
|
+
DurationInput,
|
|
18
|
+
type DurationValue,
|
|
16
19
|
} from "../../index";
|
|
17
20
|
|
|
18
21
|
import type { FormFieldProps, JsonSchemaProperty } from "./types";
|
|
19
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
getCleanDescription,
|
|
24
|
+
NONE_SENTINEL,
|
|
25
|
+
findSecretEnvSibling,
|
|
26
|
+
} from "./utils";
|
|
20
27
|
import { DynamicOptionsField } from "./DynamicOptionsField";
|
|
21
28
|
import { JsonField } from "./JsonField";
|
|
22
29
|
import { MultiTypeEditorField } from "./MultiTypeEditorField";
|
|
30
|
+
import { SecretEnvEditor } from "./SecretEnvEditor";
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
33
|
* Recursive field renderer that handles all supported JSON Schema types.
|
|
@@ -33,9 +41,16 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
33
41
|
formValues,
|
|
34
42
|
optionsResolvers,
|
|
35
43
|
templateProperties,
|
|
44
|
+
templateCompletionProvider,
|
|
36
45
|
typeDefinitions,
|
|
37
46
|
shellEnvVars,
|
|
38
47
|
starterTemplates,
|
|
48
|
+
scriptTestRenderer,
|
|
49
|
+
secretNames,
|
|
50
|
+
acquireTypes,
|
|
51
|
+
acquireResetKey,
|
|
52
|
+
importablePackages,
|
|
53
|
+
siblingSecretEnv,
|
|
39
54
|
onChange,
|
|
40
55
|
}) => {
|
|
41
56
|
const description = propSchema.description || "";
|
|
@@ -72,6 +87,34 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
72
87
|
return <></>;
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
// Duration field — render the DurationInput (single-unit duration
|
|
91
|
+
// object). Marked via `x-duration: true` or `format: "duration"`. This
|
|
92
|
+
// branch is intentionally additive and sits before the generic union /
|
|
93
|
+
// object handlers so a `for:` / threshold-window config renders the
|
|
94
|
+
// widget rather than the raw oneOf discriminator picker.
|
|
95
|
+
const isDuration =
|
|
96
|
+
propSchema["x-duration"] === true || propSchema.format === "duration";
|
|
97
|
+
if (isDuration) {
|
|
98
|
+
const cleanDesc = getCleanDescription(description);
|
|
99
|
+
return (
|
|
100
|
+
<div className="space-y-2">
|
|
101
|
+
<div>
|
|
102
|
+
<Label htmlFor={id}>
|
|
103
|
+
{label} {isRequired && "*"}
|
|
104
|
+
</Label>
|
|
105
|
+
{cleanDesc && (
|
|
106
|
+
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
<DurationInput
|
|
110
|
+
id={id}
|
|
111
|
+
value={value as DurationValue | undefined}
|
|
112
|
+
onChange={(next) => onChange(next)}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
75
118
|
// Enum handling
|
|
76
119
|
if (propSchema.enum) {
|
|
77
120
|
const cleanDesc = getCleanDescription(description);
|
|
@@ -135,6 +178,16 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
135
178
|
typeDefinitions={typeDefinitions}
|
|
136
179
|
shellEnvVars={shellEnvVars}
|
|
137
180
|
starterTemplates={starterTemplates}
|
|
181
|
+
scriptTestRenderer={
|
|
182
|
+
propSchema["x-script-testable"] === true
|
|
183
|
+
? scriptTestRenderer
|
|
184
|
+
: undefined
|
|
185
|
+
}
|
|
186
|
+
acquireTypes={acquireTypes}
|
|
187
|
+
acquireResetKey={acquireResetKey}
|
|
188
|
+
importablePackages={importablePackages}
|
|
189
|
+
fieldId={id}
|
|
190
|
+
siblingSecretEnv={siblingSecretEnv}
|
|
138
191
|
onChange={onChange as (val: string | undefined) => void}
|
|
139
192
|
/>
|
|
140
193
|
);
|
|
@@ -232,7 +285,14 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
232
285
|
);
|
|
233
286
|
}
|
|
234
287
|
|
|
235
|
-
// Default string input
|
|
288
|
+
// Default string input. When a completion provider is supplied the
|
|
289
|
+
// field is templatable (e.g. automation action config), so render a
|
|
290
|
+
// TemplateValueInput wired to it for `{{ … }}` autocomplete; without
|
|
291
|
+
// one, keep the bare Input so other DynamicForm consumers are
|
|
292
|
+
// unaffected.
|
|
293
|
+
const placeholder = propSchema.default
|
|
294
|
+
? `Default: ${String(propSchema.default)}`
|
|
295
|
+
: "";
|
|
236
296
|
return (
|
|
237
297
|
<div className="space-y-2">
|
|
238
298
|
<div>
|
|
@@ -243,14 +303,22 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
243
303
|
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
244
304
|
)}
|
|
245
305
|
</div>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
306
|
+
{templateCompletionProvider ? (
|
|
307
|
+
<TemplateValueInput
|
|
308
|
+
id={id}
|
|
309
|
+
value={(value as string) || ""}
|
|
310
|
+
onChange={(next) => onChange(next)}
|
|
311
|
+
placeholder={placeholder}
|
|
312
|
+
completionProvider={templateCompletionProvider}
|
|
313
|
+
/>
|
|
314
|
+
) : (
|
|
315
|
+
<Input
|
|
316
|
+
id={id}
|
|
317
|
+
value={(value as string) || ""}
|
|
318
|
+
onChange={(e) => onChange(e.target.value)}
|
|
319
|
+
placeholder={placeholder}
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
254
322
|
</div>
|
|
255
323
|
);
|
|
256
324
|
}
|
|
@@ -314,6 +382,34 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
314
382
|
}
|
|
315
383
|
|
|
316
384
|
// Dictionary/Record (headers)
|
|
385
|
+
// Secret -> env mapping: a dedicated editor (env name + secret-name
|
|
386
|
+
// picker) instead of the raw JSON record fallback.
|
|
387
|
+
if (
|
|
388
|
+
propSchema.type === "object" &&
|
|
389
|
+
propSchema.additionalProperties &&
|
|
390
|
+
propSchema["x-secret-env"]
|
|
391
|
+
) {
|
|
392
|
+
const cleanDesc = getCleanDescription(description);
|
|
393
|
+
return (
|
|
394
|
+
<div className="space-y-2">
|
|
395
|
+
<div>
|
|
396
|
+
<Label htmlFor={id}>
|
|
397
|
+
{label} {isRequired && "*"}
|
|
398
|
+
</Label>
|
|
399
|
+
{cleanDesc && (
|
|
400
|
+
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
<SecretEnvEditor
|
|
404
|
+
id={id}
|
|
405
|
+
value={(value as Record<string, string> | undefined) ?? {}}
|
|
406
|
+
secretNames={secretNames}
|
|
407
|
+
onChange={(next) => onChange(next)}
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
317
413
|
if (propSchema.type === "object" && propSchema.additionalProperties) {
|
|
318
414
|
const cleanDesc = getCleanDescription(description);
|
|
319
415
|
return (
|
|
@@ -338,6 +434,12 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
338
434
|
|
|
339
435
|
// Object (Nested Form)
|
|
340
436
|
if (propSchema.type === "object" && propSchema.properties) {
|
|
437
|
+
// Resolve the secret→env sibling within THIS object so a nested
|
|
438
|
+
// testable script field forwards the right mapping to the test panel.
|
|
439
|
+
const nestedSecretEnv = findSecretEnvSibling({
|
|
440
|
+
properties: propSchema.properties,
|
|
441
|
+
values: value as Record<string, unknown> | undefined,
|
|
442
|
+
});
|
|
341
443
|
return (
|
|
342
444
|
<div className="space-y-4 p-4 border rounded-lg bg-muted/30">
|
|
343
445
|
<p className="text-sm font-semibold">{label}</p>
|
|
@@ -352,9 +454,16 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
352
454
|
formValues={formValues}
|
|
353
455
|
optionsResolvers={optionsResolvers}
|
|
354
456
|
templateProperties={templateProperties}
|
|
457
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
355
458
|
typeDefinitions={typeDefinitions}
|
|
356
459
|
shellEnvVars={shellEnvVars}
|
|
357
460
|
starterTemplates={starterTemplates}
|
|
461
|
+
scriptTestRenderer={scriptTestRenderer}
|
|
462
|
+
secretNames={secretNames}
|
|
463
|
+
acquireTypes={acquireTypes}
|
|
464
|
+
acquireResetKey={acquireResetKey}
|
|
465
|
+
importablePackages={importablePackages}
|
|
466
|
+
siblingSecretEnv={nestedSecretEnv}
|
|
358
467
|
onChange={(val) =>
|
|
359
468
|
onChange({ ...(value as Record<string, unknown>), [key]: val })
|
|
360
469
|
}
|
|
@@ -463,9 +572,15 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
463
572
|
formValues={formValues}
|
|
464
573
|
optionsResolvers={optionsResolvers}
|
|
465
574
|
templateProperties={templateProperties}
|
|
575
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
466
576
|
typeDefinitions={typeDefinitions}
|
|
467
577
|
shellEnvVars={shellEnvVars}
|
|
468
578
|
starterTemplates={starterTemplates}
|
|
579
|
+
scriptTestRenderer={scriptTestRenderer}
|
|
580
|
+
secretNames={secretNames}
|
|
581
|
+
acquireTypes={acquireTypes}
|
|
582
|
+
acquireResetKey={acquireResetKey}
|
|
583
|
+
importablePackages={importablePackages}
|
|
469
584
|
onChange={(val) => {
|
|
470
585
|
const next = [...(items as unknown[])];
|
|
471
586
|
next[index] = val;
|
|
@@ -524,6 +639,12 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
524
639
|
const displayDiscriminatorField =
|
|
525
640
|
discriminatorField.charAt(0).toUpperCase() + discriminatorField.slice(1);
|
|
526
641
|
|
|
642
|
+
// Secret→env sibling within the selected variant's object.
|
|
643
|
+
const variantSecretEnv = findSecretEnvSibling({
|
|
644
|
+
properties: selectedVariant.properties,
|
|
645
|
+
values: currentValue,
|
|
646
|
+
});
|
|
647
|
+
|
|
527
648
|
return (
|
|
528
649
|
<div className="space-y-3 p-3 border rounded-lg bg-background">
|
|
529
650
|
{/* Discriminator selector */}
|
|
@@ -594,9 +715,16 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
594
715
|
formValues={formValues}
|
|
595
716
|
optionsResolvers={optionsResolvers}
|
|
596
717
|
templateProperties={templateProperties}
|
|
718
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
597
719
|
typeDefinitions={typeDefinitions}
|
|
598
720
|
shellEnvVars={shellEnvVars}
|
|
599
721
|
starterTemplates={starterTemplates}
|
|
722
|
+
scriptTestRenderer={scriptTestRenderer}
|
|
723
|
+
secretNames={secretNames}
|
|
724
|
+
acquireTypes={acquireTypes}
|
|
725
|
+
acquireResetKey={acquireResetKey}
|
|
726
|
+
importablePackages={importablePackages}
|
|
727
|
+
siblingSecretEnv={variantSecretEnv}
|
|
600
728
|
onChange={(val) => onChange({ ...currentValue, [key]: val })}
|
|
601
729
|
/>
|
|
602
730
|
))}
|
|
@@ -3,6 +3,7 @@ import { Plus, Trash2 } from "lucide-react";
|
|
|
3
3
|
import { Button } from "../Button";
|
|
4
4
|
import { Input } from "../Input";
|
|
5
5
|
import type { TemplateProperty } from "../CodeEditor";
|
|
6
|
+
import { TemplateValueInput } from "../TemplateValueInput";
|
|
6
7
|
|
|
7
8
|
export interface KeyValuePair {
|
|
8
9
|
key: string;
|
|
@@ -28,24 +29,6 @@ export interface KeyValueEditorProps {
|
|
|
28
29
|
templateProperties?: TemplateProperty[];
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
* Detect if cursor is inside an unclosed {{ template context.
|
|
33
|
-
*/
|
|
34
|
-
function detectTemplateContext(text: string, cursorPos: number) {
|
|
35
|
-
const textBefore = text.slice(0, cursorPos);
|
|
36
|
-
const lastOpenBrace = textBefore.lastIndexOf("{{");
|
|
37
|
-
const lastCloseBrace = textBefore.lastIndexOf("}}");
|
|
38
|
-
|
|
39
|
-
if (lastOpenBrace !== -1 && lastOpenBrace > lastCloseBrace) {
|
|
40
|
-
return {
|
|
41
|
-
isInTemplate: true,
|
|
42
|
-
query: textBefore.slice(lastOpenBrace + 2),
|
|
43
|
-
startPos: lastOpenBrace,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
return { isInTemplate: false, query: "", startPos: -1 };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
32
|
/**
|
|
50
33
|
* A key/value pair editor for form data and similar use cases.
|
|
51
34
|
* Supports adding/removing pairs and optional template autocomplete in values.
|
|
@@ -131,7 +114,7 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
|
|
|
131
114
|
className="flex-1 font-mono text-sm"
|
|
132
115
|
/>
|
|
133
116
|
<span className="text-muted-foreground">=</span>
|
|
134
|
-
<
|
|
117
|
+
<TemplateValueInput
|
|
135
118
|
id={`${id}-value-${index}`}
|
|
136
119
|
value={pair.value}
|
|
137
120
|
onChange={(newValue) => handleValueChange(index, newValue)}
|
|
@@ -162,153 +145,3 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
|
|
|
162
145
|
</div>
|
|
163
146
|
);
|
|
164
147
|
};
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* An input with simple template autocomplete support.
|
|
168
|
-
* Shows a dropdown when user types "{{".
|
|
169
|
-
*/
|
|
170
|
-
const TemplateInput: React.FC<{
|
|
171
|
-
id: string;
|
|
172
|
-
value: string;
|
|
173
|
-
onChange: (value: string) => void;
|
|
174
|
-
placeholder?: string;
|
|
175
|
-
templateProperties?: TemplateProperty[];
|
|
176
|
-
}> = ({ id, value, onChange, placeholder, templateProperties }) => {
|
|
177
|
-
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
178
|
-
const [showPopup, setShowPopup] = React.useState(false);
|
|
179
|
-
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
180
|
-
const [templateContext, setTemplateContext] = React.useState<{
|
|
181
|
-
query: string;
|
|
182
|
-
startPos: number;
|
|
183
|
-
}>({ query: "", startPos: -1 });
|
|
184
|
-
|
|
185
|
-
// Filter properties based on query
|
|
186
|
-
const filteredProperties = React.useMemo(() => {
|
|
187
|
-
if (!templateProperties) return [];
|
|
188
|
-
if (!templateContext.query.trim()) return templateProperties;
|
|
189
|
-
const lowerQuery = templateContext.query.toLowerCase();
|
|
190
|
-
return templateProperties.filter((prop) =>
|
|
191
|
-
prop.path.toLowerCase().includes(lowerQuery),
|
|
192
|
-
);
|
|
193
|
-
}, [templateProperties, templateContext.query]);
|
|
194
|
-
|
|
195
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
196
|
-
const newValue = e.target.value;
|
|
197
|
-
onChange(newValue);
|
|
198
|
-
|
|
199
|
-
if (!templateProperties || templateProperties.length === 0) return;
|
|
200
|
-
|
|
201
|
-
const cursorPos = e.target.selectionStart ?? newValue.length;
|
|
202
|
-
const context = detectTemplateContext(newValue, cursorPos);
|
|
203
|
-
|
|
204
|
-
if (context.isInTemplate) {
|
|
205
|
-
setTemplateContext({ query: context.query, startPos: context.startPos });
|
|
206
|
-
setShowPopup(true);
|
|
207
|
-
setSelectedIndex(0);
|
|
208
|
-
} else {
|
|
209
|
-
setShowPopup(false);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const insertProperty = (prop: TemplateProperty) => {
|
|
214
|
-
if (templateContext.startPos === -1) return;
|
|
215
|
-
|
|
216
|
-
const cursorPos = inputRef.current?.selectionStart ?? value.length;
|
|
217
|
-
const template = `{{${prop.path}}}`;
|
|
218
|
-
const newValue =
|
|
219
|
-
value.slice(0, templateContext.startPos) +
|
|
220
|
-
template +
|
|
221
|
-
value.slice(cursorPos);
|
|
222
|
-
|
|
223
|
-
onChange(newValue);
|
|
224
|
-
setShowPopup(false);
|
|
225
|
-
|
|
226
|
-
// Restore focus
|
|
227
|
-
setTimeout(() => {
|
|
228
|
-
inputRef.current?.focus();
|
|
229
|
-
const newPos = templateContext.startPos + template.length;
|
|
230
|
-
inputRef.current?.setSelectionRange(newPos, newPos);
|
|
231
|
-
}, 0);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
235
|
-
if (!showPopup || filteredProperties.length === 0) return;
|
|
236
|
-
|
|
237
|
-
switch (e.key) {
|
|
238
|
-
case "ArrowDown": {
|
|
239
|
-
e.preventDefault();
|
|
240
|
-
setSelectedIndex((prev) =>
|
|
241
|
-
prev < filteredProperties.length - 1 ? prev + 1 : 0,
|
|
242
|
-
);
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case "ArrowUp": {
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
setSelectedIndex((prev) =>
|
|
248
|
-
prev > 0 ? prev - 1 : filteredProperties.length - 1,
|
|
249
|
-
);
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
case "Enter":
|
|
253
|
-
case "Tab": {
|
|
254
|
-
e.preventDefault();
|
|
255
|
-
if (filteredProperties[selectedIndex]) {
|
|
256
|
-
insertProperty(filteredProperties[selectedIndex]);
|
|
257
|
-
}
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
case "Escape": {
|
|
261
|
-
e.preventDefault();
|
|
262
|
-
setShowPopup(false);
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
// Close popup on blur
|
|
269
|
-
const handleBlur = () => {
|
|
270
|
-
// Delay to allow click on popup item
|
|
271
|
-
setTimeout(() => setShowPopup(false), 150);
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
return (
|
|
275
|
-
<div className="relative flex-1">
|
|
276
|
-
<Input
|
|
277
|
-
ref={inputRef}
|
|
278
|
-
id={id}
|
|
279
|
-
value={value}
|
|
280
|
-
onChange={handleChange}
|
|
281
|
-
onKeyDown={handleKeyDown}
|
|
282
|
-
onBlur={handleBlur}
|
|
283
|
-
placeholder={placeholder}
|
|
284
|
-
className="font-mono text-sm"
|
|
285
|
-
/>
|
|
286
|
-
{showPopup && filteredProperties.length > 0 && (
|
|
287
|
-
<div className="absolute z-50 top-full left-0 mt-1 w-64 max-h-48 overflow-y-auto rounded-md border border-border bg-popover shadow-lg">
|
|
288
|
-
<div className="p-1">
|
|
289
|
-
{filteredProperties.map((prop, index) => (
|
|
290
|
-
<button
|
|
291
|
-
key={prop.path}
|
|
292
|
-
type="button"
|
|
293
|
-
onMouseDown={(e) => {
|
|
294
|
-
e.preventDefault();
|
|
295
|
-
insertProperty(prop);
|
|
296
|
-
}}
|
|
297
|
-
className={`w-full flex items-center justify-between gap-2 px-2 py-1.5 text-xs rounded-sm text-left hover:bg-accent hover:text-accent-foreground ${
|
|
298
|
-
index === selectedIndex
|
|
299
|
-
? "bg-accent text-accent-foreground"
|
|
300
|
-
: ""
|
|
301
|
-
}`}
|
|
302
|
-
>
|
|
303
|
-
<code className="font-mono truncate">{prop.path}</code>
|
|
304
|
-
<span className="text-muted-foreground shrink-0">
|
|
305
|
-
{prop.type}
|
|
306
|
-
</span>
|
|
307
|
-
</button>
|
|
308
|
-
))}
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
311
|
-
)}
|
|
312
|
-
</div>
|
|
313
|
-
);
|
|
314
|
-
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Label } from "../Label";
|
|
3
3
|
import { Textarea } from "../Textarea";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CodeEditor,
|
|
6
|
+
type TemplateProperty,
|
|
7
|
+
type AcquireTypes,
|
|
8
|
+
} from "../CodeEditor";
|
|
5
9
|
import {
|
|
6
10
|
Select,
|
|
7
11
|
SelectContent,
|
|
@@ -17,7 +21,11 @@ import {
|
|
|
17
21
|
parseFormData,
|
|
18
22
|
EDITOR_TYPE_LABELS,
|
|
19
23
|
} from "./utils";
|
|
20
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
EditorStarterTemplates,
|
|
26
|
+
ScriptTestRenderer,
|
|
27
|
+
ShellEnvVar,
|
|
28
|
+
} from "./types";
|
|
21
29
|
import { pickStarterForEmptyField } from "./starterTemplateSelector";
|
|
22
30
|
|
|
23
31
|
export interface MultiTypeEditorFieldProps {
|
|
@@ -54,6 +62,34 @@ export interface MultiTypeEditorFieldProps {
|
|
|
54
62
|
* working example instead of a blank canvas.
|
|
55
63
|
*/
|
|
56
64
|
starterTemplates?: EditorStarterTemplates;
|
|
65
|
+
/**
|
|
66
|
+
* Optional renderer for the inline script-test panel. When supplied (the
|
|
67
|
+
* field is `x-script-testable`), it is rendered beneath the editor for
|
|
68
|
+
* `typescript` / `shell` modes, with the currently-selected language as
|
|
69
|
+
* `kind`. The owning page wires it to the `testScript` RPC.
|
|
70
|
+
*/
|
|
71
|
+
scriptTestRenderer?: ScriptTestRenderer;
|
|
72
|
+
/**
|
|
73
|
+
* Optional lazy type-acquisition resolver, forwarded to the TS/JS
|
|
74
|
+
* `CodeEditor` so imported npm packages autocomplete (lazy ATA).
|
|
75
|
+
*/
|
|
76
|
+
acquireTypes?: AcquireTypes;
|
|
77
|
+
/** Install identity (lockfile hash); resets acquired types on a new install. */
|
|
78
|
+
acquireResetKey?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Importable installed package names (`@types/*`-free), forwarded to the
|
|
81
|
+
* TS/JS `CodeEditor` so the import specifier itself autocompletes.
|
|
82
|
+
*/
|
|
83
|
+
importablePackages?: string[];
|
|
84
|
+
/** Form key of this field, forwarded to {@link scriptTestRenderer}. */
|
|
85
|
+
fieldId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Current value of the sibling `x-secret-env` mapping (located by
|
|
88
|
+
* annotation in the parent config object), forwarded to
|
|
89
|
+
* {@link scriptTestRenderer} so the test panel injects placeholders /
|
|
90
|
+
* overrides for the same secrets the action declares.
|
|
91
|
+
*/
|
|
92
|
+
siblingSecretEnv?: Record<string, string>;
|
|
57
93
|
/** Callback when value changes */
|
|
58
94
|
onChange: (value: string | undefined) => void;
|
|
59
95
|
}
|
|
@@ -74,6 +110,12 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
74
110
|
typeDefinitions,
|
|
75
111
|
shellEnvVars,
|
|
76
112
|
starterTemplates,
|
|
113
|
+
scriptTestRenderer,
|
|
114
|
+
acquireTypes,
|
|
115
|
+
acquireResetKey,
|
|
116
|
+
importablePackages,
|
|
117
|
+
fieldId,
|
|
118
|
+
siblingSecretEnv,
|
|
77
119
|
onChange,
|
|
78
120
|
}) => {
|
|
79
121
|
// Detect initial editor type from value
|
|
@@ -336,6 +378,14 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
336
378
|
/>
|
|
337
379
|
)}
|
|
338
380
|
|
|
381
|
+
{/*
|
|
382
|
+
Native-code editors (javascript / typescript / shell) intentionally
|
|
383
|
+
do NOT receive `templateProperties`: `{{ }}` template syntax is for
|
|
384
|
+
text/markup fields only. Code fields access run context through
|
|
385
|
+
their language's native mechanism instead — a typed `context`
|
|
386
|
+
object (driven by `typeDefinitions`) for TS/JS, and `$`-prefixed
|
|
387
|
+
env vars (driven by `shellEnvVars`) for shell.
|
|
388
|
+
*/}
|
|
339
389
|
{selectedType === "javascript" && (
|
|
340
390
|
<CodeEditor
|
|
341
391
|
id={id}
|
|
@@ -343,8 +393,10 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
343
393
|
onChange={onChange}
|
|
344
394
|
language="javascript"
|
|
345
395
|
minHeight="150px"
|
|
346
|
-
templateProperties={templateProperties}
|
|
347
396
|
typeDefinitions={typeDefinitions}
|
|
397
|
+
acquireTypes={acquireTypes}
|
|
398
|
+
acquireResetKey={acquireResetKey}
|
|
399
|
+
importablePackages={importablePackages}
|
|
348
400
|
/>
|
|
349
401
|
)}
|
|
350
402
|
|
|
@@ -355,8 +407,10 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
355
407
|
onChange={onChange}
|
|
356
408
|
language="typescript"
|
|
357
409
|
minHeight="150px"
|
|
358
|
-
templateProperties={templateProperties}
|
|
359
410
|
typeDefinitions={typeDefinitions}
|
|
411
|
+
acquireTypes={acquireTypes}
|
|
412
|
+
acquireResetKey={acquireResetKey}
|
|
413
|
+
importablePackages={importablePackages}
|
|
360
414
|
/>
|
|
361
415
|
)}
|
|
362
416
|
|
|
@@ -367,10 +421,26 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
367
421
|
onChange={onChange}
|
|
368
422
|
language="shell"
|
|
369
423
|
minHeight="150px"
|
|
370
|
-
templateProperties={templateProperties}
|
|
371
424
|
shellEnvVars={shellEnvVars}
|
|
372
425
|
/>
|
|
373
426
|
)}
|
|
427
|
+
|
|
428
|
+
{/* Inline script-test panel for testable code fields. Shell maps to
|
|
429
|
+
the `shell` test kind; both typescript and javascript map to the
|
|
430
|
+
`typescript` runner kind (the ESM runner handles both). */}
|
|
431
|
+
{scriptTestRenderer !== undefined &&
|
|
432
|
+
(selectedType === "typescript" ||
|
|
433
|
+
selectedType === "javascript" ||
|
|
434
|
+
selectedType === "shell") &&
|
|
435
|
+
scriptTestRenderer({
|
|
436
|
+
fieldId: fieldId ?? id,
|
|
437
|
+
kind: selectedType === "shell" ? "shell" : "typescript",
|
|
438
|
+
script: value ?? "",
|
|
439
|
+
secretEnv:
|
|
440
|
+
siblingSecretEnv && Object.keys(siblingSecretEnv).length > 0
|
|
441
|
+
? siblingSecretEnv
|
|
442
|
+
: undefined,
|
|
443
|
+
})}
|
|
374
444
|
</div>
|
|
375
445
|
);
|
|
376
446
|
};
|
|
@@ -417,8 +487,10 @@ const RawEditor: React.FC<{
|
|
|
417
487
|
if (!templateProperties) return [];
|
|
418
488
|
if (!templateContext.query.trim()) return templateProperties;
|
|
419
489
|
const lowerQuery = templateContext.query.toLowerCase();
|
|
420
|
-
return templateProperties.filter(
|
|
421
|
-
prop
|
|
490
|
+
return templateProperties.filter(
|
|
491
|
+
(prop) =>
|
|
492
|
+
prop.path.toLowerCase().includes(lowerQuery) ||
|
|
493
|
+
(prop.templateRef?.toLowerCase().includes(lowerQuery) ?? false),
|
|
422
494
|
);
|
|
423
495
|
}, [templateProperties, templateContext.query]);
|
|
424
496
|
|
|
@@ -481,7 +553,7 @@ const RawEditor: React.FC<{
|
|
|
481
553
|
const textarea = textareaRef.current;
|
|
482
554
|
if (!textarea || templateContext.startPos === -1) return;
|
|
483
555
|
|
|
484
|
-
const template = `{{${prop.path}}}`;
|
|
556
|
+
const template = `{{${prop.templateRef ?? prop.path}}}`;
|
|
485
557
|
const cursorPos = textarea.selectionStart ?? 0;
|
|
486
558
|
const newValue =
|
|
487
559
|
value.slice(0, templateContext.startPos) +
|
|
@@ -580,7 +652,9 @@ const RawEditor: React.FC<{
|
|
|
580
652
|
: ""
|
|
581
653
|
}`}
|
|
582
654
|
>
|
|
583
|
-
<code className="font-mono truncate">
|
|
655
|
+
<code className="font-mono truncate">
|
|
656
|
+
{prop.templateRef ?? prop.path}
|
|
657
|
+
</code>
|
|
584
658
|
<span className="text-muted-foreground shrink-0">
|
|
585
659
|
{prop.type}
|
|
586
660
|
</span>
|