@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,322 @@
|
|
|
1
|
+
import { PageContent, PageHeader, useSchema } from '@magnet-cms/admin'
|
|
2
|
+
import { Button, Spinner } from '@magnet-cms/ui/components'
|
|
3
|
+
import {
|
|
4
|
+
AlertCircle,
|
|
5
|
+
Boxes,
|
|
6
|
+
Code2,
|
|
7
|
+
FileJson,
|
|
8
|
+
Info,
|
|
9
|
+
Lock,
|
|
10
|
+
PencilRuler,
|
|
11
|
+
Rocket,
|
|
12
|
+
Settings2,
|
|
13
|
+
} from 'lucide-react'
|
|
14
|
+
import { createContext, useContext, useState } from 'react'
|
|
15
|
+
import { useParams } from 'react-router-dom'
|
|
16
|
+
|
|
17
|
+
// Context to track if schema is read-only (existing code-defined schema)
|
|
18
|
+
const ReadOnlyContext = createContext(false)
|
|
19
|
+
import {
|
|
20
|
+
SchemaBuilderContext,
|
|
21
|
+
useSchemaBuilder,
|
|
22
|
+
useSchemaBuilderState,
|
|
23
|
+
} from '../hooks/useSchemaBuilder'
|
|
24
|
+
import type { SchemaBuilderState, SchemaField } from '../types/builder.types'
|
|
25
|
+
import { DEFAULT_BUILDER_STATE } from '../types/builder.types'
|
|
26
|
+
import { AddFieldDialog } from './AddFieldDialog'
|
|
27
|
+
import { CodePreview } from './CodePreview'
|
|
28
|
+
import { FieldList } from './FieldList'
|
|
29
|
+
import { FieldSettingsPanel } from './FieldSettingsPanel'
|
|
30
|
+
import { SchemaList } from './SchemaList'
|
|
31
|
+
import { SchemaOptionsDialog } from './SchemaOptionsDialog'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Transform SchemaMetadata from discovery API to builder state
|
|
35
|
+
*/
|
|
36
|
+
function schemaMetadataToBuilderState(
|
|
37
|
+
name: string,
|
|
38
|
+
metadata: any,
|
|
39
|
+
): SchemaBuilderState {
|
|
40
|
+
return {
|
|
41
|
+
schema: {
|
|
42
|
+
name: name.charAt(0).toUpperCase() + name.slice(1),
|
|
43
|
+
apiId: name,
|
|
44
|
+
versioning: metadata.options?.versioning ?? true,
|
|
45
|
+
i18n: metadata.options?.i18n ?? true,
|
|
46
|
+
},
|
|
47
|
+
fields:
|
|
48
|
+
metadata.properties?.map((prop: any, index: number) => ({
|
|
49
|
+
id: `field_${index}_${prop.name}`,
|
|
50
|
+
name: prop.name,
|
|
51
|
+
displayName: prop.ui?.label || prop.name,
|
|
52
|
+
type: inferFieldType(prop),
|
|
53
|
+
tsType: prop.type || 'string',
|
|
54
|
+
prop: {
|
|
55
|
+
required: prop.required || false,
|
|
56
|
+
unique: prop.unique || false,
|
|
57
|
+
intl: prop.intl || false,
|
|
58
|
+
hidden: prop.hidden || false,
|
|
59
|
+
readonly: prop.readonly || false,
|
|
60
|
+
},
|
|
61
|
+
ui: prop.ui || {},
|
|
62
|
+
validations: (prop.validations || []).map((v: any) => ({
|
|
63
|
+
type: v.name || v.type,
|
|
64
|
+
constraints: v.constraints,
|
|
65
|
+
})),
|
|
66
|
+
})) || [],
|
|
67
|
+
selectedFieldId: null,
|
|
68
|
+
viewMode: 'builder',
|
|
69
|
+
isDirty: false,
|
|
70
|
+
lastSaved: null,
|
|
71
|
+
isNew: false,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Infer field type from property metadata
|
|
77
|
+
*/
|
|
78
|
+
function inferFieldType(prop: any): SchemaField['type'] {
|
|
79
|
+
const uiType = prop.ui?.type
|
|
80
|
+
if (uiType === 'switch' || uiType === 'checkbox') return 'boolean'
|
|
81
|
+
if (uiType === 'date') return 'date'
|
|
82
|
+
if (uiType === 'number') return 'number'
|
|
83
|
+
if (uiType === 'select' || uiType === 'radio') return 'select'
|
|
84
|
+
if (uiType === 'relationship') return 'relation'
|
|
85
|
+
|
|
86
|
+
// prop.type can be a string, object, or array - only process if string
|
|
87
|
+
const tsType = typeof prop.type === 'string' ? prop.type.toLowerCase() : null
|
|
88
|
+
if (tsType === 'number') return 'number'
|
|
89
|
+
if (tsType === 'boolean') return 'boolean'
|
|
90
|
+
if (tsType === 'date') return 'date'
|
|
91
|
+
|
|
92
|
+
return 'text'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function SchemaPlaygroundEditor() {
|
|
96
|
+
const { schemaName } = useParams<{ schemaName: string }>()
|
|
97
|
+
const isNewSchema = !schemaName || schemaName === 'new'
|
|
98
|
+
|
|
99
|
+
// For existing schemas, fetch metadata
|
|
100
|
+
const {
|
|
101
|
+
data: schemaMetadata,
|
|
102
|
+
isLoading,
|
|
103
|
+
error,
|
|
104
|
+
} = useSchema(schemaName && schemaName !== 'new' ? schemaName : undefined)
|
|
105
|
+
|
|
106
|
+
if (!isNewSchema && isLoading) {
|
|
107
|
+
return (
|
|
108
|
+
<div className="flex justify-center items-center h-64">
|
|
109
|
+
<Spinner />
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isNewSchema && error) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="p-4 border-l-4 border-red-500 bg-red-50 text-red-700 rounded m-8">
|
|
117
|
+
<h3 className="font-medium flex items-center gap-2">
|
|
118
|
+
<AlertCircle className="h-4 w-4" />
|
|
119
|
+
Error loading schema
|
|
120
|
+
</h3>
|
|
121
|
+
<p className="text-sm mt-1">{error.message}</p>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If editing existing schema, transform metadata to builder state
|
|
127
|
+
const initialState =
|
|
128
|
+
!isNewSchema && schemaMetadata && schemaName && schemaName !== 'new'
|
|
129
|
+
? schemaMetadataToBuilderState(schemaName, schemaMetadata)
|
|
130
|
+
: { ...DEFAULT_BUILDER_STATE, isNew: true }
|
|
131
|
+
|
|
132
|
+
// TODO: In the future, we need to track schema source (plugin/core vs user-defined)
|
|
133
|
+
// to determine if a schema should be read-only. For now, all schemas are editable.
|
|
134
|
+
// Plugin and core schemas should be read-only once we have a way to identify them.
|
|
135
|
+
const isReadOnly = false
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<SchemaEditorContentWithState
|
|
139
|
+
key={schemaName || 'new'}
|
|
140
|
+
initialState={initialState}
|
|
141
|
+
isReadOnly={isReadOnly}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function SchemaEditorContentWithState({
|
|
147
|
+
initialState,
|
|
148
|
+
isReadOnly,
|
|
149
|
+
}: {
|
|
150
|
+
initialState: SchemaBuilderState
|
|
151
|
+
isReadOnly: boolean
|
|
152
|
+
}) {
|
|
153
|
+
const context = useSchemaBuilderState(initialState)
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<ReadOnlyContext.Provider value={isReadOnly}>
|
|
157
|
+
<SchemaBuilderContext.Provider value={context}>
|
|
158
|
+
<SchemaEditorInner />
|
|
159
|
+
</SchemaBuilderContext.Provider>
|
|
160
|
+
</ReadOnlyContext.Provider>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function SchemaEditorInner() {
|
|
165
|
+
const { state, setViewMode, updateSchema, generatedCode } = useSchemaBuilder()
|
|
166
|
+
const isReadOnly = useContext(ReadOnlyContext)
|
|
167
|
+
const [addFieldOpen, setAddFieldOpen] = useState(false)
|
|
168
|
+
const [optionsOpen, setOptionsOpen] = useState(false)
|
|
169
|
+
|
|
170
|
+
const handleDeploy = async () => {
|
|
171
|
+
// TODO: Implement actual deployment
|
|
172
|
+
alert(
|
|
173
|
+
`Deploy functionality will be implemented in the backend phase.\n\nGenerated code:\n\n${generatedCode}`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const statusBadges: {
|
|
178
|
+
type: 'warning' | 'success' | 'error' | 'info'
|
|
179
|
+
label: string
|
|
180
|
+
dot?: boolean
|
|
181
|
+
}[] = []
|
|
182
|
+
if (isReadOnly) {
|
|
183
|
+
statusBadges.push({
|
|
184
|
+
type: 'info',
|
|
185
|
+
label: 'Read-only',
|
|
186
|
+
})
|
|
187
|
+
} else if (state.isDirty) {
|
|
188
|
+
statusBadges.push({
|
|
189
|
+
type: 'warning',
|
|
190
|
+
label: 'Unsaved changes',
|
|
191
|
+
dot: true,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
if (!isReadOnly && state.lastSaved) {
|
|
195
|
+
statusBadges.push({ type: 'success', label: 'Saved', dot: true })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="flex flex-col h-full">
|
|
200
|
+
<PageHeader
|
|
201
|
+
status={statusBadges.length > 0 ? statusBadges : undefined}
|
|
202
|
+
icon={isReadOnly ? Lock : Boxes}
|
|
203
|
+
title={
|
|
204
|
+
isReadOnly
|
|
205
|
+
? state.schema.name || 'Schema'
|
|
206
|
+
: {
|
|
207
|
+
value: state.schema.name,
|
|
208
|
+
onChange: (name) => updateSchema({ name }),
|
|
209
|
+
placeholder: 'SchemaName',
|
|
210
|
+
editable: true as const,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
description={`api::${state.schema.name?.toLowerCase() || 'schema'}.${state.schema.name?.toLowerCase() || 'schema'}`}
|
|
214
|
+
tabs={{
|
|
215
|
+
items: [
|
|
216
|
+
{ id: 'builder', label: 'Builder', icon: PencilRuler },
|
|
217
|
+
{ id: 'json', label: 'JSON', icon: FileJson },
|
|
218
|
+
{ id: 'code', label: 'Code', icon: Code2 },
|
|
219
|
+
],
|
|
220
|
+
value: state.viewMode,
|
|
221
|
+
onChange: setViewMode,
|
|
222
|
+
}}
|
|
223
|
+
actions={
|
|
224
|
+
isReadOnly ? undefined : (
|
|
225
|
+
<>
|
|
226
|
+
<Button
|
|
227
|
+
variant="outline"
|
|
228
|
+
size="icon"
|
|
229
|
+
className="h-8 w-8"
|
|
230
|
+
onClick={() => setOptionsOpen(true)}
|
|
231
|
+
title="Schema Options"
|
|
232
|
+
>
|
|
233
|
+
<Settings2 className="h-4 w-4" />
|
|
234
|
+
</Button>
|
|
235
|
+
<Button variant="outline" size="sm" disabled>
|
|
236
|
+
Preview API
|
|
237
|
+
</Button>
|
|
238
|
+
<Button
|
|
239
|
+
size="sm"
|
|
240
|
+
onClick={handleDeploy}
|
|
241
|
+
disabled={!state.schema.name}
|
|
242
|
+
>
|
|
243
|
+
<Rocket className="h-4 w-4 mr-2" />
|
|
244
|
+
Deploy Changes
|
|
245
|
+
</Button>
|
|
246
|
+
</>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
{/* Read-only banner for existing schemas */}
|
|
252
|
+
{isReadOnly && (
|
|
253
|
+
<div className="px-6 py-2 border-b border-border bg-amber-50 dark:bg-amber-950/30 flex items-center gap-2">
|
|
254
|
+
<Lock className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />
|
|
255
|
+
<span className="text-xs text-amber-700 dark:text-amber-400">
|
|
256
|
+
This schema is defined in code and cannot be modified here. View the
|
|
257
|
+
generated code in the Code tab to copy and customize.
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{/* Main Content - 2x7x3 grid layout */}
|
|
263
|
+
<div className="flex-1 overflow-hidden grid grid-cols-12 h-full">
|
|
264
|
+
{/* Schema List - 2 columns */}
|
|
265
|
+
<div className="col-span-2 border-r bg-muted/30 overflow-y-auto">
|
|
266
|
+
<SchemaList />
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Editor Content - 7 columns */}
|
|
270
|
+
<div className="col-span-7 overflow-hidden flex flex-col">
|
|
271
|
+
{state.viewMode === 'builder' ? (
|
|
272
|
+
<PageContent className="p-6 overflow-y-auto flex-1">
|
|
273
|
+
<div className="space-y-4">
|
|
274
|
+
{/* Info Alert - only show for new schemas */}
|
|
275
|
+
{!isReadOnly && (
|
|
276
|
+
<div className="p-3 bg-blue-50 border border-blue-100 rounded-lg flex items-start gap-3">
|
|
277
|
+
<Info className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
|
|
278
|
+
<div className="text-xs text-blue-900">
|
|
279
|
+
<span className="font-medium">Schema Guidelines:</span>{' '}
|
|
280
|
+
Keep field names in camelCase for better compatibility.
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* Field List */}
|
|
286
|
+
<FieldList
|
|
287
|
+
onAddField={
|
|
288
|
+
isReadOnly ? undefined : () => setAddFieldOpen(true)
|
|
289
|
+
}
|
|
290
|
+
readOnly={isReadOnly}
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
</PageContent>
|
|
294
|
+
) : (
|
|
295
|
+
/* Code/JSON View */
|
|
296
|
+
<PageContent className="p-6 overflow-y-auto flex-1">
|
|
297
|
+
<CodePreview mode={state.viewMode} />
|
|
298
|
+
</PageContent>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{/* Settings Panel - 3 columns */}
|
|
303
|
+
<div className="col-span-3 border-l bg-background overflow-y-auto">
|
|
304
|
+
<FieldSettingsPanel />
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
{/* Dialogs - only for editable schemas */}
|
|
309
|
+
{!isReadOnly && (
|
|
310
|
+
<>
|
|
311
|
+
<AddFieldDialog open={addFieldOpen} onOpenChange={setAddFieldOpen} />
|
|
312
|
+
<SchemaOptionsDialog
|
|
313
|
+
open={optionsOpen}
|
|
314
|
+
onOpenChange={setOptionsOpen}
|
|
315
|
+
/>
|
|
316
|
+
</>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default SchemaPlaygroundEditor
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Calendar,
|
|
3
|
+
Hash,
|
|
4
|
+
Link2,
|
|
5
|
+
List,
|
|
6
|
+
type LucideIcon,
|
|
7
|
+
ToggleLeft,
|
|
8
|
+
Type,
|
|
9
|
+
} from 'lucide-react'
|
|
10
|
+
import type {
|
|
11
|
+
FieldType,
|
|
12
|
+
FieldUIConfig,
|
|
13
|
+
ValidationRule,
|
|
14
|
+
} from '../types/builder.types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Field type definition for the builder UI
|
|
18
|
+
*/
|
|
19
|
+
export interface FieldTypeDefinition {
|
|
20
|
+
id: FieldType
|
|
21
|
+
label: string
|
|
22
|
+
description: string
|
|
23
|
+
icon: LucideIcon
|
|
24
|
+
tsType: string
|
|
25
|
+
defaultValidations: ValidationRule[]
|
|
26
|
+
defaultUI: FieldUIConfig
|
|
27
|
+
hasOptions?: boolean // For select fields
|
|
28
|
+
hasRelationConfig?: boolean // For relation fields
|
|
29
|
+
requiresTransformer?: boolean // For Date fields
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* All available field types in the schema builder
|
|
34
|
+
*/
|
|
35
|
+
export const FIELD_TYPES: FieldTypeDefinition[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'text',
|
|
38
|
+
label: 'Text',
|
|
39
|
+
description: 'Short or long text content',
|
|
40
|
+
icon: Type,
|
|
41
|
+
tsType: 'string',
|
|
42
|
+
defaultValidations: [{ type: 'IsString' }, { type: 'IsNotEmpty' }],
|
|
43
|
+
defaultUI: { type: 'text' },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'number',
|
|
47
|
+
label: 'Number',
|
|
48
|
+
description: 'Integer or decimal numbers',
|
|
49
|
+
icon: Hash,
|
|
50
|
+
tsType: 'number',
|
|
51
|
+
defaultValidations: [{ type: 'IsNumber' }],
|
|
52
|
+
defaultUI: { type: 'number' },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'date',
|
|
56
|
+
label: 'Date',
|
|
57
|
+
description: 'Date and time values',
|
|
58
|
+
icon: Calendar,
|
|
59
|
+
tsType: 'Date',
|
|
60
|
+
defaultValidations: [{ type: 'IsDate' }],
|
|
61
|
+
defaultUI: { type: 'date' },
|
|
62
|
+
requiresTransformer: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'boolean',
|
|
66
|
+
label: 'Boolean',
|
|
67
|
+
description: 'True or false toggle',
|
|
68
|
+
icon: ToggleLeft,
|
|
69
|
+
tsType: 'boolean',
|
|
70
|
+
defaultValidations: [{ type: 'IsBoolean' }],
|
|
71
|
+
defaultUI: { type: 'switch' },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'select',
|
|
75
|
+
label: 'Select',
|
|
76
|
+
description: 'Dropdown with predefined options',
|
|
77
|
+
icon: List,
|
|
78
|
+
tsType: 'string',
|
|
79
|
+
defaultValidations: [{ type: 'IsString' }],
|
|
80
|
+
defaultUI: { type: 'select', options: [] },
|
|
81
|
+
hasOptions: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'relation',
|
|
85
|
+
label: 'Relation',
|
|
86
|
+
description: 'Reference to another schema',
|
|
87
|
+
icon: Link2,
|
|
88
|
+
tsType: 'string',
|
|
89
|
+
defaultValidations: [{ type: 'IsString' }],
|
|
90
|
+
defaultUI: { type: 'relationship' },
|
|
91
|
+
hasRelationConfig: true,
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get field type definition by ID
|
|
97
|
+
*/
|
|
98
|
+
export function getFieldTypeDefinition(
|
|
99
|
+
type: FieldType,
|
|
100
|
+
): FieldTypeDefinition | undefined {
|
|
101
|
+
return FIELD_TYPES.find((ft) => ft.id === type)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* UI subtype definition for field type variations
|
|
106
|
+
*/
|
|
107
|
+
export interface UISubtypeDefinition {
|
|
108
|
+
id: string
|
|
109
|
+
label: string
|
|
110
|
+
description: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Available UI subtypes for each base field type.
|
|
115
|
+
* These map to the UITypes in @magnet-cms/common and are rendered by FormBuilder.
|
|
116
|
+
*/
|
|
117
|
+
export const UI_SUBTYPES: Record<FieldType, UISubtypeDefinition[]> = {
|
|
118
|
+
text: [
|
|
119
|
+
{ id: 'text', label: 'Text Input', description: 'Single line text' },
|
|
120
|
+
{ id: 'textarea', label: 'Text Area', description: 'Multi-line text' },
|
|
121
|
+
{ id: 'email', label: 'Email', description: 'Email with validation' },
|
|
122
|
+
{ id: 'phone', label: 'Phone', description: 'Phone number input' },
|
|
123
|
+
{ id: 'richText', label: 'Rich Text', description: 'Rich text editor' },
|
|
124
|
+
],
|
|
125
|
+
number: [
|
|
126
|
+
{
|
|
127
|
+
id: 'number',
|
|
128
|
+
label: 'Number Input',
|
|
129
|
+
description: 'Standard number field',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'quantity',
|
|
133
|
+
label: 'Quantity',
|
|
134
|
+
description: 'Number with +/- buttons',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
date: [
|
|
138
|
+
{
|
|
139
|
+
id: 'date',
|
|
140
|
+
label: 'Date Picker',
|
|
141
|
+
description: 'Calendar date selection',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
boolean: [
|
|
145
|
+
{ id: 'switch', label: 'Switch', description: 'Toggle switch' },
|
|
146
|
+
{ id: 'checkbox', label: 'Checkbox', description: 'Checkbox input' },
|
|
147
|
+
],
|
|
148
|
+
select: [
|
|
149
|
+
{ id: 'select', label: 'Dropdown', description: 'Single select dropdown' },
|
|
150
|
+
{
|
|
151
|
+
id: 'multiSelect',
|
|
152
|
+
label: 'Multi Select',
|
|
153
|
+
description: 'Select multiple options',
|
|
154
|
+
},
|
|
155
|
+
{ id: 'combobox', label: 'Combobox', description: 'Searchable dropdown' },
|
|
156
|
+
{ id: 'radio', label: 'Radio Group', description: 'Radio button options' },
|
|
157
|
+
],
|
|
158
|
+
relation: [
|
|
159
|
+
{
|
|
160
|
+
id: 'relationship',
|
|
161
|
+
label: 'Relationship',
|
|
162
|
+
description: 'Link to another schema',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get UI subtypes for a field type
|
|
169
|
+
*/
|
|
170
|
+
export function getUISubtypes(type: FieldType): UISubtypeDefinition[] {
|
|
171
|
+
return UI_SUBTYPES[type] || []
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the icon component for a field type
|
|
176
|
+
*/
|
|
177
|
+
export function getFieldTypeIcon(type: FieldType): LucideIcon {
|
|
178
|
+
const definition = getFieldTypeDefinition(type)
|
|
179
|
+
return definition?.icon ?? Type
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Available validation rules for each field type
|
|
184
|
+
*/
|
|
185
|
+
export const VALIDATION_RULES_BY_TYPE: Record<FieldType, string[]> = {
|
|
186
|
+
text: [
|
|
187
|
+
'IsString',
|
|
188
|
+
'IsNotEmpty',
|
|
189
|
+
'Length',
|
|
190
|
+
'MinLength',
|
|
191
|
+
'MaxLength',
|
|
192
|
+
'IsEmail',
|
|
193
|
+
'IsUrl',
|
|
194
|
+
'IsUUID',
|
|
195
|
+
'Matches',
|
|
196
|
+
],
|
|
197
|
+
number: ['IsNumber', 'IsInt', 'IsPositive', 'IsNegative', 'Min', 'Max'],
|
|
198
|
+
date: ['IsDate', 'IsNotEmpty', 'MinDate', 'MaxDate'],
|
|
199
|
+
boolean: ['IsBoolean'],
|
|
200
|
+
select: ['IsString', 'IsNotEmpty', 'IsIn'],
|
|
201
|
+
relation: ['IsString', 'IsNotEmpty', 'IsMongoId'],
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validation rule definitions with constraint info
|
|
206
|
+
*/
|
|
207
|
+
export interface ValidationRuleDefinition {
|
|
208
|
+
type: string
|
|
209
|
+
label: string
|
|
210
|
+
description: string
|
|
211
|
+
constraintCount: number // 0 = no args, 1 = single arg, 2 = two args (min, max)
|
|
212
|
+
constraintLabels?: string[]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const VALIDATION_RULE_DEFINITIONS: ValidationRuleDefinition[] = [
|
|
216
|
+
{
|
|
217
|
+
type: 'IsString',
|
|
218
|
+
label: 'Is String',
|
|
219
|
+
description: 'Must be a string',
|
|
220
|
+
constraintCount: 0,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: 'IsNumber',
|
|
224
|
+
label: 'Is Number',
|
|
225
|
+
description: 'Must be a number',
|
|
226
|
+
constraintCount: 0,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: 'IsInt',
|
|
230
|
+
label: 'Is Integer',
|
|
231
|
+
description: 'Must be an integer',
|
|
232
|
+
constraintCount: 0,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'IsBoolean',
|
|
236
|
+
label: 'Is Boolean',
|
|
237
|
+
description: 'Must be true or false',
|
|
238
|
+
constraintCount: 0,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
type: 'IsDate',
|
|
242
|
+
label: 'Is Date',
|
|
243
|
+
description: 'Must be a valid date',
|
|
244
|
+
constraintCount: 0,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: 'IsNotEmpty',
|
|
248
|
+
label: 'Not Empty',
|
|
249
|
+
description: 'Cannot be empty',
|
|
250
|
+
constraintCount: 0,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: 'IsEmail',
|
|
254
|
+
label: 'Is Email',
|
|
255
|
+
description: 'Must be a valid email',
|
|
256
|
+
constraintCount: 0,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
type: 'IsUrl',
|
|
260
|
+
label: 'Is URL',
|
|
261
|
+
description: 'Must be a valid URL',
|
|
262
|
+
constraintCount: 0,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
type: 'IsUUID',
|
|
266
|
+
label: 'Is UUID',
|
|
267
|
+
description: 'Must be a valid UUID',
|
|
268
|
+
constraintCount: 0,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
type: 'IsMongoId',
|
|
272
|
+
label: 'Is MongoDB ID',
|
|
273
|
+
description: 'Must be a valid MongoDB ObjectId',
|
|
274
|
+
constraintCount: 0,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: 'IsPositive',
|
|
278
|
+
label: 'Is Positive',
|
|
279
|
+
description: 'Must be a positive number',
|
|
280
|
+
constraintCount: 0,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
type: 'IsNegative',
|
|
284
|
+
label: 'Is Negative',
|
|
285
|
+
description: 'Must be a negative number',
|
|
286
|
+
constraintCount: 0,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: 'Length',
|
|
290
|
+
label: 'Length',
|
|
291
|
+
description: 'String length between min and max',
|
|
292
|
+
constraintCount: 2,
|
|
293
|
+
constraintLabels: ['Min', 'Max'],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
type: 'MinLength',
|
|
297
|
+
label: 'Min Length',
|
|
298
|
+
description: 'Minimum string length',
|
|
299
|
+
constraintCount: 1,
|
|
300
|
+
constraintLabels: ['Min'],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: 'MaxLength',
|
|
304
|
+
label: 'Max Length',
|
|
305
|
+
description: 'Maximum string length',
|
|
306
|
+
constraintCount: 1,
|
|
307
|
+
constraintLabels: ['Max'],
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: 'Min',
|
|
311
|
+
label: 'Minimum',
|
|
312
|
+
description: 'Minimum number value',
|
|
313
|
+
constraintCount: 1,
|
|
314
|
+
constraintLabels: ['Min'],
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
type: 'Max',
|
|
318
|
+
label: 'Maximum',
|
|
319
|
+
description: 'Maximum number value',
|
|
320
|
+
constraintCount: 1,
|
|
321
|
+
constraintLabels: ['Max'],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
type: 'Matches',
|
|
325
|
+
label: 'Matches Pattern',
|
|
326
|
+
description: 'Must match regex pattern',
|
|
327
|
+
constraintCount: 1,
|
|
328
|
+
constraintLabels: ['Pattern'],
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: 'IsIn',
|
|
332
|
+
label: 'Is In List',
|
|
333
|
+
description: 'Must be one of the allowed values',
|
|
334
|
+
constraintCount: 1,
|
|
335
|
+
constraintLabels: ['Values (comma-separated)'],
|
|
336
|
+
},
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get validation rule definition by type
|
|
341
|
+
*/
|
|
342
|
+
export function getValidationRuleDefinition(
|
|
343
|
+
type: string,
|
|
344
|
+
): ValidationRuleDefinition | undefined {
|
|
345
|
+
return VALIDATION_RULE_DEFINITIONS.find((r) => r.type === type)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Relation type options
|
|
350
|
+
*/
|
|
351
|
+
export const RELATION_TYPES = [
|
|
352
|
+
{
|
|
353
|
+
value: 'oneToOne',
|
|
354
|
+
label: 'One to One',
|
|
355
|
+
description: 'Each record relates to exactly one other',
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
value: 'oneToMany',
|
|
359
|
+
label: 'One to Many',
|
|
360
|
+
description: 'One record relates to many others',
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
value: 'manyToOne',
|
|
364
|
+
label: 'Many to One',
|
|
365
|
+
description: 'Many records relate to one',
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
value: 'manyToMany',
|
|
369
|
+
label: 'Many to Many',
|
|
370
|
+
description: 'Many records relate to many others',
|
|
371
|
+
},
|
|
372
|
+
] as const
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Color mapping for field type badges
|
|
376
|
+
*/
|
|
377
|
+
export const FIELD_TYPE_COLORS: Record<FieldType, string> = {
|
|
378
|
+
text: 'bg-blue-50 text-blue-700 border-blue-200',
|
|
379
|
+
number: 'bg-amber-50 text-amber-700 border-amber-200',
|
|
380
|
+
date: 'bg-green-50 text-green-700 border-green-200',
|
|
381
|
+
boolean: 'bg-purple-50 text-purple-700 border-purple-200',
|
|
382
|
+
select: 'bg-cyan-50 text-cyan-700 border-cyan-200',
|
|
383
|
+
relation: 'bg-indigo-50 text-indigo-700 border-indigo-200',
|
|
384
|
+
}
|