@checkstack/ui 0.0.2
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 +153 -0
- package/bunfig.toml +2 -0
- package/package.json +40 -0
- package/src/components/Accordion.tsx +55 -0
- package/src/components/Alert.tsx +90 -0
- package/src/components/AmbientBackground.tsx +105 -0
- package/src/components/AnimatedCounter.tsx +54 -0
- package/src/components/BackLink.tsx +56 -0
- package/src/components/Badge.tsx +38 -0
- package/src/components/Button.tsx +55 -0
- package/src/components/Card.tsx +56 -0
- package/src/components/Checkbox.tsx +46 -0
- package/src/components/ColorPicker.tsx +69 -0
- package/src/components/CommandPalette.tsx +74 -0
- package/src/components/ConfirmationModal.tsx +134 -0
- package/src/components/DateRangeFilter.tsx +128 -0
- package/src/components/DateTimePicker.tsx +65 -0
- package/src/components/Dialog.tsx +134 -0
- package/src/components/DropdownMenu.tsx +96 -0
- package/src/components/DynamicForm/DynamicForm.tsx +126 -0
- package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
- package/src/components/DynamicForm/FormField.tsx +690 -0
- package/src/components/DynamicForm/JsonField.tsx +98 -0
- package/src/components/DynamicForm/index.ts +11 -0
- package/src/components/DynamicForm/types.ts +95 -0
- package/src/components/DynamicForm/utils.ts +39 -0
- package/src/components/DynamicIcon.tsx +45 -0
- package/src/components/EditableText.tsx +141 -0
- package/src/components/EmptyState.tsx +32 -0
- package/src/components/HealthBadge.tsx +57 -0
- package/src/components/InfoBanner.tsx +97 -0
- package/src/components/Input.tsx +20 -0
- package/src/components/Label.tsx +17 -0
- package/src/components/LoadingSpinner.tsx +29 -0
- package/src/components/Markdown.tsx +206 -0
- package/src/components/NavItem.tsx +112 -0
- package/src/components/Page.tsx +58 -0
- package/src/components/PageLayout.tsx +83 -0
- package/src/components/PaginatedList.tsx +135 -0
- package/src/components/Pagination.tsx +195 -0
- package/src/components/PermissionDenied.tsx +31 -0
- package/src/components/PermissionGate.tsx +97 -0
- package/src/components/PluginConfigForm.tsx +91 -0
- package/src/components/SectionHeader.tsx +30 -0
- package/src/components/Select.tsx +157 -0
- package/src/components/StatusCard.tsx +78 -0
- package/src/components/StatusUpdateTimeline.tsx +222 -0
- package/src/components/StrategyConfigCard.tsx +333 -0
- package/src/components/SubscribeButton.tsx +96 -0
- package/src/components/Table.tsx +119 -0
- package/src/components/Tabs.tsx +141 -0
- package/src/components/TemplateEditor.test.ts +156 -0
- package/src/components/TemplateEditor.tsx +435 -0
- package/src/components/TerminalFeed.tsx +152 -0
- package/src/components/Textarea.tsx +22 -0
- package/src/components/ThemeProvider.tsx +76 -0
- package/src/components/Toast.tsx +118 -0
- package/src/components/ToastProvider.tsx +126 -0
- package/src/components/Toggle.tsx +47 -0
- package/src/components/Tooltip.tsx +20 -0
- package/src/components/UserMenu.tsx +79 -0
- package/src/hooks/usePagination.e2e.ts +275 -0
- package/src/hooks/usePagination.ts +231 -0
- package/src/index.ts +53 -0
- package/src/themes.css +204 -0
- package/src/utils/strip-markdown.ts +44 -0
- package/src/utils.ts +8 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Input,
|
|
6
|
+
Label,
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
Button,
|
|
13
|
+
Textarea,
|
|
14
|
+
Toggle,
|
|
15
|
+
ColorPicker,
|
|
16
|
+
TemplateEditor,
|
|
17
|
+
} from "../../index";
|
|
18
|
+
|
|
19
|
+
import type { FormFieldProps, JsonSchemaProperty } from "./types";
|
|
20
|
+
import { getCleanDescription } from "./utils";
|
|
21
|
+
import { DynamicOptionsField } from "./DynamicOptionsField";
|
|
22
|
+
import { JsonField } from "./JsonField";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recursive field renderer that handles all supported JSON Schema types.
|
|
26
|
+
*/
|
|
27
|
+
export const FormField: React.FC<FormFieldProps> = ({
|
|
28
|
+
id,
|
|
29
|
+
label,
|
|
30
|
+
propSchema,
|
|
31
|
+
value,
|
|
32
|
+
isRequired,
|
|
33
|
+
formValues,
|
|
34
|
+
optionsResolvers,
|
|
35
|
+
templateProperties,
|
|
36
|
+
onChange,
|
|
37
|
+
}) => {
|
|
38
|
+
const description = propSchema.description || "";
|
|
39
|
+
|
|
40
|
+
// Dynamic options via resolver
|
|
41
|
+
const resolverName = propSchema["x-options-resolver"];
|
|
42
|
+
if (resolverName && optionsResolvers) {
|
|
43
|
+
return (
|
|
44
|
+
<DynamicOptionsField
|
|
45
|
+
id={id}
|
|
46
|
+
label={label}
|
|
47
|
+
description={description}
|
|
48
|
+
value={value}
|
|
49
|
+
isRequired={isRequired}
|
|
50
|
+
resolverName={resolverName}
|
|
51
|
+
dependsOn={propSchema["x-depends-on"]}
|
|
52
|
+
searchable={propSchema["x-searchable"] === true}
|
|
53
|
+
formValues={formValues}
|
|
54
|
+
optionsResolvers={optionsResolvers}
|
|
55
|
+
onChange={onChange}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Const field handling - auto-set value and hide (value is fixed)
|
|
61
|
+
if (propSchema.const !== undefined) {
|
|
62
|
+
// Silently ensure the value is set, no UI needed
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (value !== propSchema.const) {
|
|
65
|
+
onChange(propSchema.const);
|
|
66
|
+
}
|
|
67
|
+
}, [value, propSchema.const, onChange]);
|
|
68
|
+
return <></>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Enum handling
|
|
72
|
+
if (propSchema.enum) {
|
|
73
|
+
const cleanDesc = getCleanDescription(description);
|
|
74
|
+
return (
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<div>
|
|
77
|
+
<Label htmlFor={id}>
|
|
78
|
+
{label} {isRequired && "*"}
|
|
79
|
+
</Label>
|
|
80
|
+
{cleanDesc && (
|
|
81
|
+
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
<div className="relative">
|
|
85
|
+
<Select
|
|
86
|
+
value={(value as string) || (propSchema.default as string) || ""}
|
|
87
|
+
onValueChange={(val) => onChange(val)}
|
|
88
|
+
>
|
|
89
|
+
<SelectTrigger id={id}>
|
|
90
|
+
<SelectValue placeholder={`Select ${label}`} />
|
|
91
|
+
</SelectTrigger>
|
|
92
|
+
<SelectContent>
|
|
93
|
+
{propSchema.enum.map((opt: string) => (
|
|
94
|
+
<SelectItem key={opt} value={opt}>
|
|
95
|
+
{opt}
|
|
96
|
+
</SelectItem>
|
|
97
|
+
))}
|
|
98
|
+
</SelectContent>
|
|
99
|
+
</Select>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// String
|
|
106
|
+
if (propSchema.type === "string") {
|
|
107
|
+
const isTextarea =
|
|
108
|
+
propSchema.format === "textarea" ||
|
|
109
|
+
propSchema.description?.includes("[textarea]");
|
|
110
|
+
const isSecret = (
|
|
111
|
+
propSchema as JsonSchemaProperty & { "x-secret"?: boolean }
|
|
112
|
+
)["x-secret"];
|
|
113
|
+
const cleanDesc = getCleanDescription(description);
|
|
114
|
+
|
|
115
|
+
// Textarea fields - use TemplateEditor if templateProperties available
|
|
116
|
+
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
|
+
return (
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
<div>
|
|
150
|
+
<Label htmlFor={id}>
|
|
151
|
+
{label} {isRequired && "*"}
|
|
152
|
+
</Label>
|
|
153
|
+
{cleanDesc && (
|
|
154
|
+
<p className="text-sm text-muted-foreground mt-0.5">
|
|
155
|
+
{cleanDesc}
|
|
156
|
+
</p>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
<Textarea
|
|
160
|
+
id={id}
|
|
161
|
+
value={(value as string) || ""}
|
|
162
|
+
onChange={(e) => onChange(e.target.value)}
|
|
163
|
+
placeholder={
|
|
164
|
+
propSchema.default ? `Default: ${String(propSchema.default)}` : ""
|
|
165
|
+
}
|
|
166
|
+
rows={5}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Secret field (password input)
|
|
173
|
+
if (isSecret) {
|
|
174
|
+
return (
|
|
175
|
+
<SecretField
|
|
176
|
+
id={id}
|
|
177
|
+
label={label}
|
|
178
|
+
description={cleanDesc}
|
|
179
|
+
value={value as string}
|
|
180
|
+
isRequired={isRequired}
|
|
181
|
+
onChange={onChange}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Color picker field
|
|
187
|
+
const isColor = (
|
|
188
|
+
propSchema as JsonSchemaProperty & { "x-color"?: boolean }
|
|
189
|
+
)["x-color"];
|
|
190
|
+
|
|
191
|
+
if (isColor) {
|
|
192
|
+
return (
|
|
193
|
+
<div className="space-y-2">
|
|
194
|
+
<div>
|
|
195
|
+
<Label htmlFor={id}>
|
|
196
|
+
{label} {isRequired && "*"}
|
|
197
|
+
</Label>
|
|
198
|
+
{cleanDesc && (
|
|
199
|
+
<p className="text-sm text-muted-foreground mt-0.5">
|
|
200
|
+
{cleanDesc}
|
|
201
|
+
</p>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
<ColorPicker
|
|
205
|
+
id={id}
|
|
206
|
+
value={(value as string) || ""}
|
|
207
|
+
onChange={(val) => onChange(val)}
|
|
208
|
+
placeholder={
|
|
209
|
+
propSchema.default ? String(propSchema.default) : "#000000"
|
|
210
|
+
}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Default string input - use TemplateEditor if templateProperties available
|
|
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
|
|
247
|
+
return (
|
|
248
|
+
<div className="space-y-2">
|
|
249
|
+
<div>
|
|
250
|
+
<Label htmlFor={id}>
|
|
251
|
+
{label} {isRequired && "*"}
|
|
252
|
+
</Label>
|
|
253
|
+
{cleanDesc && (
|
|
254
|
+
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
<Input
|
|
258
|
+
id={id}
|
|
259
|
+
value={(value as string) || ""}
|
|
260
|
+
onChange={(e) => onChange(e.target.value)}
|
|
261
|
+
placeholder={
|
|
262
|
+
propSchema.default ? `Default: ${String(propSchema.default)}` : ""
|
|
263
|
+
}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Number
|
|
270
|
+
if (propSchema.type === "number" || propSchema.type === "integer") {
|
|
271
|
+
const cleanDesc = getCleanDescription(description);
|
|
272
|
+
return (
|
|
273
|
+
<div className="space-y-2">
|
|
274
|
+
<div>
|
|
275
|
+
<Label htmlFor={id}>
|
|
276
|
+
{label} {isRequired && "*"}
|
|
277
|
+
</Label>
|
|
278
|
+
{cleanDesc && (
|
|
279
|
+
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
<Input
|
|
283
|
+
id={id}
|
|
284
|
+
type="number"
|
|
285
|
+
value={
|
|
286
|
+
value === undefined
|
|
287
|
+
? (propSchema.default as number | string) || ""
|
|
288
|
+
: (value as number | string)
|
|
289
|
+
}
|
|
290
|
+
onChange={(e) =>
|
|
291
|
+
onChange(
|
|
292
|
+
propSchema.type === "integer"
|
|
293
|
+
? Number.parseInt(e.target.value, 10)
|
|
294
|
+
: Number.parseFloat(e.target.value)
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Boolean
|
|
303
|
+
if (propSchema.type === "boolean") {
|
|
304
|
+
const cleanDesc = getCleanDescription(description);
|
|
305
|
+
return (
|
|
306
|
+
<div className="flex items-center justify-between gap-4">
|
|
307
|
+
<div className="flex-1 space-y-1">
|
|
308
|
+
<Label htmlFor={id} className="cursor-pointer">
|
|
309
|
+
{label} {isRequired && "*"}
|
|
310
|
+
</Label>
|
|
311
|
+
{cleanDesc && (
|
|
312
|
+
<p className="text-sm text-muted-foreground">{cleanDesc}</p>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
<Toggle
|
|
316
|
+
checked={
|
|
317
|
+
value === undefined
|
|
318
|
+
? (propSchema.default as boolean) || false
|
|
319
|
+
: (value as boolean)
|
|
320
|
+
}
|
|
321
|
+
onCheckedChange={(checked) => onChange(checked)}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Dictionary/Record (headers)
|
|
328
|
+
if (propSchema.type === "object" && propSchema.additionalProperties) {
|
|
329
|
+
const cleanDesc = getCleanDescription(description);
|
|
330
|
+
return (
|
|
331
|
+
<div className="space-y-2">
|
|
332
|
+
<div>
|
|
333
|
+
<Label htmlFor={id}>
|
|
334
|
+
{label} (JSON) {isRequired && "*"}
|
|
335
|
+
</Label>
|
|
336
|
+
{cleanDesc && (
|
|
337
|
+
<p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
<JsonField
|
|
341
|
+
id={id}
|
|
342
|
+
value={value as Record<string, unknown>}
|
|
343
|
+
propSchema={propSchema}
|
|
344
|
+
onChange={(val) => onChange(val)}
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Object (Nested Form)
|
|
351
|
+
if (propSchema.type === "object" && propSchema.properties) {
|
|
352
|
+
return (
|
|
353
|
+
<div className="space-y-4 p-4 border rounded-lg bg-muted/30">
|
|
354
|
+
<p className="text-sm font-semibold">{label}</p>
|
|
355
|
+
{Object.entries(propSchema.properties).map(([key, subSchema]) => (
|
|
356
|
+
<FormField
|
|
357
|
+
key={key}
|
|
358
|
+
id={`${id}.${key}`}
|
|
359
|
+
label={key.charAt(0).toUpperCase() + key.slice(1)}
|
|
360
|
+
propSchema={subSchema}
|
|
361
|
+
value={(value as Record<string, unknown>)?.[key]}
|
|
362
|
+
isRequired={propSchema.required?.includes(key)}
|
|
363
|
+
formValues={formValues}
|
|
364
|
+
optionsResolvers={optionsResolvers}
|
|
365
|
+
templateProperties={templateProperties}
|
|
366
|
+
onChange={(val) =>
|
|
367
|
+
onChange({ ...(value as Record<string, unknown>), [key]: val })
|
|
368
|
+
}
|
|
369
|
+
/>
|
|
370
|
+
))}
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Array support
|
|
376
|
+
if (propSchema.type === "array") {
|
|
377
|
+
const items = (value as unknown[]) || [];
|
|
378
|
+
const itemSchema = propSchema.items;
|
|
379
|
+
const cleanDesc = getCleanDescription(description);
|
|
380
|
+
|
|
381
|
+
if (!itemSchema) return <></>;
|
|
382
|
+
|
|
383
|
+
// Helper to create initial value for new array items
|
|
384
|
+
const createNewItem = (): Record<string, unknown> => {
|
|
385
|
+
// Check if itemSchema is a discriminated union
|
|
386
|
+
const variants = itemSchema.oneOf || itemSchema.anyOf;
|
|
387
|
+
if (variants && variants.length > 0) {
|
|
388
|
+
const firstVariant = variants[0];
|
|
389
|
+
if (firstVariant.properties) {
|
|
390
|
+
const newItem: Record<string, unknown> = {};
|
|
391
|
+
// Find discriminator and set all properties with defaults
|
|
392
|
+
for (const [propKey, propDef] of Object.entries(
|
|
393
|
+
firstVariant.properties
|
|
394
|
+
)) {
|
|
395
|
+
if (propDef.const !== undefined) {
|
|
396
|
+
// This is the discriminator field
|
|
397
|
+
newItem[propKey] = propDef.const;
|
|
398
|
+
} else if (propDef.default !== undefined) {
|
|
399
|
+
newItem[propKey] = propDef.default;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return newItem;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Fallback to empty object for regular object items
|
|
406
|
+
return {};
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div className="space-y-4">
|
|
411
|
+
<div className="flex items-center justify-between">
|
|
412
|
+
<div>
|
|
413
|
+
<Label>
|
|
414
|
+
{label} {isRequired && "*"}
|
|
415
|
+
</Label>
|
|
416
|
+
{cleanDesc && (
|
|
417
|
+
<p className="text-sm text-muted-foreground mt-0.5">
|
|
418
|
+
{cleanDesc}
|
|
419
|
+
</p>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
<Button
|
|
423
|
+
type="button"
|
|
424
|
+
variant="outline"
|
|
425
|
+
size="sm"
|
|
426
|
+
onClick={() =>
|
|
427
|
+
onChange([
|
|
428
|
+
...(items as Record<string, unknown>[]),
|
|
429
|
+
createNewItem(),
|
|
430
|
+
])
|
|
431
|
+
}
|
|
432
|
+
className="h-8 gap-1 transition-all hover:bg-accent hover:text-accent-foreground"
|
|
433
|
+
>
|
|
434
|
+
<Plus className="h-4 w-4" />
|
|
435
|
+
Add Item
|
|
436
|
+
</Button>
|
|
437
|
+
</div>
|
|
438
|
+
{items.length === 0 && (
|
|
439
|
+
<p className="text-xs text-muted-foreground italic">
|
|
440
|
+
No items added yet.
|
|
441
|
+
</p>
|
|
442
|
+
)}
|
|
443
|
+
<div className="space-y-4">
|
|
444
|
+
{items.map((item: unknown, index: number) => (
|
|
445
|
+
<div key={index} className="relative group">
|
|
446
|
+
<div className="p-4 border rounded-lg bg-background shadow-sm border-border transition-all hover:border-border/80">
|
|
447
|
+
<Button
|
|
448
|
+
type="button"
|
|
449
|
+
variant="ghost"
|
|
450
|
+
size="icon"
|
|
451
|
+
onClick={() => {
|
|
452
|
+
const next = [...(items as unknown[])];
|
|
453
|
+
next.splice(index, 1);
|
|
454
|
+
onChange(next);
|
|
455
|
+
}}
|
|
456
|
+
className="absolute -top-2 -right-2 h-7 w-7 rounded-full bg-background border shadow-sm text-destructive hover:text-destructive/90 hover:bg-destructive/10"
|
|
457
|
+
>
|
|
458
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
459
|
+
</Button>
|
|
460
|
+
<FormField
|
|
461
|
+
id={`${id}[${index}]`}
|
|
462
|
+
label={`${label} #${index + 1}`}
|
|
463
|
+
propSchema={itemSchema}
|
|
464
|
+
value={item}
|
|
465
|
+
formValues={formValues}
|
|
466
|
+
optionsResolvers={optionsResolvers}
|
|
467
|
+
templateProperties={templateProperties}
|
|
468
|
+
onChange={(val) => {
|
|
469
|
+
const next = [...(items as unknown[])];
|
|
470
|
+
next[index] = val;
|
|
471
|
+
onChange(next);
|
|
472
|
+
}}
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
))}
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Discriminated Union (oneOf/anyOf) with object variants
|
|
483
|
+
const unionVariants = propSchema.oneOf || propSchema.anyOf;
|
|
484
|
+
if (unionVariants && unionVariants.length > 0) {
|
|
485
|
+
// Find the discriminator field by looking for a property with "const" in each variant
|
|
486
|
+
const firstVariant = unionVariants[0];
|
|
487
|
+
if (!firstVariant.properties) return <></>;
|
|
488
|
+
|
|
489
|
+
// Find discriminator: the field that has "const" in each variant
|
|
490
|
+
let discriminatorField: string | undefined;
|
|
491
|
+
for (const [fieldName, fieldSchema] of Object.entries(
|
|
492
|
+
firstVariant.properties
|
|
493
|
+
)) {
|
|
494
|
+
if (fieldSchema.const !== undefined) {
|
|
495
|
+
discriminatorField = fieldName;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!discriminatorField) return <></>;
|
|
501
|
+
|
|
502
|
+
// Get current discriminator value and find matching variant
|
|
503
|
+
const currentValue = value as Record<string, unknown> | undefined;
|
|
504
|
+
const currentDiscriminatorValue = currentValue?.[discriminatorField];
|
|
505
|
+
|
|
506
|
+
// Extract variant options from all variants
|
|
507
|
+
const variantOptions = unionVariants
|
|
508
|
+
.map((variant) => {
|
|
509
|
+
const discProp = variant.properties?.[discriminatorField];
|
|
510
|
+
const constValue = discProp?.const;
|
|
511
|
+
if (constValue === undefined) return;
|
|
512
|
+
return String(constValue);
|
|
513
|
+
})
|
|
514
|
+
.filter((v): v is string => v !== undefined);
|
|
515
|
+
|
|
516
|
+
// Find the currently selected variant
|
|
517
|
+
const selectedVariant =
|
|
518
|
+
unionVariants.find((variant) => {
|
|
519
|
+
const discProp = variant.properties?.[discriminatorField];
|
|
520
|
+
return discProp?.const === currentDiscriminatorValue;
|
|
521
|
+
}) || unionVariants[0];
|
|
522
|
+
|
|
523
|
+
const displayDiscriminatorField =
|
|
524
|
+
discriminatorField.charAt(0).toUpperCase() + discriminatorField.slice(1);
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<div className="space-y-3 p-3 border rounded-lg bg-background">
|
|
528
|
+
{/* Discriminator selector */}
|
|
529
|
+
<div className="space-y-2">
|
|
530
|
+
<div>
|
|
531
|
+
<Label htmlFor={`${id}.${discriminatorField}`}>
|
|
532
|
+
{displayDiscriminatorField}
|
|
533
|
+
</Label>
|
|
534
|
+
</div>
|
|
535
|
+
<Select
|
|
536
|
+
value={String(currentDiscriminatorValue || variantOptions[0] || "")}
|
|
537
|
+
onValueChange={(newValue) => {
|
|
538
|
+
// When discriminator changes, reset to new variant with only discriminator set
|
|
539
|
+
const newVariant = unionVariants.find((v) => {
|
|
540
|
+
const discProp = v.properties?.[discriminatorField];
|
|
541
|
+
return String(discProp?.const) === newValue;
|
|
542
|
+
});
|
|
543
|
+
if (newVariant) {
|
|
544
|
+
// Initialize with defaults for the new variant
|
|
545
|
+
const newObj: Record<string, unknown> = {
|
|
546
|
+
[discriminatorField]: newValue,
|
|
547
|
+
};
|
|
548
|
+
// Set defaults for other properties
|
|
549
|
+
for (const [propKey, propDef] of Object.entries(
|
|
550
|
+
newVariant.properties || {}
|
|
551
|
+
)) {
|
|
552
|
+
if (
|
|
553
|
+
propKey !== discriminatorField &&
|
|
554
|
+
propDef.default !== undefined
|
|
555
|
+
) {
|
|
556
|
+
newObj[propKey] = propDef.default;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
onChange(newObj);
|
|
560
|
+
}
|
|
561
|
+
}}
|
|
562
|
+
>
|
|
563
|
+
<SelectTrigger id={`${id}.${discriminatorField}`}>
|
|
564
|
+
<SelectValue
|
|
565
|
+
placeholder={`Select ${displayDiscriminatorField}`}
|
|
566
|
+
/>
|
|
567
|
+
</SelectTrigger>
|
|
568
|
+
<SelectContent>
|
|
569
|
+
{variantOptions.map((opt) => (
|
|
570
|
+
<SelectItem key={opt} value={opt}>
|
|
571
|
+
{opt}
|
|
572
|
+
</SelectItem>
|
|
573
|
+
))}
|
|
574
|
+
</SelectContent>
|
|
575
|
+
</Select>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
{/* Render other fields from selected variant */}
|
|
579
|
+
{selectedVariant.properties &&
|
|
580
|
+
Object.entries(selectedVariant.properties)
|
|
581
|
+
.filter(([key]) => key !== discriminatorField)
|
|
582
|
+
.map(([key, subSchema]) => (
|
|
583
|
+
<FormField
|
|
584
|
+
key={`${id}.${key}`}
|
|
585
|
+
id={`${id}.${key}`}
|
|
586
|
+
label={
|
|
587
|
+
key.charAt(0).toUpperCase() +
|
|
588
|
+
key.slice(1).replaceAll(/([A-Z])/g, " $1")
|
|
589
|
+
}
|
|
590
|
+
propSchema={subSchema}
|
|
591
|
+
value={currentValue?.[key]}
|
|
592
|
+
isRequired={selectedVariant.required?.includes(key)}
|
|
593
|
+
formValues={formValues}
|
|
594
|
+
optionsResolvers={optionsResolvers}
|
|
595
|
+
templateProperties={templateProperties}
|
|
596
|
+
onChange={(val) => onChange({ ...currentValue, [key]: val })}
|
|
597
|
+
/>
|
|
598
|
+
))}
|
|
599
|
+
</div>
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return <></>;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Secret field component with password visibility toggle.
|
|
608
|
+
* Extracted to keep hooks at component top level.
|
|
609
|
+
*/
|
|
610
|
+
const SecretField: React.FC<{
|
|
611
|
+
id: string;
|
|
612
|
+
label: string;
|
|
613
|
+
description?: string;
|
|
614
|
+
value: string;
|
|
615
|
+
isRequired?: boolean;
|
|
616
|
+
onChange: (val: unknown) => void;
|
|
617
|
+
}> = ({ id, label, description, value, isRequired, onChange }) => {
|
|
618
|
+
const [showPassword, setShowPassword] = React.useState(false);
|
|
619
|
+
const currentValue = value || "";
|
|
620
|
+
const hasExistingValue = currentValue.length > 0;
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<div className="space-y-2">
|
|
624
|
+
<div>
|
|
625
|
+
<Label htmlFor={id}>
|
|
626
|
+
{label} {isRequired && "*"}
|
|
627
|
+
</Label>
|
|
628
|
+
{description && (
|
|
629
|
+
<p className="text-sm text-muted-foreground mt-0.5">{description}</p>
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
<div className="relative">
|
|
633
|
+
<Input
|
|
634
|
+
id={id}
|
|
635
|
+
type={showPassword ? "text" : "password"}
|
|
636
|
+
value={currentValue}
|
|
637
|
+
onChange={(e) => onChange(e.target.value)}
|
|
638
|
+
placeholder={hasExistingValue ? "••••••••" : "Enter secret value"}
|
|
639
|
+
className="pr-10"
|
|
640
|
+
/>
|
|
641
|
+
<button
|
|
642
|
+
type="button"
|
|
643
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
644
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
645
|
+
>
|
|
646
|
+
{showPassword ? (
|
|
647
|
+
<svg
|
|
648
|
+
className="h-5 w-5"
|
|
649
|
+
fill="none"
|
|
650
|
+
stroke="currentColor"
|
|
651
|
+
viewBox="0 0 24 24"
|
|
652
|
+
>
|
|
653
|
+
<path
|
|
654
|
+
strokeLinecap="round"
|
|
655
|
+
strokeLinejoin="round"
|
|
656
|
+
strokeWidth={2}
|
|
657
|
+
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
|
658
|
+
/>
|
|
659
|
+
</svg>
|
|
660
|
+
) : (
|
|
661
|
+
<svg
|
|
662
|
+
className="h-5 w-5"
|
|
663
|
+
fill="none"
|
|
664
|
+
stroke="currentColor"
|
|
665
|
+
viewBox="0 0 24 24"
|
|
666
|
+
>
|
|
667
|
+
<path
|
|
668
|
+
strokeLinecap="round"
|
|
669
|
+
strokeLinejoin="round"
|
|
670
|
+
strokeWidth={2}
|
|
671
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
672
|
+
/>
|
|
673
|
+
<path
|
|
674
|
+
strokeLinecap="round"
|
|
675
|
+
strokeLinejoin="round"
|
|
676
|
+
strokeWidth={2}
|
|
677
|
+
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
678
|
+
/>
|
|
679
|
+
</svg>
|
|
680
|
+
)}
|
|
681
|
+
</button>
|
|
682
|
+
</div>
|
|
683
|
+
{hasExistingValue && currentValue === "" && (
|
|
684
|
+
<p className="text-xs text-muted-foreground">
|
|
685
|
+
Leave empty to keep existing value
|
|
686
|
+
</p>
|
|
687
|
+
)}
|
|
688
|
+
</div>
|
|
689
|
+
);
|
|
690
|
+
};
|