@checkstack/ui 0.2.4 → 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 +30 -0
- package/package.json +10 -3
- package/src/components/CodeEditor/CodeEditor.tsx +420 -0
- package/src/components/CodeEditor/index.ts +10 -0
- package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +173 -0
- package/src/components/CodeEditor/languageSupport/enterBehavior.ts +35 -0
- package/src/components/CodeEditor/languageSupport/index.ts +22 -0
- package/src/components/CodeEditor/languageSupport/json.test.ts +271 -0
- package/src/components/CodeEditor/languageSupport/json.ts +240 -0
- package/src/components/CodeEditor/languageSupport/markdown.test.ts +245 -0
- package/src/components/CodeEditor/languageSupport/markdown.ts +183 -0
- package/src/components/CodeEditor/languageSupport/types.ts +48 -0
- package/src/components/CodeEditor/languageSupport/xml.test.ts +236 -0
- package/src/components/CodeEditor/languageSupport/xml.ts +194 -0
- package/src/components/CodeEditor/languageSupport/yaml.test.ts +200 -0
- package/src/components/CodeEditor/languageSupport/yaml.ts +205 -0
- package/src/components/DynamicForm/DynamicForm.tsx +2 -24
- package/src/components/DynamicForm/DynamicOptionsField.tsx +48 -21
- package/src/components/DynamicForm/FormField.tsx +38 -70
- package/src/components/DynamicForm/JsonField.tsx +19 -25
- package/src/components/DynamicForm/KeyValueEditor.tsx +314 -0
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +465 -0
- package/src/components/DynamicForm/index.ts +13 -0
- package/src/components/DynamicForm/types.ts +14 -8
- package/src/components/DynamicForm/utils.test.ts +390 -0
- package/src/components/DynamicForm/utils.ts +142 -3
- package/src/index.ts +1 -1
- package/src/components/TemplateEditor.test.ts +0 -156
- package/src/components/TemplateEditor.tsx +0 -435
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { yaml } from "@codemirror/lang-yaml";
|
|
2
|
+
import { Decoration } from "@codemirror/view";
|
|
3
|
+
import type { LanguageSupport, DecorationRange } from "./types";
|
|
4
|
+
|
|
5
|
+
// Decoration marks for YAML syntax highlighting using inline styles
|
|
6
|
+
const yamlKeyMark = Decoration.mark({
|
|
7
|
+
attributes: { style: "color: hsl(210, 100%, 75%)" }, // Bright blue for better visibility
|
|
8
|
+
});
|
|
9
|
+
const yamlStringMark = Decoration.mark({
|
|
10
|
+
attributes: { style: "color: hsl(142.1, 76.2%, 36.3%)" },
|
|
11
|
+
});
|
|
12
|
+
const yamlNumberMark = Decoration.mark({
|
|
13
|
+
attributes: { style: "color: hsl(217.2, 91.2%, 59.8%)" },
|
|
14
|
+
});
|
|
15
|
+
const yamlBoolMark = Decoration.mark({
|
|
16
|
+
attributes: { style: "color: hsl(280, 65%, 60%)" },
|
|
17
|
+
});
|
|
18
|
+
const templateMark = Decoration.mark({
|
|
19
|
+
attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if cursor is in a valid YAML template position.
|
|
24
|
+
* In YAML, templates can appear as values after "key: ".
|
|
25
|
+
* @internal Exported for testing
|
|
26
|
+
*/
|
|
27
|
+
export function isValidYamlTemplatePosition(text: string): boolean {
|
|
28
|
+
// Find the current line
|
|
29
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
30
|
+
const currentLine = text.slice(lastNewline + 1);
|
|
31
|
+
|
|
32
|
+
// Valid positions in YAML:
|
|
33
|
+
// 1. After a colon and space (value position): "key: |"
|
|
34
|
+
// 2. Inside a quoted string: 'key: "hello|'
|
|
35
|
+
// 3. In a list item position: "- |"
|
|
36
|
+
|
|
37
|
+
// Check if we're inside a quoted string
|
|
38
|
+
const singleQuotes = (currentLine.match(/'/g) || []).length;
|
|
39
|
+
const doubleQuotes = (currentLine.match(/"/g) || []).length;
|
|
40
|
+
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if we're after "key: " (value position)
|
|
45
|
+
const colonMatch = currentLine.match(/^\s*[\w-]+:\s+/);
|
|
46
|
+
if (colonMatch) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if we're in a list item position "- "
|
|
51
|
+
const listMatch = currentLine.match(/^\s*-\s+/);
|
|
52
|
+
if (listMatch) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if we're after ": " anywhere in the line
|
|
57
|
+
if (currentLine.includes(": ")) {
|
|
58
|
+
const afterColon = currentLine.split(": ").slice(1).join(": ");
|
|
59
|
+
// If we're in the value part (after colon)
|
|
60
|
+
if (afterColon.length > 0 || currentLine.endsWith(": ")) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build YAML + template decorations.
|
|
70
|
+
*/
|
|
71
|
+
function buildYamlDecorations(doc: string): DecorationRange[] {
|
|
72
|
+
const ranges: DecorationRange[] = [];
|
|
73
|
+
|
|
74
|
+
// Match templates first (highest priority)
|
|
75
|
+
const templateRegex = /\{\{[\w.[\]]*\}\}/g;
|
|
76
|
+
let match;
|
|
77
|
+
while ((match = templateRegex.exec(doc)) !== null) {
|
|
78
|
+
ranges.push({
|
|
79
|
+
from: match.index,
|
|
80
|
+
to: match.index + match[0].length,
|
|
81
|
+
decoration: templateMark,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Match YAML keys (word followed by colon)
|
|
86
|
+
const keyRegex = /^(\s*)([\w-]+):/gm;
|
|
87
|
+
while ((match = keyRegex.exec(doc)) !== null) {
|
|
88
|
+
const keyStart = match.index + match[1].length;
|
|
89
|
+
const keyEnd = keyStart + match[2].length;
|
|
90
|
+
ranges.push({
|
|
91
|
+
from: keyStart,
|
|
92
|
+
to: keyEnd,
|
|
93
|
+
decoration: yamlKeyMark,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Match quoted strings
|
|
98
|
+
const stringRegex = /(["'])(?:(?!\1)[^\\]|\\.)*\1/g;
|
|
99
|
+
while ((match = stringRegex.exec(doc)) !== null) {
|
|
100
|
+
ranges.push({
|
|
101
|
+
from: match.index,
|
|
102
|
+
to: match.index + match[0].length,
|
|
103
|
+
decoration: yamlStringMark,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Match numbers (standalone)
|
|
108
|
+
const numberRegex = /(?<=:\s+)-?\d+\.?\d*(?:\s|$)/g;
|
|
109
|
+
while ((match = numberRegex.exec(doc)) !== null) {
|
|
110
|
+
ranges.push({
|
|
111
|
+
from: match.index,
|
|
112
|
+
to: match.index + match[0].trim().length,
|
|
113
|
+
decoration: yamlNumberMark,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Match booleans
|
|
118
|
+
const boolRegex = /(?<=:\s+)(true|false|yes|no|on|off)(?:\s|$)/gi;
|
|
119
|
+
while ((match = boolRegex.exec(doc)) !== null) {
|
|
120
|
+
ranges.push({
|
|
121
|
+
from: match.index,
|
|
122
|
+
to: match.index + match[1].length,
|
|
123
|
+
decoration: yamlBoolMark,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Sort by position
|
|
128
|
+
ranges.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
129
|
+
|
|
130
|
+
// Remove overlaps (templates take priority)
|
|
131
|
+
const filtered: DecorationRange[] = [];
|
|
132
|
+
for (const range of ranges) {
|
|
133
|
+
const overlaps = filtered.some(
|
|
134
|
+
(existing) =>
|
|
135
|
+
(range.from >= existing.from && range.from < existing.to) ||
|
|
136
|
+
(range.to > existing.from && range.to <= existing.to),
|
|
137
|
+
);
|
|
138
|
+
if (!overlaps) {
|
|
139
|
+
filtered.push(range);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return filtered;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Calculate indentation for YAML, properly handling template expressions.
|
|
148
|
+
* YAML uses indentation-based structure.
|
|
149
|
+
* @internal Exported for testing
|
|
150
|
+
*/
|
|
151
|
+
export function calculateYamlIndentation(
|
|
152
|
+
textBefore: string,
|
|
153
|
+
indentUnit: number,
|
|
154
|
+
): number {
|
|
155
|
+
const lines = textBefore.split("\n");
|
|
156
|
+
if (lines.length === 0) return 0;
|
|
157
|
+
|
|
158
|
+
// Get the last line (the one we're currently on after Enter)
|
|
159
|
+
const currentLine = lines.at(-1);
|
|
160
|
+
|
|
161
|
+
// If current line is empty or whitespace, look at previous line
|
|
162
|
+
if (currentLine?.trim().length === 0 && lines.length > 1) {
|
|
163
|
+
// Find the last non-empty line
|
|
164
|
+
for (let i = lines.length - 2; i >= 0; i--) {
|
|
165
|
+
const line = lines[i];
|
|
166
|
+
if (line.trim().length === 0) continue;
|
|
167
|
+
|
|
168
|
+
// Count leading spaces
|
|
169
|
+
const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
170
|
+
|
|
171
|
+
// If line ends with ":" it's a key that starts a new block
|
|
172
|
+
if (line.trimEnd().endsWith(":")) {
|
|
173
|
+
return leadingSpaces + indentUnit;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// If line starts with "- " it's a list item
|
|
177
|
+
if (line.trim().startsWith("-")) {
|
|
178
|
+
return leadingSpaces + indentUnit;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Otherwise, maintain current indentation
|
|
182
|
+
return leadingSpaces;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// For non-empty current line, find previous non-empty for context
|
|
187
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
188
|
+
const line = lines[i];
|
|
189
|
+
if (line.trim().length === 0) continue;
|
|
190
|
+
const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
191
|
+
return leadingSpaces;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* YAML language support for CodeEditor with template expression handling.
|
|
199
|
+
*/
|
|
200
|
+
export const yamlLanguageSupport: LanguageSupport = {
|
|
201
|
+
extension: yaml(),
|
|
202
|
+
buildDecorations: buildYamlDecorations,
|
|
203
|
+
isValidTemplatePosition: isValidYamlTemplatePosition,
|
|
204
|
+
calculateIndentation: calculateYamlIndentation,
|
|
205
|
+
};
|
|
@@ -2,32 +2,10 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { EmptyState } from "../../index";
|
|
4
4
|
|
|
5
|
-
import type { DynamicFormProps
|
|
6
|
-
import { extractDefaults } from "./utils";
|
|
5
|
+
import type { DynamicFormProps } from "./types";
|
|
6
|
+
import { extractDefaults, isValueEmpty } from "./utils";
|
|
7
7
|
import { FormField } from "./FormField";
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Check if a value is considered "empty" for validation purposes.
|
|
11
|
-
*/
|
|
12
|
-
function isValueEmpty(val: unknown, propSchema: JsonSchemaProperty): boolean {
|
|
13
|
-
if (val === undefined || val === null) return true;
|
|
14
|
-
if (typeof val === "string" && val.trim() === "") return true;
|
|
15
|
-
// For arrays, check if empty
|
|
16
|
-
if (Array.isArray(val) && val.length === 0) return true;
|
|
17
|
-
// For objects (nested schemas), recursively check required fields
|
|
18
|
-
if (propSchema.type === "object" && propSchema.properties) {
|
|
19
|
-
const objVal = val as Record<string, unknown>;
|
|
20
|
-
const requiredKeys = propSchema.required ?? [];
|
|
21
|
-
for (const key of requiredKeys) {
|
|
22
|
-
const nestedPropSchema = propSchema.properties[key];
|
|
23
|
-
if (nestedPropSchema && isValueEmpty(objVal[key], nestedPropSchema)) {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
9
|
/**
|
|
32
10
|
* DynamicForm generates a form from a JSON Schema.
|
|
33
11
|
* Supports all standard JSON Schema types plus custom extensions for
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../../index";
|
|
13
13
|
|
|
14
14
|
import type { DynamicOptionsFieldProps, ResolverOption } from "./types";
|
|
15
|
-
import { getCleanDescription } from "./utils";
|
|
15
|
+
import { getCleanDescription, NONE_SENTINEL } from "./utils";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Field component for dynamically resolved options.
|
|
@@ -72,7 +72,7 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
|
|
|
72
72
|
.catch((error_) => {
|
|
73
73
|
if (!cancelled) {
|
|
74
74
|
setError(
|
|
75
|
-
error_ instanceof Error ? error_.message : "Failed to load options"
|
|
75
|
+
error_ instanceof Error ? error_.message : "Failed to load options",
|
|
76
76
|
);
|
|
77
77
|
setLoading(false);
|
|
78
78
|
}
|
|
@@ -140,24 +140,41 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
|
|
|
140
140
|
No matching options
|
|
141
141
|
</div>
|
|
142
142
|
) : (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
<>
|
|
144
|
+
{!isRequired && (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => {
|
|
148
|
+
onChange();
|
|
149
|
+
setOpen(false);
|
|
150
|
+
setSearchQuery("");
|
|
151
|
+
}}
|
|
152
|
+
className={`w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground text-muted-foreground ${
|
|
153
|
+
value ? "" : "bg-accent text-accent-foreground"
|
|
154
|
+
}`}
|
|
155
|
+
>
|
|
156
|
+
None
|
|
157
|
+
</button>
|
|
158
|
+
)}
|
|
159
|
+
{filteredOptions.map((opt) => (
|
|
160
|
+
<button
|
|
161
|
+
key={opt.value}
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={() => {
|
|
164
|
+
onChange(opt.value);
|
|
165
|
+
setOpen(false);
|
|
166
|
+
setSearchQuery("");
|
|
167
|
+
}}
|
|
168
|
+
className={`w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground ${
|
|
169
|
+
opt.value === value
|
|
170
|
+
? "bg-accent text-accent-foreground"
|
|
171
|
+
: ""
|
|
172
|
+
}`}
|
|
173
|
+
>
|
|
174
|
+
{opt.label}
|
|
175
|
+
</button>
|
|
176
|
+
))}
|
|
177
|
+
</>
|
|
161
178
|
)}
|
|
162
179
|
</div>
|
|
163
180
|
</div>
|
|
@@ -193,7 +210,9 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
|
|
|
193
210
|
) : (
|
|
194
211
|
<Select
|
|
195
212
|
value={(value as string) || ""}
|
|
196
|
-
onValueChange={(val) =>
|
|
213
|
+
onValueChange={(val) =>
|
|
214
|
+
onChange(val === NONE_SENTINEL ? undefined : val)
|
|
215
|
+
}
|
|
197
216
|
disabled={options.length === 0}
|
|
198
217
|
>
|
|
199
218
|
<SelectTrigger id={id}>
|
|
@@ -206,6 +225,14 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
|
|
|
206
225
|
/>
|
|
207
226
|
</SelectTrigger>
|
|
208
227
|
<SelectContent>
|
|
228
|
+
{!isRequired && (
|
|
229
|
+
<SelectItem
|
|
230
|
+
value={NONE_SENTINEL}
|
|
231
|
+
className="text-muted-foreground"
|
|
232
|
+
>
|
|
233
|
+
None
|
|
234
|
+
</SelectItem>
|
|
235
|
+
)}
|
|
209
236
|
{options.map((opt) => (
|
|
210
237
|
<SelectItem key={opt.value} value={opt.value}>
|
|
211
238
|
{opt.label}
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
Textarea,
|
|
14
14
|
Toggle,
|
|
15
15
|
ColorPicker,
|
|
16
|
-
TemplateEditor,
|
|
17
16
|
} from "../../index";
|
|
18
17
|
|
|
19
18
|
import type { FormFieldProps, JsonSchemaProperty } from "./types";
|
|
20
|
-
import { getCleanDescription } from "./utils";
|
|
19
|
+
import { getCleanDescription, NONE_SENTINEL } from "./utils";
|
|
21
20
|
import { DynamicOptionsField } from "./DynamicOptionsField";
|
|
22
21
|
import { JsonField } from "./JsonField";
|
|
22
|
+
import { MultiTypeEditorField } from "./MultiTypeEditorField";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Recursive field renderer that handles all supported JSON Schema types.
|
|
@@ -84,12 +84,22 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
84
84
|
<div className="relative">
|
|
85
85
|
<Select
|
|
86
86
|
value={(value as string) || (propSchema.default as string) || ""}
|
|
87
|
-
onValueChange={(val) =>
|
|
87
|
+
onValueChange={(val) =>
|
|
88
|
+
onChange(val === NONE_SENTINEL ? undefined : val)
|
|
89
|
+
}
|
|
88
90
|
>
|
|
89
91
|
<SelectTrigger id={id}>
|
|
90
92
|
<SelectValue placeholder={`Select ${label}`} />
|
|
91
93
|
</SelectTrigger>
|
|
92
94
|
<SelectContent>
|
|
95
|
+
{!isRequired && (
|
|
96
|
+
<SelectItem
|
|
97
|
+
value={NONE_SENTINEL}
|
|
98
|
+
className="text-muted-foreground"
|
|
99
|
+
>
|
|
100
|
+
None
|
|
101
|
+
</SelectItem>
|
|
102
|
+
)}
|
|
93
103
|
{propSchema.enum.map((opt: string) => (
|
|
94
104
|
<SelectItem key={opt} value={opt}>
|
|
95
105
|
{opt}
|
|
@@ -104,46 +114,34 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
104
114
|
|
|
105
115
|
// String
|
|
106
116
|
if (propSchema.type === "string") {
|
|
117
|
+
const cleanDesc = getCleanDescription(description);
|
|
118
|
+
|
|
119
|
+
// Multi-type editor field (x-editor-types)
|
|
120
|
+
const editorTypes = propSchema["x-editor-types"];
|
|
121
|
+
if (editorTypes && editorTypes.length > 0) {
|
|
122
|
+
return (
|
|
123
|
+
<MultiTypeEditorField
|
|
124
|
+
id={id}
|
|
125
|
+
label={label}
|
|
126
|
+
description={cleanDesc}
|
|
127
|
+
value={value as string | undefined}
|
|
128
|
+
isRequired={isRequired}
|
|
129
|
+
editorTypes={editorTypes}
|
|
130
|
+
templateProperties={templateProperties}
|
|
131
|
+
onChange={onChange as (val: string | undefined) => void}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
const isTextarea =
|
|
108
137
|
propSchema.format === "textarea" ||
|
|
109
138
|
propSchema.description?.includes("[textarea]");
|
|
110
139
|
const isSecret = (
|
|
111
140
|
propSchema as JsonSchemaProperty & { "x-secret"?: boolean }
|
|
112
141
|
)["x-secret"];
|
|
113
|
-
const cleanDesc = getCleanDescription(description);
|
|
114
142
|
|
|
115
|
-
// Textarea fields
|
|
143
|
+
// Textarea fields
|
|
116
144
|
if (isTextarea) {
|
|
117
|
-
// If we have template properties, use TemplateEditor
|
|
118
|
-
if (templateProperties && templateProperties.length > 0) {
|
|
119
|
-
return (
|
|
120
|
-
<div className="space-y-2">
|
|
121
|
-
<div>
|
|
122
|
-
<Label htmlFor={id}>
|
|
123
|
-
{label} {isRequired && "*"}
|
|
124
|
-
</Label>
|
|
125
|
-
{cleanDesc && (
|
|
126
|
-
<p className="text-sm text-muted-foreground mt-0.5">
|
|
127
|
-
{cleanDesc}
|
|
128
|
-
</p>
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
<TemplateEditor
|
|
132
|
-
value={(value as string) || ""}
|
|
133
|
-
onChange={(val) => onChange(val)}
|
|
134
|
-
availableProperties={templateProperties}
|
|
135
|
-
placeholder={
|
|
136
|
-
propSchema.default
|
|
137
|
-
? `Default: ${String(propSchema.default)}`
|
|
138
|
-
: "Enter template..."
|
|
139
|
-
}
|
|
140
|
-
rows={5}
|
|
141
|
-
/>
|
|
142
|
-
</div>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// No template properties, fall back to regular textarea
|
|
147
145
|
return (
|
|
148
146
|
<div className="space-y-2">
|
|
149
147
|
<div>
|
|
@@ -213,37 +211,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
213
211
|
);
|
|
214
212
|
}
|
|
215
213
|
|
|
216
|
-
// Default string input
|
|
217
|
-
// If we have template properties, use TemplateEditor with smaller rows
|
|
218
|
-
if (templateProperties && templateProperties.length > 0) {
|
|
219
|
-
return (
|
|
220
|
-
<div className="space-y-2">
|
|
221
|
-
<div>
|
|
222
|
-
<Label htmlFor={id}>
|
|
223
|
-
{label} {isRequired && "*"}
|
|
224
|
-
</Label>
|
|
225
|
-
{cleanDesc && (
|
|
226
|
-
<p className="text-sm text-muted-foreground mt-0.5">
|
|
227
|
-
{cleanDesc}
|
|
228
|
-
</p>
|
|
229
|
-
)}
|
|
230
|
-
</div>
|
|
231
|
-
<TemplateEditor
|
|
232
|
-
value={(value as string) || ""}
|
|
233
|
-
onChange={(val) => onChange(val)}
|
|
234
|
-
availableProperties={templateProperties}
|
|
235
|
-
placeholder={
|
|
236
|
-
propSchema.default
|
|
237
|
-
? `Default: ${String(propSchema.default)}`
|
|
238
|
-
: undefined
|
|
239
|
-
}
|
|
240
|
-
rows={2}
|
|
241
|
-
/>
|
|
242
|
-
</div>
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// No template properties - fallback to regular Input
|
|
214
|
+
// Default string input
|
|
247
215
|
return (
|
|
248
216
|
<div className="space-y-2">
|
|
249
217
|
<div>
|
|
@@ -291,7 +259,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
291
259
|
onChange(
|
|
292
260
|
propSchema.type === "integer"
|
|
293
261
|
? Number.parseInt(e.target.value, 10)
|
|
294
|
-
: Number.parseFloat(e.target.value)
|
|
262
|
+
: Number.parseFloat(e.target.value),
|
|
295
263
|
)
|
|
296
264
|
}
|
|
297
265
|
/>
|
|
@@ -390,7 +358,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
390
358
|
const newItem: Record<string, unknown> = {};
|
|
391
359
|
// Find discriminator and set all properties with defaults
|
|
392
360
|
for (const [propKey, propDef] of Object.entries(
|
|
393
|
-
firstVariant.properties
|
|
361
|
+
firstVariant.properties,
|
|
394
362
|
)) {
|
|
395
363
|
if (propDef.const !== undefined) {
|
|
396
364
|
// This is the discriminator field
|
|
@@ -489,7 +457,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
489
457
|
// Find discriminator: the field that has "const" in each variant
|
|
490
458
|
let discriminatorField: string | undefined;
|
|
491
459
|
for (const [fieldName, fieldSchema] of Object.entries(
|
|
492
|
-
firstVariant.properties
|
|
460
|
+
firstVariant.properties,
|
|
493
461
|
)) {
|
|
494
462
|
if (fieldSchema.const !== undefined) {
|
|
495
463
|
discriminatorField = fieldName;
|
|
@@ -547,7 +515,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
547
515
|
};
|
|
548
516
|
// Set defaults for other properties
|
|
549
517
|
for (const [propKey, propDef] of Object.entries(
|
|
550
|
-
newVariant.properties || {}
|
|
518
|
+
newVariant.properties || {},
|
|
551
519
|
)) {
|
|
552
520
|
if (
|
|
553
521
|
propKey !== discriminatorField &&
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import Ajv from "ajv";
|
|
3
3
|
import addFormats from "ajv-formats";
|
|
4
|
-
import Editor from "react-simple-code-editor";
|
|
5
|
-
// @ts-expect-error - prismjs doesn't have types for deep imports in some environments
|
|
6
|
-
import { highlight, languages } from "prismjs/components/prism-core";
|
|
7
|
-
import "prismjs/components/prism-json";
|
|
8
4
|
|
|
5
|
+
import { CodeEditor } from "../CodeEditor";
|
|
9
6
|
import type { JsonFieldProps } from "./types";
|
|
10
7
|
|
|
11
8
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
@@ -21,7 +18,7 @@ export const JsonField: React.FC<JsonFieldProps> = ({
|
|
|
21
18
|
onChange,
|
|
22
19
|
}) => {
|
|
23
20
|
const [internalValue, setInternalValue] = React.useState(
|
|
24
|
-
JSON.stringify(value || {}, undefined, 2)
|
|
21
|
+
JSON.stringify(value || {}, undefined, 2),
|
|
25
22
|
);
|
|
26
23
|
const [error, setError] = React.useState<string | undefined>();
|
|
27
24
|
const lastPropValue = React.useRef(value);
|
|
@@ -59,7 +56,7 @@ export const JsonField: React.FC<JsonFieldProps> = ({
|
|
|
59
56
|
setError(
|
|
60
57
|
validateFn.errors
|
|
61
58
|
?.map((e) => `${e.instancePath} ${e.message}`)
|
|
62
|
-
.join(", ")
|
|
59
|
+
.join(", "),
|
|
63
60
|
);
|
|
64
61
|
return false;
|
|
65
62
|
}
|
|
@@ -71,27 +68,24 @@ export const JsonField: React.FC<JsonFieldProps> = ({
|
|
|
71
68
|
}
|
|
72
69
|
};
|
|
73
70
|
|
|
71
|
+
const handleChange = (code: string) => {
|
|
72
|
+
setInternalValue(code);
|
|
73
|
+
const parsed = validate(code);
|
|
74
|
+
if (parsed !== false) {
|
|
75
|
+
lastPropValue.current = parsed;
|
|
76
|
+
onChange(parsed);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
74
80
|
return (
|
|
75
81
|
<div className="space-y-2">
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (parsed !== false) {
|
|
84
|
-
lastPropValue.current = parsed;
|
|
85
|
-
onChange(parsed);
|
|
86
|
-
}
|
|
87
|
-
}}
|
|
88
|
-
highlight={(code) => highlight(code, languages.json)}
|
|
89
|
-
padding={10}
|
|
90
|
-
style={{
|
|
91
|
-
minHeight: "100px",
|
|
92
|
-
}}
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
82
|
+
<CodeEditor
|
|
83
|
+
id={id}
|
|
84
|
+
value={internalValue}
|
|
85
|
+
onChange={handleChange}
|
|
86
|
+
language="json"
|
|
87
|
+
minHeight="100px"
|
|
88
|
+
/>
|
|
95
89
|
{error && <p className="text-xs text-destructive font-medium">{error}</p>}
|
|
96
90
|
</div>
|
|
97
91
|
);
|