@checkstack/ui 1.9.0 → 1.11.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 +417 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +2 -2
- package/src/components/ActionCard.tsx +221 -0
- package/src/components/CodeEditor/CodeEditor.tsx +51 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +868 -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/index.ts +2 -0
- package/src/components/CodeEditor/scriptContext.test.ts +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +109 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -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/DynamicForm/DynamicForm.tsx +2 -0
- package/src/components/DynamicForm/FormField.tsx +29 -9
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +16 -7
- package/src/components/DynamicForm/types.ts +11 -0
- package/src/components/ListEmptyState.tsx +51 -0
- package/src/components/QueryErrorState.tsx +64 -0
- package/src/components/ResponsiveTable.tsx +92 -0
- package/src/components/Skeleton.tsx +39 -0
- 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/VariablePicker.tsx +271 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +10 -0
- package/src/utils/toastTemplates.test.ts +82 -0
- package/src/utils/toastTemplates.ts +47 -0
- package/stories/ActionCard.stories.tsx +62 -0
- package/stories/Alert.stories.tsx +5 -5
- package/stories/ListEmptyState.stories.tsx +48 -0
- package/stories/QueryErrorState.stories.tsx +40 -0
- package/stories/ResponsiveTable.stories.tsx +93 -0
- package/stories/Skeleton.stories.tsx +53 -0
- package/stories/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/stories/toastTemplates.stories.tsx +60 -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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { validateJsonTemplate } from "./validateJsonTemplate";
|
|
3
|
+
|
|
4
|
+
describe("validateJsonTemplate", () => {
|
|
5
|
+
it("accepts valid JSON", () => {
|
|
6
|
+
expect(validateJsonTemplate('{"a": 1, "b": "two"}')).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("accepts empty content", () => {
|
|
10
|
+
expect(validateJsonTemplate("")).toEqual([]);
|
|
11
|
+
expect(validateJsonTemplate(" \n ")).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts a template as a quoted string value", () => {
|
|
15
|
+
expect(
|
|
16
|
+
validateJsonTemplate('{"repo": "{{trigger.payload.repository}}"}'),
|
|
17
|
+
).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts a template embedded inside a string", () => {
|
|
21
|
+
expect(validateJsonTemplate('{"msg": "build {{id}} done"}')).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("accepts an UNQUOTED template value (e.g. a number)", () => {
|
|
25
|
+
// This is the case a plain JSON validator would reject.
|
|
26
|
+
expect(
|
|
27
|
+
validateJsonTemplate('{"timeout": {{trigger.payload.timeout}}}'),
|
|
28
|
+
).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("accepts a template as an array element", () => {
|
|
32
|
+
expect(validateJsonTemplate('{"items": [{{count}}, 2]}')).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("flags a genuine structural error (missing comma)", () => {
|
|
36
|
+
const diagnostics = validateJsonTemplate('{"a": 1 "b": 2}');
|
|
37
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("flags an unclosed object", () => {
|
|
41
|
+
const diagnostics = validateJsonTemplate('{"a": 1');
|
|
42
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("flags a genuine error even when templates are present, at the right offset", () => {
|
|
46
|
+
// The `2` is missing its preceding colon; offset must point into the
|
|
47
|
+
// ORIGINAL text (same-length substitution preserves offsets).
|
|
48
|
+
const text = '{"a": {{x}}, "b" 2}';
|
|
49
|
+
const diagnostics = validateJsonTemplate(text);
|
|
50
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
51
|
+
// The error should land at/after the `2`, not inside the template.
|
|
52
|
+
const firstOffset = diagnostics[0]?.offset ?? -1;
|
|
53
|
+
expect(text.slice(firstOffset, firstOffset + 1)).toBe("2");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("does not let an unclosed `{{` swallow a later template", () => {
|
|
57
|
+
// `{{` on its own is not a complete template, so substitution leaves it;
|
|
58
|
+
// the real `{{y}}` value is still substituted and the JSON stays valid.
|
|
59
|
+
expect(validateJsonTemplate('{"a": "{{", "b": {{y}}}')).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Template-aware JSON validation. See templateValidation.ts for the approach.
|
|
2
|
+
// The real VS Code JSON service still provides highlighting + completion; only
|
|
3
|
+
// its built-in (raw-text) validation is turned off in favour of this, so `{{ }}`
|
|
4
|
+
// is tolerated in any position (including unquoted, e.g. a numeric value).
|
|
5
|
+
import { parse, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
6
|
+
import {
|
|
7
|
+
substituteTemplates,
|
|
8
|
+
type TemplateDiagnostic,
|
|
9
|
+
} from "./templateValidation";
|
|
10
|
+
|
|
11
|
+
/** Validate template-bearing JSON. Empty content is allowed (no diagnostics). */
|
|
12
|
+
export const validateJsonTemplate = (
|
|
13
|
+
text: string,
|
|
14
|
+
): TemplateDiagnostic[] => {
|
|
15
|
+
const errors: ParseError[] = [];
|
|
16
|
+
parse(substituteTemplates(text), errors, {
|
|
17
|
+
allowEmptyContent: true,
|
|
18
|
+
allowTrailingComma: false,
|
|
19
|
+
disallowComments: true,
|
|
20
|
+
});
|
|
21
|
+
return errors.map((parseError) => ({
|
|
22
|
+
offset: parseError.offset,
|
|
23
|
+
length: Math.max(parseError.length, 1),
|
|
24
|
+
message: `JSON: ${printParseErrorCode(parseError.error)}`,
|
|
25
|
+
}));
|
|
26
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { validateXmlTemplate } from "./validateXmlTemplate";
|
|
3
|
+
|
|
4
|
+
describe("validateXmlTemplate", () => {
|
|
5
|
+
it("accepts valid XML", () => {
|
|
6
|
+
expect(validateXmlTemplate("<root><a>1</a></root>")).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("accepts empty content", () => {
|
|
10
|
+
expect(validateXmlTemplate("")).toEqual([]);
|
|
11
|
+
expect(validateXmlTemplate(" \n ")).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts a template in element text", () => {
|
|
15
|
+
expect(
|
|
16
|
+
validateXmlTemplate("<root><repo>{{trigger.payload.repository}}</repo></root>"),
|
|
17
|
+
).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts a template in a (quoted) attribute value", () => {
|
|
21
|
+
expect(validateXmlTemplate('<root attr="{{id}}"></root>')).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("flags a mismatched closing tag", () => {
|
|
25
|
+
const diagnostics = validateXmlTemplate("<a><b></a>");
|
|
26
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
27
|
+
expect(diagnostics[0]?.message).toContain("XML:");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("flags an unclosed tag even with templates present", () => {
|
|
31
|
+
const diagnostics = validateXmlTemplate("<root><x>{{v}}</root>");
|
|
32
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Template-aware XML validation. See templateValidation.ts for the approach.
|
|
2
|
+
// There is no @codingame standalone XML language service (only Monarch
|
|
3
|
+
// highlighting), so we use fast-xml-parser's validator after template
|
|
4
|
+
// substitution. The validator reports the FIRST error only, with 1-based
|
|
5
|
+
// line/column, which we convert to an offset/range in the original text.
|
|
6
|
+
import { XMLValidator } from "fast-xml-parser";
|
|
7
|
+
import {
|
|
8
|
+
lineColumnToOffset,
|
|
9
|
+
substituteTemplates,
|
|
10
|
+
type TemplateDiagnostic,
|
|
11
|
+
} from "./templateValidation";
|
|
12
|
+
|
|
13
|
+
/** Validate template-bearing XML. Empty content is allowed (no diagnostics). */
|
|
14
|
+
export const validateXmlTemplate = (
|
|
15
|
+
text: string,
|
|
16
|
+
): TemplateDiagnostic[] => {
|
|
17
|
+
const substituted = substituteTemplates(text);
|
|
18
|
+
if (substituted.trim() === "") {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const result = XMLValidator.validate(substituted, {
|
|
22
|
+
allowBooleanAttributes: true,
|
|
23
|
+
});
|
|
24
|
+
if (result === true) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const { msg, line, col } = result.err;
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
offset: lineColumnToOffset({ text: substituted, line, column: col }),
|
|
31
|
+
length: 1,
|
|
32
|
+
message: `XML: ${msg}`,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { validateYamlTemplate } from "./validateYamlTemplate";
|
|
3
|
+
|
|
4
|
+
describe("validateYamlTemplate", () => {
|
|
5
|
+
it("accepts valid YAML", () => {
|
|
6
|
+
expect(validateYamlTemplate("a: 1\nb: two\n")).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("accepts empty content", () => {
|
|
10
|
+
expect(validateYamlTemplate("")).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("accepts a template as a quoted string value", () => {
|
|
14
|
+
expect(validateYamlTemplate('repo: "{{trigger.payload.repository}}"')).toEqual(
|
|
15
|
+
[],
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("accepts an UNQUOTED template value (e.g. a number)", () => {
|
|
20
|
+
expect(validateYamlTemplate("timeout: {{trigger.payload.timeout}}")).toEqual(
|
|
21
|
+
[],
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("accepts a template as a list item", () => {
|
|
26
|
+
expect(validateYamlTemplate("items:\n - {{count}}\n - 2")).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("flags tabs used as indentation", () => {
|
|
30
|
+
const diagnostics = validateYamlTemplate("foo:\n\tbar: 1");
|
|
31
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
32
|
+
expect(diagnostics[0]?.message).toContain("YAML:");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("flags a genuine structural error (with templates present)", () => {
|
|
36
|
+
const diagnostics = validateYamlTemplate("a: {{x}}\n b: 2");
|
|
37
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Template-aware YAML validation. See templateValidation.ts for the approach.
|
|
2
|
+
// There is no @codingame standalone YAML language service (only Monarch
|
|
3
|
+
// highlighting), so we parse with the `yaml` package after template
|
|
4
|
+
// substitution. YAML is permissive (it often parses `{{ }}` as a flow
|
|
5
|
+
// collection), but substitution gives accurate structural validation and still
|
|
6
|
+
// catches real mistakes (bad indentation, tabs, duplicate keys, ...).
|
|
7
|
+
import { parseDocument } from "yaml";
|
|
8
|
+
import {
|
|
9
|
+
substituteTemplates,
|
|
10
|
+
type TemplateDiagnostic,
|
|
11
|
+
} from "./templateValidation";
|
|
12
|
+
|
|
13
|
+
/** Validate template-bearing YAML. */
|
|
14
|
+
export const validateYamlTemplate = (
|
|
15
|
+
text: string,
|
|
16
|
+
): TemplateDiagnostic[] => {
|
|
17
|
+
const doc = parseDocument(substituteTemplates(text), {
|
|
18
|
+
prettyErrors: false,
|
|
19
|
+
});
|
|
20
|
+
return doc.errors.map((error) => {
|
|
21
|
+
const [start, end] = error.pos;
|
|
22
|
+
return {
|
|
23
|
+
offset: start,
|
|
24
|
+
length: Math.max(end - start, 1),
|
|
25
|
+
message: `YAML: ${error.message}`,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -18,6 +18,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
18
18
|
onValidChange,
|
|
19
19
|
optionsResolvers,
|
|
20
20
|
templateProperties,
|
|
21
|
+
templateCompletionProvider,
|
|
21
22
|
typeDefinitions,
|
|
22
23
|
shellEnvVars,
|
|
23
24
|
starterTemplates,
|
|
@@ -116,6 +117,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
116
117
|
formValues={value}
|
|
117
118
|
optionsResolvers={optionsResolvers}
|
|
118
119
|
templateProperties={templateProperties}
|
|
120
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
119
121
|
typeDefinitions={typeDefinitions}
|
|
120
122
|
shellEnvVars={shellEnvVars}
|
|
121
123
|
starterTemplates={starterTemplates}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Textarea,
|
|
14
14
|
Toggle,
|
|
15
15
|
ColorPicker,
|
|
16
|
+
TemplateValueInput,
|
|
16
17
|
} from "../../index";
|
|
17
18
|
|
|
18
19
|
import type { FormFieldProps, JsonSchemaProperty } from "./types";
|
|
@@ -33,6 +34,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
33
34
|
formValues,
|
|
34
35
|
optionsResolvers,
|
|
35
36
|
templateProperties,
|
|
37
|
+
templateCompletionProvider,
|
|
36
38
|
typeDefinitions,
|
|
37
39
|
shellEnvVars,
|
|
38
40
|
starterTemplates,
|
|
@@ -232,7 +234,14 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
232
234
|
);
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
// Default string input
|
|
237
|
+
// Default string input. When a completion provider is supplied the
|
|
238
|
+
// field is templatable (e.g. automation action config), so render a
|
|
239
|
+
// TemplateValueInput wired to it for `{{ … }}` autocomplete; without
|
|
240
|
+
// one, keep the bare Input so other DynamicForm consumers are
|
|
241
|
+
// unaffected.
|
|
242
|
+
const placeholder = propSchema.default
|
|
243
|
+
? `Default: ${String(propSchema.default)}`
|
|
244
|
+
: "";
|
|
236
245
|
return (
|
|
237
246
|
<div className="space-y-2">
|
|
238
247
|
<div>
|
|
@@ -243,14 +252,22 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
243
252
|
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
244
253
|
)}
|
|
245
254
|
</div>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
{templateCompletionProvider ? (
|
|
256
|
+
<TemplateValueInput
|
|
257
|
+
id={id}
|
|
258
|
+
value={(value as string) || ""}
|
|
259
|
+
onChange={(next) => onChange(next)}
|
|
260
|
+
placeholder={placeholder}
|
|
261
|
+
completionProvider={templateCompletionProvider}
|
|
262
|
+
/>
|
|
263
|
+
) : (
|
|
264
|
+
<Input
|
|
265
|
+
id={id}
|
|
266
|
+
value={(value as string) || ""}
|
|
267
|
+
onChange={(e) => onChange(e.target.value)}
|
|
268
|
+
placeholder={placeholder}
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
254
271
|
</div>
|
|
255
272
|
);
|
|
256
273
|
}
|
|
@@ -352,6 +369,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
352
369
|
formValues={formValues}
|
|
353
370
|
optionsResolvers={optionsResolvers}
|
|
354
371
|
templateProperties={templateProperties}
|
|
372
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
355
373
|
typeDefinitions={typeDefinitions}
|
|
356
374
|
shellEnvVars={shellEnvVars}
|
|
357
375
|
starterTemplates={starterTemplates}
|
|
@@ -463,6 +481,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
463
481
|
formValues={formValues}
|
|
464
482
|
optionsResolvers={optionsResolvers}
|
|
465
483
|
templateProperties={templateProperties}
|
|
484
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
466
485
|
typeDefinitions={typeDefinitions}
|
|
467
486
|
shellEnvVars={shellEnvVars}
|
|
468
487
|
starterTemplates={starterTemplates}
|
|
@@ -594,6 +613,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
594
613
|
formValues={formValues}
|
|
595
614
|
optionsResolvers={optionsResolvers}
|
|
596
615
|
templateProperties={templateProperties}
|
|
616
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
597
617
|
typeDefinitions={typeDefinitions}
|
|
598
618
|
shellEnvVars={shellEnvVars}
|
|
599
619
|
starterTemplates={starterTemplates}
|
|
@@ -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
|
-
};
|
|
@@ -336,6 +336,14 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
336
336
|
/>
|
|
337
337
|
)}
|
|
338
338
|
|
|
339
|
+
{/*
|
|
340
|
+
Native-code editors (javascript / typescript / shell) intentionally
|
|
341
|
+
do NOT receive `templateProperties`: `{{ }}` template syntax is for
|
|
342
|
+
text/markup fields only. Code fields access run context through
|
|
343
|
+
their language's native mechanism instead — a typed `context`
|
|
344
|
+
object (driven by `typeDefinitions`) for TS/JS, and `$`-prefixed
|
|
345
|
+
env vars (driven by `shellEnvVars`) for shell.
|
|
346
|
+
*/}
|
|
339
347
|
{selectedType === "javascript" && (
|
|
340
348
|
<CodeEditor
|
|
341
349
|
id={id}
|
|
@@ -343,7 +351,6 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
343
351
|
onChange={onChange}
|
|
344
352
|
language="javascript"
|
|
345
353
|
minHeight="150px"
|
|
346
|
-
templateProperties={templateProperties}
|
|
347
354
|
typeDefinitions={typeDefinitions}
|
|
348
355
|
/>
|
|
349
356
|
)}
|
|
@@ -355,7 +362,6 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
355
362
|
onChange={onChange}
|
|
356
363
|
language="typescript"
|
|
357
364
|
minHeight="150px"
|
|
358
|
-
templateProperties={templateProperties}
|
|
359
365
|
typeDefinitions={typeDefinitions}
|
|
360
366
|
/>
|
|
361
367
|
)}
|
|
@@ -367,7 +373,6 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
367
373
|
onChange={onChange}
|
|
368
374
|
language="shell"
|
|
369
375
|
minHeight="150px"
|
|
370
|
-
templateProperties={templateProperties}
|
|
371
376
|
shellEnvVars={shellEnvVars}
|
|
372
377
|
/>
|
|
373
378
|
)}
|
|
@@ -417,8 +422,10 @@ const RawEditor: React.FC<{
|
|
|
417
422
|
if (!templateProperties) return [];
|
|
418
423
|
if (!templateContext.query.trim()) return templateProperties;
|
|
419
424
|
const lowerQuery = templateContext.query.toLowerCase();
|
|
420
|
-
return templateProperties.filter(
|
|
421
|
-
prop
|
|
425
|
+
return templateProperties.filter(
|
|
426
|
+
(prop) =>
|
|
427
|
+
prop.path.toLowerCase().includes(lowerQuery) ||
|
|
428
|
+
(prop.templateRef?.toLowerCase().includes(lowerQuery) ?? false),
|
|
422
429
|
);
|
|
423
430
|
}, [templateProperties, templateContext.query]);
|
|
424
431
|
|
|
@@ -481,7 +488,7 @@ const RawEditor: React.FC<{
|
|
|
481
488
|
const textarea = textareaRef.current;
|
|
482
489
|
if (!textarea || templateContext.startPos === -1) return;
|
|
483
490
|
|
|
484
|
-
const template = `{{${prop.path}}}`;
|
|
491
|
+
const template = `{{${prop.templateRef ?? prop.path}}}`;
|
|
485
492
|
const cursorPos = textarea.selectionStart ?? 0;
|
|
486
493
|
const newValue =
|
|
487
494
|
value.slice(0, templateContext.startPos) +
|
|
@@ -580,7 +587,9 @@ const RawEditor: React.FC<{
|
|
|
580
587
|
: ""
|
|
581
588
|
}`}
|
|
582
589
|
>
|
|
583
|
-
<code className="font-mono truncate">
|
|
590
|
+
<code className="font-mono truncate">
|
|
591
|
+
{prop.templateRef ?? prop.path}
|
|
592
|
+
</code>
|
|
584
593
|
<span className="text-muted-foreground shrink-0">
|
|
585
594
|
{prop.type}
|
|
586
595
|
</span>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TemplateProperty, ShellEnvVar } from "../CodeEditor";
|
|
2
|
+
import type { TemplateCompletionProvider } from "../TemplateValueInput";
|
|
2
3
|
import type { EditorType } from "@checkstack/common";
|
|
3
4
|
|
|
4
5
|
// Re-export types used by multi-type editor
|
|
@@ -69,6 +70,15 @@ export interface DynamicFormProps {
|
|
|
69
70
|
* When provided, fields with x-editor-types get {{ autocomplete suggestions.
|
|
70
71
|
*/
|
|
71
72
|
templateProperties?: TemplateProperty[];
|
|
73
|
+
/**
|
|
74
|
+
* Optional staged, context-aware completion provider for plain
|
|
75
|
+
* single-line string fields. When supplied, default string inputs
|
|
76
|
+
* render a {@link TemplateValueInput} wired to this provider instead
|
|
77
|
+
* of a bare `Input`, so `{{ … }}` expressions get field / comparator /
|
|
78
|
+
* value / filter autocomplete (the automation editor passes the
|
|
79
|
+
* template-mode provider here). Omit it and string fields stay plain.
|
|
80
|
+
*/
|
|
81
|
+
templateCompletionProvider?: TemplateCompletionProvider;
|
|
72
82
|
/**
|
|
73
83
|
* Optional TypeScript declarations to inject into Monaco for `typescript`
|
|
74
84
|
* or `javascript` editor-type fields. Typically built from a schema via
|
|
@@ -100,6 +110,7 @@ export interface FormFieldProps {
|
|
|
100
110
|
formValues: Record<string, unknown>;
|
|
101
111
|
optionsResolvers?: Record<string, OptionsResolver>;
|
|
102
112
|
templateProperties?: TemplateProperty[];
|
|
113
|
+
templateCompletionProvider?: TemplateCompletionProvider;
|
|
103
114
|
typeDefinitions?: string;
|
|
104
115
|
shellEnvVars?: ShellEnvVar[];
|
|
105
116
|
starterTemplates?: EditorStarterTemplates;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Inbox } from "lucide-react";
|
|
3
|
+
import { EmptyState } from "./EmptyState";
|
|
4
|
+
|
|
5
|
+
interface ListEmptyStateProps {
|
|
6
|
+
/**
|
|
7
|
+
* The name of the resource type the list would display (e.g. `"checks"`,
|
|
8
|
+
* `"incidents"`). Drives the default `"No {resource} yet"` headline.
|
|
9
|
+
*/
|
|
10
|
+
resource: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional supplemental description rendered beneath the headline.
|
|
13
|
+
*/
|
|
14
|
+
description?: React.ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Optional action area, typically a primary CTA button such as
|
|
17
|
+
* "Create your first check".
|
|
18
|
+
*/
|
|
19
|
+
actions?: React.ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Optional icon override. Defaults to the lucide `Inbox` glyph so callers
|
|
22
|
+
* don't have to pick one for every list.
|
|
23
|
+
*/
|
|
24
|
+
icon?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ListEmptyState - the canonical empty state for a list-shaped resource.
|
|
29
|
+
*
|
|
30
|
+
* Thin wrapper around {@link EmptyState} that supplies a consistent
|
|
31
|
+
* "No {resource} yet" headline and a sensible default icon. Use this on
|
|
32
|
+
* any page that renders a list and may have zero items so the UX stays
|
|
33
|
+
* uniform across plugins.
|
|
34
|
+
*/
|
|
35
|
+
export const ListEmptyState: React.FC<ListEmptyStateProps> = ({
|
|
36
|
+
resource,
|
|
37
|
+
description,
|
|
38
|
+
actions,
|
|
39
|
+
icon,
|
|
40
|
+
}) => {
|
|
41
|
+
const resolvedIcon = icon ?? <Inbox className="h-10 w-10" />;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<EmptyState
|
|
45
|
+
title={`No ${resource} yet`}
|
|
46
|
+
description={description}
|
|
47
|
+
icon={resolvedIcon}
|
|
48
|
+
actions={actions}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
};
|