@checkstack/ui 0.2.3 → 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 +48 -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/components/StrategyConfigCard.tsx +8 -4
- package/src/index.ts +1 -1
- package/src/components/TemplateEditor.test.ts +0 -156
- package/src/components/TemplateEditor.tsx +0 -435
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { JsonSchema, JsonSchemaProperty } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
extractDefaults,
|
|
6
|
+
getCleanDescription,
|
|
7
|
+
isValueEmpty,
|
|
8
|
+
NONE_SENTINEL,
|
|
9
|
+
parseSelectValue,
|
|
10
|
+
serializeFormData,
|
|
11
|
+
parseFormData,
|
|
12
|
+
detectEditorType,
|
|
13
|
+
type EditorType,
|
|
14
|
+
} from "./utils";
|
|
15
|
+
|
|
16
|
+
describe("getCleanDescription", () => {
|
|
17
|
+
it("returns undefined for empty string", () => {
|
|
18
|
+
expect(getCleanDescription("")).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns undefined for undefined input", () => {
|
|
22
|
+
expect(getCleanDescription(undefined)).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns undefined for 'textarea' marker only", () => {
|
|
26
|
+
expect(getCleanDescription("textarea")).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("removes [textarea] marker from description", () => {
|
|
30
|
+
expect(getCleanDescription("[textarea] Some description")).toBe(
|
|
31
|
+
"Some description",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns cleaned description without marker", () => {
|
|
36
|
+
expect(getCleanDescription("Regular description")).toBe(
|
|
37
|
+
"Regular description",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("trims whitespace after removing marker", () => {
|
|
42
|
+
expect(getCleanDescription(" [textarea] Description ")).toBe(
|
|
43
|
+
"Description",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("extractDefaults", () => {
|
|
49
|
+
it("returns empty object for schema without properties", () => {
|
|
50
|
+
const schema: JsonSchema = {};
|
|
51
|
+
expect(extractDefaults(schema)).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("extracts simple default values", () => {
|
|
55
|
+
const schema: JsonSchema = {
|
|
56
|
+
properties: {
|
|
57
|
+
name: { type: "string", default: "default name" },
|
|
58
|
+
count: { type: "number", default: 0 },
|
|
59
|
+
enabled: { type: "boolean", default: true },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
expect(extractDefaults(schema)).toEqual({
|
|
63
|
+
name: "default name",
|
|
64
|
+
count: 0,
|
|
65
|
+
enabled: true,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("defaults arrays to empty array", () => {
|
|
70
|
+
const schema: JsonSchema = {
|
|
71
|
+
properties: {
|
|
72
|
+
items: { type: "array" },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
expect(extractDefaults(schema)).toEqual({
|
|
76
|
+
items: [],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("recursively extracts defaults from nested objects", () => {
|
|
81
|
+
const schema: JsonSchema = {
|
|
82
|
+
properties: {
|
|
83
|
+
config: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
setting1: { type: "string", default: "value1" },
|
|
87
|
+
setting2: { type: "number", default: 42 },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
expect(extractDefaults(schema)).toEqual({
|
|
93
|
+
config: {
|
|
94
|
+
setting1: "value1",
|
|
95
|
+
setting2: 42,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("ignores properties without defaults", () => {
|
|
101
|
+
const schema: JsonSchema = {
|
|
102
|
+
properties: {
|
|
103
|
+
withDefault: { type: "string", default: "has default" },
|
|
104
|
+
withoutDefault: { type: "string" },
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
expect(extractDefaults(schema)).toEqual({
|
|
108
|
+
withDefault: "has default",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("isValueEmpty", () => {
|
|
114
|
+
const stringSchema: JsonSchemaProperty = { type: "string" };
|
|
115
|
+
const numberSchema: JsonSchemaProperty = { type: "number" };
|
|
116
|
+
const arraySchema: JsonSchemaProperty = { type: "array" };
|
|
117
|
+
const objectSchema: JsonSchemaProperty = {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
requiredField: { type: "string" },
|
|
121
|
+
optionalField: { type: "string" },
|
|
122
|
+
},
|
|
123
|
+
required: ["requiredField"],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
describe("primitive values", () => {
|
|
127
|
+
it("treats undefined as empty", () => {
|
|
128
|
+
expect(isValueEmpty(undefined, stringSchema)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("treats null as empty", () => {
|
|
132
|
+
expect(isValueEmpty(null, stringSchema)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("treats empty string as empty", () => {
|
|
136
|
+
expect(isValueEmpty("", stringSchema)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("treats whitespace-only string as empty", () => {
|
|
140
|
+
expect(isValueEmpty(" ", stringSchema)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("treats non-empty string as not empty", () => {
|
|
144
|
+
expect(isValueEmpty("hello", stringSchema)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("treats zero as not empty", () => {
|
|
148
|
+
expect(isValueEmpty(0, numberSchema)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("treats false as not empty", () => {
|
|
152
|
+
const boolSchema: JsonSchemaProperty = { type: "boolean" };
|
|
153
|
+
expect(isValueEmpty(false, boolSchema)).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("arrays", () => {
|
|
158
|
+
it("treats empty array as empty", () => {
|
|
159
|
+
expect(isValueEmpty([], arraySchema)).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("treats non-empty array as not empty", () => {
|
|
163
|
+
expect(isValueEmpty([1, 2, 3], arraySchema)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("objects", () => {
|
|
168
|
+
it("treats object with empty required field as empty", () => {
|
|
169
|
+
expect(isValueEmpty({ requiredField: "" }, objectSchema)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("treats object with filled required field as not empty", () => {
|
|
173
|
+
expect(isValueEmpty({ requiredField: "value" }, objectSchema)).toBe(
|
|
174
|
+
false,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("ignores optional fields when checking emptiness", () => {
|
|
179
|
+
expect(
|
|
180
|
+
isValueEmpty(
|
|
181
|
+
{ requiredField: "value", optionalField: "" },
|
|
182
|
+
objectSchema,
|
|
183
|
+
),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("treats object with missing required field as empty", () => {
|
|
188
|
+
expect(isValueEmpty({ optionalField: "value" }, objectSchema)).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("NONE_SENTINEL", () => {
|
|
194
|
+
it("is a specific string constant", () => {
|
|
195
|
+
expect(NONE_SENTINEL).toBe("__none__");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("parseSelectValue", () => {
|
|
200
|
+
it("returns undefined for NONE_SENTINEL", () => {
|
|
201
|
+
expect(parseSelectValue(NONE_SENTINEL)).toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("returns undefined for '__none__' string", () => {
|
|
205
|
+
expect(parseSelectValue("__none__")).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("returns the value as-is for regular strings", () => {
|
|
209
|
+
expect(parseSelectValue("some-role-id")).toBe("some-role-id");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("returns empty string as-is", () => {
|
|
213
|
+
expect(parseSelectValue("")).toBe("");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("returns whitespace as-is", () => {
|
|
217
|
+
expect(parseSelectValue(" ")).toBe(" ");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// =============================================================================
|
|
222
|
+
// Multi-Type Editor Utility Tests
|
|
223
|
+
// =============================================================================
|
|
224
|
+
|
|
225
|
+
describe("serializeFormData", () => {
|
|
226
|
+
it("should serialize empty array to empty string", () => {
|
|
227
|
+
expect(serializeFormData([])).toBe("");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should serialize single key-value pair", () => {
|
|
231
|
+
expect(serializeFormData([{ key: "name", value: "John" }])).toBe(
|
|
232
|
+
"name=John",
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should serialize multiple key-value pairs", () => {
|
|
237
|
+
expect(
|
|
238
|
+
serializeFormData([
|
|
239
|
+
{ key: "name", value: "John" },
|
|
240
|
+
{ key: "age", value: "30" },
|
|
241
|
+
]),
|
|
242
|
+
).toBe("name=John&age=30");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should URL-encode special characters", () => {
|
|
246
|
+
expect(serializeFormData([{ key: "message", value: "Hello World!" }])).toBe(
|
|
247
|
+
"message=Hello%20World!",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should handle empty values", () => {
|
|
252
|
+
expect(serializeFormData([{ key: "empty", value: "" }])).toBe("empty=");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should filter out entries with empty keys", () => {
|
|
256
|
+
expect(
|
|
257
|
+
serializeFormData([
|
|
258
|
+
{ key: "", value: "ignored" },
|
|
259
|
+
{ key: "valid", value: "kept" },
|
|
260
|
+
]),
|
|
261
|
+
).toBe("valid=kept");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should handle values with equals sign", () => {
|
|
265
|
+
expect(serializeFormData([{ key: "expr", value: "a=b" }])).toBe(
|
|
266
|
+
"expr=a%3Db",
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("parseFormData", () => {
|
|
272
|
+
it("should parse empty string to empty array", () => {
|
|
273
|
+
expect(parseFormData("")).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should parse whitespace-only string to empty array", () => {
|
|
277
|
+
expect(parseFormData(" ")).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should parse single key-value pair", () => {
|
|
281
|
+
expect(parseFormData("name=John")).toEqual([
|
|
282
|
+
{ key: "name", value: "John" },
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should parse multiple key-value pairs", () => {
|
|
287
|
+
expect(parseFormData("name=John&age=30")).toEqual([
|
|
288
|
+
{ key: "name", value: "John" },
|
|
289
|
+
{ key: "age", value: "30" },
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should URL-decode special characters", () => {
|
|
294
|
+
expect(parseFormData("message=Hello%20World!")).toEqual([
|
|
295
|
+
{ key: "message", value: "Hello World!" },
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should handle empty values", () => {
|
|
300
|
+
expect(parseFormData("empty=")).toEqual([{ key: "empty", value: "" }]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should handle values with equals sign", () => {
|
|
304
|
+
expect(parseFormData("expr=a%3Db")).toEqual([
|
|
305
|
+
{ key: "expr", value: "a=b" },
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should handle value containing literal equals", () => {
|
|
310
|
+
expect(parseFormData("expr=a=b")).toEqual([{ key: "expr", value: "a=b" }]);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("detectEditorType", () => {
|
|
315
|
+
const allTypes: EditorType[] = ["none", "raw", "json", "formdata"];
|
|
316
|
+
const withoutNone: EditorType[] = ["raw", "json", "formdata"];
|
|
317
|
+
|
|
318
|
+
describe("empty/undefined values", () => {
|
|
319
|
+
it("should return 'none' for undefined when available", () => {
|
|
320
|
+
expect(detectEditorType(undefined, allTypes)).toBe("none");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("should return 'raw' for undefined when 'none' not available", () => {
|
|
324
|
+
expect(detectEditorType(undefined, withoutNone)).toBe("raw");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should return 'none' for empty string when available", () => {
|
|
328
|
+
expect(detectEditorType("", allTypes)).toBe("none");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("should return 'raw' for whitespace-only when 'none' not available", () => {
|
|
332
|
+
expect(detectEditorType(" ", withoutNone)).toBe("raw");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("JSON detection", () => {
|
|
337
|
+
it("should detect valid JSON object", () => {
|
|
338
|
+
expect(detectEditorType('{"key": "value"}', allTypes)).toBe("json");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should detect valid JSON array", () => {
|
|
342
|
+
expect(detectEditorType("[1, 2, 3]", allTypes)).toBe("json");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should not detect invalid JSON", () => {
|
|
346
|
+
expect(detectEditorType("{invalid json}", allTypes)).toBe("raw");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should not detect JSON when json type not available", () => {
|
|
350
|
+
expect(detectEditorType('{"key": "value"}', ["raw", "formdata"])).toBe(
|
|
351
|
+
"raw",
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("formdata detection", () => {
|
|
357
|
+
it("should detect key=value format", () => {
|
|
358
|
+
expect(detectEditorType("name=John", allTypes)).toBe("formdata");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should detect multiple pairs", () => {
|
|
362
|
+
expect(detectEditorType("name=John&age=30", allTypes)).toBe("formdata");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should not detect formdata with newlines", () => {
|
|
366
|
+
expect(detectEditorType("name=John\nage=30", allTypes)).toBe("raw");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("should not detect formdata when formdata type not available", () => {
|
|
370
|
+
expect(detectEditorType("name=John", ["raw", "json"])).toBe("raw");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should prefer json over formdata for ambiguous content", () => {
|
|
374
|
+
const jsonLikeFormdata = '{"name":"John"}';
|
|
375
|
+
expect(detectEditorType(jsonLikeFormdata, allTypes)).toBe("json");
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("fallback behavior", () => {
|
|
380
|
+
it("should fall back to raw for plain text", () => {
|
|
381
|
+
expect(detectEditorType("Hello, world!", allTypes)).toBe("raw");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should fall back to first available type when raw not available", () => {
|
|
385
|
+
expect(detectEditorType("Hello, world!", ["json", "formdata"])).toBe(
|
|
386
|
+
"json",
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { JsonSchema } from "./types";
|
|
1
|
+
import type { JsonSchema, JsonSchemaProperty } from "./types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Cleans a description string by removing textarea markers.
|
|
5
5
|
* Returns undefined if the description is empty or just "textarea".
|
|
6
6
|
*/
|
|
7
7
|
export const getCleanDescription = (
|
|
8
|
-
description?: string
|
|
8
|
+
description?: string,
|
|
9
9
|
): string | undefined => {
|
|
10
10
|
if (!description || description === "textarea") return;
|
|
11
11
|
const cleaned = description.replace("[textarea]", "").trim();
|
|
@@ -17,7 +17,7 @@ export const getCleanDescription = (
|
|
|
17
17
|
* Extracts default values from a JSON schema recursively.
|
|
18
18
|
*/
|
|
19
19
|
export const extractDefaults = (
|
|
20
|
-
schema: JsonSchema
|
|
20
|
+
schema: JsonSchema,
|
|
21
21
|
): Record<string, unknown> => {
|
|
22
22
|
const defaults: Record<string, unknown> = {};
|
|
23
23
|
|
|
@@ -37,3 +37,142 @@ export const extractDefaults = (
|
|
|
37
37
|
|
|
38
38
|
return defaults;
|
|
39
39
|
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a value is considered "empty" for validation purposes.
|
|
43
|
+
* Used to determine if required fields are filled.
|
|
44
|
+
*/
|
|
45
|
+
export function isValueEmpty(
|
|
46
|
+
val: unknown,
|
|
47
|
+
propSchema: JsonSchemaProperty,
|
|
48
|
+
): boolean {
|
|
49
|
+
if (val === undefined || val === null) return true;
|
|
50
|
+
if (typeof val === "string" && val.trim() === "") return true;
|
|
51
|
+
// For arrays, check if empty
|
|
52
|
+
if (Array.isArray(val) && val.length === 0) return true;
|
|
53
|
+
// For objects (nested schemas), recursively check required fields
|
|
54
|
+
if (propSchema.type === "object" && propSchema.properties) {
|
|
55
|
+
const objVal = val as Record<string, unknown>;
|
|
56
|
+
const requiredKeys = propSchema.required ?? [];
|
|
57
|
+
for (const key of requiredKeys) {
|
|
58
|
+
const nestedPropSchema = propSchema.properties[key];
|
|
59
|
+
if (nestedPropSchema && isValueEmpty(objVal[key], nestedPropSchema)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Sentinel value used to represent "None" selection in Select components */
|
|
68
|
+
export const NONE_SENTINEL = "__none__";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Converts a select value to the actual form value.
|
|
72
|
+
* Handles the "None" sentinel value by returning undefined.
|
|
73
|
+
*/
|
|
74
|
+
export function parseSelectValue(val: string): string | undefined {
|
|
75
|
+
return val === NONE_SENTINEL ? undefined : val;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Multi-Type Editor Utilities
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
import type { KeyValuePair } from "./KeyValueEditor";
|
|
83
|
+
import type { EditorType } from "@checkstack/common";
|
|
84
|
+
|
|
85
|
+
// Re-export for local consumers
|
|
86
|
+
export type { EditorType } from "@checkstack/common";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Serialize key-value pairs to URL-encoded string format.
|
|
90
|
+
* Example: [{ key: "a", value: "1" }] -> "a=1"
|
|
91
|
+
*/
|
|
92
|
+
export function serializeFormData(pairs: KeyValuePair[]): string {
|
|
93
|
+
const filtered = pairs.filter((p) => p.key.trim() !== "");
|
|
94
|
+
if (filtered.length === 0) return "";
|
|
95
|
+
return filtered
|
|
96
|
+
.map((p) => `${encodeURIComponent(p.key)}=${encodeURIComponent(p.value)}`)
|
|
97
|
+
.join("&");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse URL-encoded string to key-value pairs.
|
|
102
|
+
* Example: "a=1&b=2" -> [{ key: "a", value: "1" }, { key: "b", value: "2" }]
|
|
103
|
+
*/
|
|
104
|
+
export function parseFormData(str: string): KeyValuePair[] {
|
|
105
|
+
if (!str || str.trim() === "") return [];
|
|
106
|
+
|
|
107
|
+
return str.split("&").map((pair) => {
|
|
108
|
+
const [key, ...valueParts] = pair.split("=");
|
|
109
|
+
return {
|
|
110
|
+
key: decodeURIComponent(key || ""),
|
|
111
|
+
value: decodeURIComponent(valueParts.join("=") || ""),
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Detect the most likely editor type from a string value.
|
|
118
|
+
* Used to auto-select the initial editor type when loading existing data.
|
|
119
|
+
*/
|
|
120
|
+
export function detectEditorType(
|
|
121
|
+
value: string | undefined,
|
|
122
|
+
availableTypes: EditorType[],
|
|
123
|
+
): EditorType {
|
|
124
|
+
// If no value, prefer "none" if available, otherwise first type
|
|
125
|
+
if (!value || value.trim() === "") {
|
|
126
|
+
if (availableTypes.includes("none")) return "none";
|
|
127
|
+
return availableTypes[0] ?? "raw";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Try to detect JSON
|
|
131
|
+
if (availableTypes.includes("json")) {
|
|
132
|
+
const trimmed = value.trim();
|
|
133
|
+
if (
|
|
134
|
+
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
135
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))
|
|
136
|
+
) {
|
|
137
|
+
try {
|
|
138
|
+
JSON.parse(value);
|
|
139
|
+
return "json";
|
|
140
|
+
} catch {
|
|
141
|
+
// Not valid JSON, continue checking
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Try to detect formdata (URL-encoded key=value pairs)
|
|
147
|
+
// Simple heuristic: contains = and optionally &, no newlines
|
|
148
|
+
if (
|
|
149
|
+
availableTypes.includes("formdata") &&
|
|
150
|
+
value.includes("=") &&
|
|
151
|
+
!value.includes("\n")
|
|
152
|
+
) {
|
|
153
|
+
const parts = value.split("&");
|
|
154
|
+
const looksLikeFormData = parts.every((p) => p.includes("="));
|
|
155
|
+
if (looksLikeFormData) {
|
|
156
|
+
return "formdata";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Default to raw if available
|
|
161
|
+
if (availableTypes.includes("raw")) return "raw";
|
|
162
|
+
|
|
163
|
+
// Fallback to first available type
|
|
164
|
+
return availableTypes[0] ?? "raw";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Human-readable labels for editor types
|
|
169
|
+
*/
|
|
170
|
+
export const EDITOR_TYPE_LABELS: Record<EditorType, string> = {
|
|
171
|
+
none: "None",
|
|
172
|
+
raw: "Plain Text",
|
|
173
|
+
json: "JSON",
|
|
174
|
+
yaml: "YAML",
|
|
175
|
+
xml: "XML",
|
|
176
|
+
markdown: "Markdown",
|
|
177
|
+
formdata: "Form Data",
|
|
178
|
+
};
|
|
@@ -5,6 +5,7 @@ import { Button } from "./Button";
|
|
|
5
5
|
import { Badge, type BadgeProps } from "./Badge";
|
|
6
6
|
import { Toggle } from "./Toggle";
|
|
7
7
|
import { DynamicForm } from "./DynamicForm";
|
|
8
|
+
import type { OptionsResolver } from "./DynamicForm/types";
|
|
8
9
|
import { DynamicIcon, type LucideIconName } from "./DynamicIcon";
|
|
9
10
|
import { MarkdownBlock } from "./Markdown";
|
|
10
11
|
import { cn } from "../utils";
|
|
@@ -23,6 +24,8 @@ export interface ConfigSection {
|
|
|
23
24
|
value?: Record<string, unknown>;
|
|
24
25
|
/** Called when configuration is saved */
|
|
25
26
|
onSave?: (config: Record<string, unknown>) => Promise<void>;
|
|
27
|
+
/** Optional resolvers for dynamic dropdown fields (x-options-resolver) */
|
|
28
|
+
optionsResolvers?: Record<string, OptionsResolver>;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -130,7 +133,7 @@ export function StrategyConfigCard({
|
|
|
130
133
|
initial[section.id] = true;
|
|
131
134
|
}
|
|
132
135
|
return initial;
|
|
133
|
-
}
|
|
136
|
+
},
|
|
134
137
|
);
|
|
135
138
|
|
|
136
139
|
// Determine if we're in controlled or uncontrolled mode
|
|
@@ -167,7 +170,7 @@ export function StrategyConfigCard({
|
|
|
167
170
|
|
|
168
171
|
const handleSectionValueChange = (
|
|
169
172
|
sectionId: string,
|
|
170
|
-
value: Record<string, unknown
|
|
173
|
+
value: Record<string, unknown>,
|
|
171
174
|
) => {
|
|
172
175
|
setSectionValues((prev) => ({ ...prev, [sectionId]: value }));
|
|
173
176
|
};
|
|
@@ -186,7 +189,7 @@ export function StrategyConfigCard({
|
|
|
186
189
|
<Card
|
|
187
190
|
className={cn(
|
|
188
191
|
"overflow-hidden transition-all",
|
|
189
|
-
localEnabled ? "border-primary/30" : "opacity-80"
|
|
192
|
+
localEnabled ? "border-primary/30" : "opacity-80",
|
|
190
193
|
)}
|
|
191
194
|
>
|
|
192
195
|
<CardHeader className="p-4">
|
|
@@ -257,7 +260,7 @@ export function StrategyConfigCard({
|
|
|
257
260
|
<Power
|
|
258
261
|
className={cn(
|
|
259
262
|
"h-4 w-4 mr-1",
|
|
260
|
-
localEnabled ? "text-green-300" : "text-muted-foreground"
|
|
263
|
+
localEnabled ? "text-green-300" : "text-muted-foreground",
|
|
261
264
|
)}
|
|
262
265
|
/>
|
|
263
266
|
{localEnabled ? "Enabled" : "Disabled"}
|
|
@@ -311,6 +314,7 @@ export function StrategyConfigCard({
|
|
|
311
314
|
onValidChange={(isValid) =>
|
|
312
315
|
handleSectionValidChange(section.id, isValid)
|
|
313
316
|
}
|
|
317
|
+
optionsResolvers={section.optionsResolvers}
|
|
314
318
|
/>
|
|
315
319
|
|
|
316
320
|
{section.onSave && (
|
package/src/index.ts
CHANGED
|
@@ -46,8 +46,8 @@ export * from "./components/DynamicIcon";
|
|
|
46
46
|
export * from "./components/StrategyConfigCard";
|
|
47
47
|
export * from "./components/Markdown";
|
|
48
48
|
export * from "./components/ColorPicker";
|
|
49
|
-
export * from "./components/TemplateEditor";
|
|
50
49
|
export * from "./components/AnimatedCounter";
|
|
51
50
|
export * from "./components/CommandPalette";
|
|
52
51
|
export * from "./components/TerminalFeed";
|
|
53
52
|
export * from "./components/AmbientBackground";
|
|
53
|
+
export * from "./components/CodeEditor";
|