@magnet-cms/plugin-playground 2.0.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/dist/backend/index.cjs +1023 -0
- package/dist/backend/index.d.cts +10 -0
- package/dist/backend/index.d.ts +10 -0
- package/dist/backend/index.js +1 -0
- package/dist/chunk-WY4YMBWZ.js +1044 -0
- package/dist/frontend/bundle.iife.js +2163 -0
- package/dist/frontend/bundle.iife.js.map +1 -0
- package/dist/index.cjs +1135 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +76 -0
- package/package.json +81 -0
- package/src/frontend/index.ts +110 -0
- package/src/frontend/pages/Playground/Editor/AddFieldDialog.tsx +187 -0
- package/src/frontend/pages/Playground/Editor/CodePreview.tsx +59 -0
- package/src/frontend/pages/Playground/Editor/FieldCard.tsx +161 -0
- package/src/frontend/pages/Playground/Editor/FieldList.tsx +121 -0
- package/src/frontend/pages/Playground/Editor/FieldSettingsPanel.tsx +652 -0
- package/src/frontend/pages/Playground/Editor/RelationConfigModal.tsx +292 -0
- package/src/frontend/pages/Playground/Editor/SchemaList.tsx +76 -0
- package/src/frontend/pages/Playground/Editor/SchemaOptionsDialog.tsx +109 -0
- package/src/frontend/pages/Playground/Editor/index.tsx +322 -0
- package/src/frontend/pages/Playground/constants/field-types.ts +384 -0
- package/src/frontend/pages/Playground/hooks/useSchemaBuilder.ts +280 -0
- package/src/frontend/pages/Playground/index.tsx +19 -0
- package/src/frontend/pages/Playground/types/builder.types.ts +191 -0
- package/src/frontend/pages/Playground/utils/code-generator.ts +319 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { useAdmin } from '@magnet-cms/admin'
|
|
2
|
+
import {
|
|
3
|
+
Accordion,
|
|
4
|
+
AccordionContent,
|
|
5
|
+
AccordionItem,
|
|
6
|
+
AccordionTrigger,
|
|
7
|
+
Button,
|
|
8
|
+
Input,
|
|
9
|
+
Label,
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
Switch,
|
|
16
|
+
Textarea,
|
|
17
|
+
} from '@magnet-cms/ui/components'
|
|
18
|
+
import { ChevronRight, Lock, Plus, Trash2, X } from 'lucide-react'
|
|
19
|
+
import { useState } from 'react'
|
|
20
|
+
import {
|
|
21
|
+
FIELD_TYPES,
|
|
22
|
+
RELATION_TYPES,
|
|
23
|
+
UI_SUBTYPES,
|
|
24
|
+
VALIDATION_RULES_BY_TYPE,
|
|
25
|
+
getValidationRuleDefinition,
|
|
26
|
+
} from '../constants/field-types'
|
|
27
|
+
import { useSchemaBuilder } from '../hooks/useSchemaBuilder'
|
|
28
|
+
import type {
|
|
29
|
+
FieldType,
|
|
30
|
+
RelationConfig,
|
|
31
|
+
ValidationRule,
|
|
32
|
+
} from '../types/builder.types'
|
|
33
|
+
import { RelationConfigModal } from './RelationConfigModal'
|
|
34
|
+
|
|
35
|
+
export function FieldSettingsPanel() {
|
|
36
|
+
const { selectedField, updateField, selectField, state } = useSchemaBuilder()
|
|
37
|
+
const { schemas } = useAdmin()
|
|
38
|
+
const [newOptionKey, setNewOptionKey] = useState('')
|
|
39
|
+
const [newOptionValue, setNewOptionValue] = useState('')
|
|
40
|
+
const [relationModalOpen, setRelationModalOpen] = useState(false)
|
|
41
|
+
|
|
42
|
+
if (!selectedField) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="h-full flex items-center justify-center text-muted-foreground p-8 text-center">
|
|
45
|
+
<div>
|
|
46
|
+
<p className="text-sm font-medium mb-1">No field selected</p>
|
|
47
|
+
<p className="text-xs">Select a field to edit its settings</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fieldTypeDef = FIELD_TYPES.find((t) => t.id === selectedField.type)
|
|
54
|
+
const availableValidations =
|
|
55
|
+
VALIDATION_RULES_BY_TYPE[selectedField.type] || []
|
|
56
|
+
const uiSubtypes = UI_SUBTYPES[selectedField.type] || []
|
|
57
|
+
|
|
58
|
+
const handleAddOption = () => {
|
|
59
|
+
if (!newOptionKey.trim() || !newOptionValue.trim()) return
|
|
60
|
+
const currentOptions = selectedField.ui.options || []
|
|
61
|
+
updateField(selectedField.id, {
|
|
62
|
+
ui: {
|
|
63
|
+
...selectedField.ui,
|
|
64
|
+
options: [
|
|
65
|
+
...currentOptions,
|
|
66
|
+
{ key: newOptionKey.trim(), value: newOptionValue.trim() },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
setNewOptionKey('')
|
|
71
|
+
setNewOptionValue('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handleRemoveOption = (index: number) => {
|
|
75
|
+
const currentOptions = selectedField.ui.options || []
|
|
76
|
+
updateField(selectedField.id, {
|
|
77
|
+
ui: {
|
|
78
|
+
...selectedField.ui,
|
|
79
|
+
options: currentOptions.filter((_, i) => i !== index),
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const handleAddValidation = (type: string) => {
|
|
85
|
+
const currentValidations = selectedField.validations || []
|
|
86
|
+
if (currentValidations.some((v) => v.type === type)) return
|
|
87
|
+
|
|
88
|
+
const ruleDef = getValidationRuleDefinition(type)
|
|
89
|
+
const newRule: ValidationRule = {
|
|
90
|
+
type,
|
|
91
|
+
constraints: ruleDef?.constraintCount ? [] : undefined,
|
|
92
|
+
}
|
|
93
|
+
updateField(selectedField.id, {
|
|
94
|
+
validations: [...currentValidations, newRule],
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handleRemoveValidation = (type: string) => {
|
|
99
|
+
updateField(selectedField.id, {
|
|
100
|
+
validations: selectedField.validations.filter((v) => v.type !== type),
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleUpdateValidationConstraints = (
|
|
105
|
+
type: string,
|
|
106
|
+
constraints: (string | number)[],
|
|
107
|
+
) => {
|
|
108
|
+
updateField(selectedField.id, {
|
|
109
|
+
validations: selectedField.validations.map((v) =>
|
|
110
|
+
v.type === type ? { ...v, constraints } : v,
|
|
111
|
+
),
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const getRelationTypeLabel = (type: string | undefined) => {
|
|
116
|
+
return RELATION_TYPES.find((t) => t.value === type)?.label || 'Not Set'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="h-full flex flex-col">
|
|
121
|
+
{/* Header */}
|
|
122
|
+
<div className="p-4 border-b flex items-center justify-between">
|
|
123
|
+
<h2 className="text-xs font-semibold uppercase tracking-wide">
|
|
124
|
+
Field Settings
|
|
125
|
+
</h2>
|
|
126
|
+
<Button
|
|
127
|
+
variant="ghost"
|
|
128
|
+
size="icon"
|
|
129
|
+
className="h-8 w-8"
|
|
130
|
+
onClick={() => selectField(null)}
|
|
131
|
+
>
|
|
132
|
+
<X className="h-4 w-4" />
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Content */}
|
|
137
|
+
<div className="flex-1 overflow-y-auto">
|
|
138
|
+
<Accordion
|
|
139
|
+
type="multiple"
|
|
140
|
+
defaultValue={['basic-info', 'constraints']}
|
|
141
|
+
className="w-full"
|
|
142
|
+
>
|
|
143
|
+
{/* Basic Info */}
|
|
144
|
+
<AccordionItem value="basic-info" className="px-5">
|
|
145
|
+
<AccordionTrigger className="text-xs font-semibold uppercase tracking-wide">
|
|
146
|
+
Basic Info
|
|
147
|
+
</AccordionTrigger>
|
|
148
|
+
<AccordionContent className="space-y-4">
|
|
149
|
+
<div className="space-y-1.5">
|
|
150
|
+
<Label className="text-xs">Display Name</Label>
|
|
151
|
+
<Input
|
|
152
|
+
value={selectedField.displayName}
|
|
153
|
+
onChange={(e) =>
|
|
154
|
+
updateField(selectedField.id, {
|
|
155
|
+
displayName: e.target.value,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
className="font-medium"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="space-y-1.5">
|
|
163
|
+
<Label className="text-xs">API ID</Label>
|
|
164
|
+
<div className="relative">
|
|
165
|
+
<Input
|
|
166
|
+
value={selectedField.name}
|
|
167
|
+
onChange={(e) =>
|
|
168
|
+
updateField(selectedField.id, { name: e.target.value })
|
|
169
|
+
}
|
|
170
|
+
className="font-mono text-muted-foreground pr-8"
|
|
171
|
+
/>
|
|
172
|
+
<Lock className="absolute right-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground/50" />
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="space-y-1.5">
|
|
177
|
+
<Label className="text-xs">Field Type</Label>
|
|
178
|
+
<Select
|
|
179
|
+
value={selectedField.type}
|
|
180
|
+
onValueChange={(value) =>
|
|
181
|
+
updateField(selectedField.id, { type: value as FieldType })
|
|
182
|
+
}
|
|
183
|
+
>
|
|
184
|
+
<SelectTrigger>
|
|
185
|
+
<SelectValue />
|
|
186
|
+
</SelectTrigger>
|
|
187
|
+
<SelectContent>
|
|
188
|
+
{FIELD_TYPES.map((type) => (
|
|
189
|
+
<SelectItem key={type.id} value={type.id}>
|
|
190
|
+
<div className="flex items-center gap-2">
|
|
191
|
+
<type.icon className="h-4 w-4" />
|
|
192
|
+
{type.label}
|
|
193
|
+
</div>
|
|
194
|
+
</SelectItem>
|
|
195
|
+
))}
|
|
196
|
+
</SelectContent>
|
|
197
|
+
</Select>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* UI Subtype Selector */}
|
|
201
|
+
{uiSubtypes.length > 1 && (
|
|
202
|
+
<div className="space-y-1.5">
|
|
203
|
+
<Label className="text-xs">UI Type</Label>
|
|
204
|
+
<Select
|
|
205
|
+
value={selectedField.ui.type || uiSubtypes[0]?.id}
|
|
206
|
+
onValueChange={(value) =>
|
|
207
|
+
updateField(selectedField.id, {
|
|
208
|
+
ui: { ...selectedField.ui, type: value },
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
>
|
|
212
|
+
<SelectTrigger>
|
|
213
|
+
<SelectValue />
|
|
214
|
+
</SelectTrigger>
|
|
215
|
+
<SelectContent>
|
|
216
|
+
{uiSubtypes.map((subtype) => (
|
|
217
|
+
<SelectItem key={subtype.id} value={subtype.id}>
|
|
218
|
+
<div className="flex flex-col">
|
|
219
|
+
<span>{subtype.label}</span>
|
|
220
|
+
</div>
|
|
221
|
+
</SelectItem>
|
|
222
|
+
))}
|
|
223
|
+
</SelectContent>
|
|
224
|
+
</Select>
|
|
225
|
+
<p className="text-xs text-muted-foreground">
|
|
226
|
+
{uiSubtypes.find((s) => s.id === selectedField.ui.type)
|
|
227
|
+
?.description || uiSubtypes[0]?.description}
|
|
228
|
+
</p>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</AccordionContent>
|
|
232
|
+
</AccordionItem>
|
|
233
|
+
|
|
234
|
+
{/* UI Settings */}
|
|
235
|
+
<AccordionItem value="ui-settings" className="px-5">
|
|
236
|
+
<AccordionTrigger className="text-xs font-semibold uppercase tracking-wide">
|
|
237
|
+
UI Settings
|
|
238
|
+
</AccordionTrigger>
|
|
239
|
+
<AccordionContent className="space-y-4">
|
|
240
|
+
<div className="space-y-1.5">
|
|
241
|
+
<Label className="text-xs">Tab</Label>
|
|
242
|
+
<Input
|
|
243
|
+
value={selectedField.ui.tab || ''}
|
|
244
|
+
onChange={(e) =>
|
|
245
|
+
updateField(selectedField.id, {
|
|
246
|
+
ui: { ...selectedField.ui, tab: e.target.value },
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
placeholder="General"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div className="space-y-1.5">
|
|
254
|
+
<Label className="text-xs">Description</Label>
|
|
255
|
+
<Textarea
|
|
256
|
+
value={selectedField.ui.description || ''}
|
|
257
|
+
onChange={(e) =>
|
|
258
|
+
updateField(selectedField.id, {
|
|
259
|
+
ui: { ...selectedField.ui, description: e.target.value },
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
placeholder="Help text for this field"
|
|
263
|
+
rows={2}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div className="flex items-center justify-between">
|
|
268
|
+
<Label className="text-xs">Show in Side Panel</Label>
|
|
269
|
+
<Switch
|
|
270
|
+
checked={selectedField.ui.side || false}
|
|
271
|
+
onCheckedChange={(checked) =>
|
|
272
|
+
updateField(selectedField.id, {
|
|
273
|
+
ui: { ...selectedField.ui, side: checked },
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="flex items-center justify-between">
|
|
280
|
+
<Label className="text-xs">Half Width (Row)</Label>
|
|
281
|
+
<Switch
|
|
282
|
+
checked={selectedField.ui.row || false}
|
|
283
|
+
onCheckedChange={(checked) =>
|
|
284
|
+
updateField(selectedField.id, {
|
|
285
|
+
ui: { ...selectedField.ui, row: checked },
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</AccordionContent>
|
|
291
|
+
</AccordionItem>
|
|
292
|
+
|
|
293
|
+
{/* Constraints */}
|
|
294
|
+
<AccordionItem value="constraints" className="px-5">
|
|
295
|
+
<AccordionTrigger className="text-xs font-semibold uppercase tracking-wide">
|
|
296
|
+
Constraints
|
|
297
|
+
</AccordionTrigger>
|
|
298
|
+
<AccordionContent className="space-y-4">
|
|
299
|
+
<div className="flex items-center justify-between">
|
|
300
|
+
<Label className="text-xs">Required</Label>
|
|
301
|
+
<Switch
|
|
302
|
+
checked={selectedField.prop.required || false}
|
|
303
|
+
onCheckedChange={(checked) =>
|
|
304
|
+
updateField(selectedField.id, {
|
|
305
|
+
prop: { ...selectedField.prop, required: checked },
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div className="flex items-center justify-between">
|
|
312
|
+
<Label className="text-xs">Unique</Label>
|
|
313
|
+
<Switch
|
|
314
|
+
checked={selectedField.prop.unique || false}
|
|
315
|
+
onCheckedChange={(checked) =>
|
|
316
|
+
updateField(selectedField.id, {
|
|
317
|
+
prop: { ...selectedField.prop, unique: checked },
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
/>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div className="flex items-center justify-between">
|
|
324
|
+
<Label className="text-xs">Hidden</Label>
|
|
325
|
+
<Switch
|
|
326
|
+
checked={selectedField.prop.hidden || false}
|
|
327
|
+
onCheckedChange={(checked) =>
|
|
328
|
+
updateField(selectedField.id, {
|
|
329
|
+
prop: { ...selectedField.prop, hidden: checked },
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div className="flex items-center justify-between">
|
|
336
|
+
<Label className="text-xs">Read Only</Label>
|
|
337
|
+
<Switch
|
|
338
|
+
checked={selectedField.prop.readonly || false}
|
|
339
|
+
onCheckedChange={(checked) =>
|
|
340
|
+
updateField(selectedField.id, {
|
|
341
|
+
prop: { ...selectedField.prop, readonly: checked },
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div className="flex items-center justify-between">
|
|
348
|
+
<Label className="text-xs">Enable i18n</Label>
|
|
349
|
+
<Switch
|
|
350
|
+
checked={selectedField.prop.intl || false}
|
|
351
|
+
onCheckedChange={(checked) =>
|
|
352
|
+
updateField(selectedField.id, {
|
|
353
|
+
prop: { ...selectedField.prop, intl: checked },
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
358
|
+
</AccordionContent>
|
|
359
|
+
</AccordionItem>
|
|
360
|
+
|
|
361
|
+
{/* Select Options */}
|
|
362
|
+
{fieldTypeDef?.hasOptions && (
|
|
363
|
+
<AccordionItem value="select-options" className="px-5">
|
|
364
|
+
<AccordionTrigger className="text-xs font-semibold uppercase tracking-wide">
|
|
365
|
+
Select Options
|
|
366
|
+
</AccordionTrigger>
|
|
367
|
+
<AccordionContent className="space-y-4">
|
|
368
|
+
{(selectedField.ui.options || []).map((option, index) => (
|
|
369
|
+
<div
|
|
370
|
+
key={option.key || `option-${index}`}
|
|
371
|
+
className="flex items-center gap-2"
|
|
372
|
+
>
|
|
373
|
+
<Input
|
|
374
|
+
value={option.key}
|
|
375
|
+
onChange={(e) => {
|
|
376
|
+
const newOptions = [...(selectedField.ui.options || [])]
|
|
377
|
+
newOptions[index] = { ...option, key: e.target.value }
|
|
378
|
+
updateField(selectedField.id, {
|
|
379
|
+
ui: { ...selectedField.ui, options: newOptions },
|
|
380
|
+
})
|
|
381
|
+
}}
|
|
382
|
+
placeholder="Key"
|
|
383
|
+
className="flex-1"
|
|
384
|
+
/>
|
|
385
|
+
<Input
|
|
386
|
+
value={option.value}
|
|
387
|
+
onChange={(e) => {
|
|
388
|
+
const newOptions = [...(selectedField.ui.options || [])]
|
|
389
|
+
newOptions[index] = { ...option, value: e.target.value }
|
|
390
|
+
updateField(selectedField.id, {
|
|
391
|
+
ui: { ...selectedField.ui, options: newOptions },
|
|
392
|
+
})
|
|
393
|
+
}}
|
|
394
|
+
placeholder="Label"
|
|
395
|
+
className="flex-1"
|
|
396
|
+
/>
|
|
397
|
+
<Button
|
|
398
|
+
variant="ghost"
|
|
399
|
+
size="icon"
|
|
400
|
+
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
|
401
|
+
onClick={() => handleRemoveOption(index)}
|
|
402
|
+
>
|
|
403
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
404
|
+
</Button>
|
|
405
|
+
</div>
|
|
406
|
+
))}
|
|
407
|
+
|
|
408
|
+
<div className="flex items-center gap-2">
|
|
409
|
+
<Input
|
|
410
|
+
value={newOptionKey}
|
|
411
|
+
onChange={(e) => setNewOptionKey(e.target.value)}
|
|
412
|
+
placeholder="Key"
|
|
413
|
+
className="flex-1"
|
|
414
|
+
/>
|
|
415
|
+
<Input
|
|
416
|
+
value={newOptionValue}
|
|
417
|
+
onChange={(e) => setNewOptionValue(e.target.value)}
|
|
418
|
+
placeholder="Label"
|
|
419
|
+
className="flex-1"
|
|
420
|
+
/>
|
|
421
|
+
<Button
|
|
422
|
+
variant="outline"
|
|
423
|
+
size="icon"
|
|
424
|
+
className="h-8 w-8"
|
|
425
|
+
onClick={handleAddOption}
|
|
426
|
+
disabled={!newOptionKey.trim() || !newOptionValue.trim()}
|
|
427
|
+
>
|
|
428
|
+
<Plus className="h-3.5 w-3.5" />
|
|
429
|
+
</Button>
|
|
430
|
+
</div>
|
|
431
|
+
</AccordionContent>
|
|
432
|
+
</AccordionItem>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{/* Relation Config */}
|
|
436
|
+
{fieldTypeDef?.hasRelationConfig && (
|
|
437
|
+
<AccordionItem value="relation-config" className="px-5">
|
|
438
|
+
<AccordionTrigger className="text-xs font-semibold uppercase tracking-wide">
|
|
439
|
+
Relation Configuration
|
|
440
|
+
</AccordionTrigger>
|
|
441
|
+
<AccordionContent className="space-y-4">
|
|
442
|
+
{/* Visual Relation Preview */}
|
|
443
|
+
<div className="p-3 bg-muted/50 rounded-lg border space-y-3">
|
|
444
|
+
<div className="flex items-center justify-between text-xs">
|
|
445
|
+
<span className="text-muted-foreground">Relation Type</span>
|
|
446
|
+
<span className="font-medium">
|
|
447
|
+
{getRelationTypeLabel(
|
|
448
|
+
selectedField.relationConfig?.relationType,
|
|
449
|
+
)}
|
|
450
|
+
</span>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* Visual Diagram */}
|
|
454
|
+
<div className="flex items-center justify-center py-3 gap-3">
|
|
455
|
+
<div className="px-2 py-1 bg-background border rounded text-xs font-medium shadow-sm">
|
|
456
|
+
{state.schema.name || 'Current'}
|
|
457
|
+
</div>
|
|
458
|
+
<div className="flex items-center gap-0.5">
|
|
459
|
+
<div className="w-2 h-2 bg-muted-foreground/60 rounded-full" />
|
|
460
|
+
<div className="w-8 h-px bg-muted-foreground/60" />
|
|
461
|
+
<ChevronRight className="w-3 h-3 text-muted-foreground/60" />
|
|
462
|
+
</div>
|
|
463
|
+
<div className="px-2 py-1 bg-background border rounded text-xs font-medium shadow-sm">
|
|
464
|
+
{selectedField.relationConfig?.targetSchema ||
|
|
465
|
+
'Select...'}
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<Button
|
|
470
|
+
variant="secondary"
|
|
471
|
+
size="sm"
|
|
472
|
+
className="w-full"
|
|
473
|
+
onClick={() => setRelationModalOpen(true)}
|
|
474
|
+
>
|
|
475
|
+
Change Relation
|
|
476
|
+
</Button>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div className="space-y-1.5">
|
|
480
|
+
<Label className="text-xs">Related Collection</Label>
|
|
481
|
+
<Select
|
|
482
|
+
value={selectedField.relationConfig?.targetSchema || ''}
|
|
483
|
+
onValueChange={(value) =>
|
|
484
|
+
updateField(selectedField.id, {
|
|
485
|
+
relationConfig: {
|
|
486
|
+
...selectedField.relationConfig,
|
|
487
|
+
targetSchema: value,
|
|
488
|
+
relationType:
|
|
489
|
+
selectedField.relationConfig?.relationType ||
|
|
490
|
+
'manyToOne',
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
>
|
|
495
|
+
<SelectTrigger>
|
|
496
|
+
<SelectValue placeholder="Select schema..." />
|
|
497
|
+
</SelectTrigger>
|
|
498
|
+
<SelectContent>
|
|
499
|
+
{schemas?.map((schema) => (
|
|
500
|
+
<SelectItem key={schema} value={schema}>
|
|
501
|
+
{schema}
|
|
502
|
+
</SelectItem>
|
|
503
|
+
))}
|
|
504
|
+
</SelectContent>
|
|
505
|
+
</Select>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<div className="space-y-1.5">
|
|
509
|
+
<Label className="text-xs">Relation Type</Label>
|
|
510
|
+
<Select
|
|
511
|
+
value={
|
|
512
|
+
selectedField.relationConfig?.relationType || 'manyToOne'
|
|
513
|
+
}
|
|
514
|
+
onValueChange={(value) =>
|
|
515
|
+
updateField(selectedField.id, {
|
|
516
|
+
relationConfig: {
|
|
517
|
+
...selectedField.relationConfig,
|
|
518
|
+
targetSchema:
|
|
519
|
+
selectedField.relationConfig?.targetSchema || '',
|
|
520
|
+
relationType: value as any,
|
|
521
|
+
},
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
>
|
|
525
|
+
<SelectTrigger>
|
|
526
|
+
<SelectValue />
|
|
527
|
+
</SelectTrigger>
|
|
528
|
+
<SelectContent>
|
|
529
|
+
{RELATION_TYPES.map((type) => (
|
|
530
|
+
<SelectItem key={type.value} value={type.value}>
|
|
531
|
+
{type.label}
|
|
532
|
+
</SelectItem>
|
|
533
|
+
))}
|
|
534
|
+
</SelectContent>
|
|
535
|
+
</Select>
|
|
536
|
+
</div>
|
|
537
|
+
</AccordionContent>
|
|
538
|
+
</AccordionItem>
|
|
539
|
+
)}
|
|
540
|
+
|
|
541
|
+
{/* Validations */}
|
|
542
|
+
<AccordionItem value="validations" className="px-5">
|
|
543
|
+
<AccordionTrigger className="text-xs font-semibold uppercase tracking-wide">
|
|
544
|
+
Validations
|
|
545
|
+
</AccordionTrigger>
|
|
546
|
+
<AccordionContent className="space-y-4">
|
|
547
|
+
{selectedField.validations.map((validation) => {
|
|
548
|
+
const ruleDef = getValidationRuleDefinition(validation.type)
|
|
549
|
+
return (
|
|
550
|
+
<div
|
|
551
|
+
key={validation.type}
|
|
552
|
+
className="p-3 bg-muted/50 rounded-lg border space-y-2"
|
|
553
|
+
>
|
|
554
|
+
<div className="flex items-center justify-between">
|
|
555
|
+
<span className="text-sm font-medium">
|
|
556
|
+
{ruleDef?.label || validation.type}
|
|
557
|
+
</span>
|
|
558
|
+
<Button
|
|
559
|
+
variant="ghost"
|
|
560
|
+
size="icon"
|
|
561
|
+
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
|
562
|
+
onClick={() => handleRemoveValidation(validation.type)}
|
|
563
|
+
>
|
|
564
|
+
<Trash2 className="h-3 w-3" />
|
|
565
|
+
</Button>
|
|
566
|
+
</div>
|
|
567
|
+
{ruleDef?.constraintCount &&
|
|
568
|
+
ruleDef.constraintCount > 0 && (
|
|
569
|
+
<div className="flex gap-2">
|
|
570
|
+
{Array.from({ length: ruleDef.constraintCount }).map(
|
|
571
|
+
(_, i) => (
|
|
572
|
+
<Input
|
|
573
|
+
key={`${validation.type}-constraint-${i}`}
|
|
574
|
+
type={
|
|
575
|
+
validation.type.includes('Length') ||
|
|
576
|
+
validation.type.includes('Min') ||
|
|
577
|
+
validation.type.includes('Max')
|
|
578
|
+
? 'number'
|
|
579
|
+
: 'text'
|
|
580
|
+
}
|
|
581
|
+
placeholder={
|
|
582
|
+
ruleDef.constraintLabels?.[i] ||
|
|
583
|
+
`Arg ${i + 1}`
|
|
584
|
+
}
|
|
585
|
+
value={validation.constraints?.[i] ?? ''}
|
|
586
|
+
onChange={(e) => {
|
|
587
|
+
const newConstraints = [
|
|
588
|
+
...(validation.constraints || []),
|
|
589
|
+
]
|
|
590
|
+
newConstraints[i] =
|
|
591
|
+
validation.type.includes('Length') ||
|
|
592
|
+
validation.type.includes('Min') ||
|
|
593
|
+
validation.type.includes('Max')
|
|
594
|
+
? Number(e.target.value)
|
|
595
|
+
: e.target.value
|
|
596
|
+
handleUpdateValidationConstraints(
|
|
597
|
+
validation.type,
|
|
598
|
+
newConstraints,
|
|
599
|
+
)
|
|
600
|
+
}}
|
|
601
|
+
className="flex-1"
|
|
602
|
+
/>
|
|
603
|
+
),
|
|
604
|
+
)}
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
)
|
|
609
|
+
})}
|
|
610
|
+
|
|
611
|
+
<Select onValueChange={handleAddValidation}>
|
|
612
|
+
<SelectTrigger>
|
|
613
|
+
<SelectValue placeholder="Add validation..." />
|
|
614
|
+
</SelectTrigger>
|
|
615
|
+
<SelectContent>
|
|
616
|
+
{availableValidations
|
|
617
|
+
.filter(
|
|
618
|
+
(v) =>
|
|
619
|
+
!selectedField.validations.some((sv) => sv.type === v),
|
|
620
|
+
)
|
|
621
|
+
.map((validation) => {
|
|
622
|
+
const ruleDef = getValidationRuleDefinition(validation)
|
|
623
|
+
return (
|
|
624
|
+
<SelectItem key={validation} value={validation}>
|
|
625
|
+
{ruleDef?.label || validation}
|
|
626
|
+
</SelectItem>
|
|
627
|
+
)
|
|
628
|
+
})}
|
|
629
|
+
</SelectContent>
|
|
630
|
+
</Select>
|
|
631
|
+
</AccordionContent>
|
|
632
|
+
</AccordionItem>
|
|
633
|
+
</Accordion>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
{/* Relation Config Modal */}
|
|
637
|
+
{fieldTypeDef?.hasRelationConfig && (
|
|
638
|
+
<RelationConfigModal
|
|
639
|
+
open={relationModalOpen}
|
|
640
|
+
onOpenChange={setRelationModalOpen}
|
|
641
|
+
currentSchema={state.schema.name}
|
|
642
|
+
relationConfig={selectedField.relationConfig}
|
|
643
|
+
onSave={(config: RelationConfig) => {
|
|
644
|
+
updateField(selectedField.id, {
|
|
645
|
+
relationConfig: config,
|
|
646
|
+
})
|
|
647
|
+
}}
|
|
648
|
+
/>
|
|
649
|
+
)}
|
|
650
|
+
</div>
|
|
651
|
+
)
|
|
652
|
+
}
|