@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.
@@ -0,0 +1,280 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useReducer,
7
+ } from 'react'
8
+ import { getFieldTypeDefinition } from '../constants/field-types'
9
+ import {
10
+ DEFAULT_BUILDER_STATE,
11
+ FIELD_TYPE_TO_TS_TYPE,
12
+ type SchemaBuilderAction,
13
+ type SchemaBuilderContextType,
14
+ type SchemaBuilderState,
15
+ type SchemaConfig,
16
+ type SchemaField,
17
+ type ViewMode,
18
+ } from '../types/builder.types'
19
+ import { generateSchemaCode, generateSchemaJSON } from '../utils/code-generator'
20
+
21
+ /**
22
+ * Generate a unique ID for fields
23
+ */
24
+ function generateId(): string {
25
+ return `field_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
26
+ }
27
+
28
+ /**
29
+ * Convert display name to API ID (camelCase)
30
+ */
31
+ function toApiId(displayName: string): string {
32
+ return displayName
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9\s]/g, '')
35
+ .split(/\s+/)
36
+ .filter(Boolean)
37
+ .map((word, index) =>
38
+ index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),
39
+ )
40
+ .join('')
41
+ }
42
+
43
+ /**
44
+ * Schema builder reducer
45
+ */
46
+ function schemaBuilderReducer(
47
+ state: SchemaBuilderState,
48
+ action: SchemaBuilderAction,
49
+ ): SchemaBuilderState {
50
+ switch (action.type) {
51
+ case 'SET_SCHEMA':
52
+ return {
53
+ ...state,
54
+ schema: { ...state.schema, ...action.payload },
55
+ isDirty: true,
56
+ }
57
+
58
+ case 'ADD_FIELD':
59
+ return {
60
+ ...state,
61
+ fields: [...state.fields, action.payload],
62
+ selectedFieldId: action.payload.id,
63
+ isDirty: true,
64
+ }
65
+
66
+ case 'UPDATE_FIELD':
67
+ return {
68
+ ...state,
69
+ fields: state.fields.map((field) =>
70
+ field.id === action.payload.id
71
+ ? { ...field, ...action.payload.changes }
72
+ : field,
73
+ ),
74
+ isDirty: true,
75
+ }
76
+
77
+ case 'DELETE_FIELD':
78
+ return {
79
+ ...state,
80
+ fields: state.fields.filter((field) => field.id !== action.payload),
81
+ selectedFieldId:
82
+ state.selectedFieldId === action.payload
83
+ ? null
84
+ : state.selectedFieldId,
85
+ isDirty: true,
86
+ }
87
+
88
+ case 'REORDER_FIELDS':
89
+ return {
90
+ ...state,
91
+ fields: action.payload,
92
+ isDirty: true,
93
+ }
94
+
95
+ case 'SELECT_FIELD':
96
+ return {
97
+ ...state,
98
+ selectedFieldId: action.payload,
99
+ }
100
+
101
+ case 'SET_VIEW_MODE':
102
+ return {
103
+ ...state,
104
+ viewMode: action.payload,
105
+ }
106
+
107
+ case 'MARK_DIRTY':
108
+ return {
109
+ ...state,
110
+ isDirty: true,
111
+ }
112
+
113
+ case 'MARK_SAVED':
114
+ return {
115
+ ...state,
116
+ isDirty: false,
117
+ lastSaved: new Date(),
118
+ }
119
+
120
+ case 'RESET':
121
+ return action.payload
122
+
123
+ case 'LOAD_SCHEMA':
124
+ return {
125
+ ...state,
126
+ schema: action.payload.schema,
127
+ fields: action.payload.fields,
128
+ isDirty: false,
129
+ isNew: false,
130
+ }
131
+
132
+ default:
133
+ return state
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Schema builder context
139
+ */
140
+ export const SchemaBuilderContext =
141
+ createContext<SchemaBuilderContextType | null>(null)
142
+
143
+ /**
144
+ * Hook to access the schema builder context
145
+ */
146
+ export function useSchemaBuilder(): SchemaBuilderContextType {
147
+ const context = useContext(SchemaBuilderContext)
148
+ if (!context) {
149
+ throw new Error(
150
+ 'useSchemaBuilder must be used within a SchemaBuilderProvider',
151
+ )
152
+ }
153
+ return context
154
+ }
155
+
156
+ /**
157
+ * Hook to create the schema builder state and actions
158
+ */
159
+ export function useSchemaBuilderState(
160
+ initialState: SchemaBuilderState = DEFAULT_BUILDER_STATE,
161
+ ): SchemaBuilderContextType {
162
+ const [state, dispatch] = useReducer(schemaBuilderReducer, initialState)
163
+
164
+ // Convenience actions
165
+ const addField = useCallback((partial: Partial<SchemaField>) => {
166
+ const fieldType = partial.type || 'text'
167
+ const definition = getFieldTypeDefinition(fieldType)
168
+ const displayName = partial.displayName || 'New Field'
169
+ const name = partial.name || toApiId(displayName)
170
+
171
+ const field: SchemaField = {
172
+ id: generateId(),
173
+ name,
174
+ displayName,
175
+ type: fieldType,
176
+ tsType:
177
+ definition?.tsType || FIELD_TYPE_TO_TS_TYPE[fieldType] || 'string',
178
+ prop: {
179
+ required: false,
180
+ unique: false,
181
+ ...partial.prop,
182
+ },
183
+ ui: {
184
+ tab: 'General',
185
+ ...definition?.defaultUI,
186
+ ...partial.ui,
187
+ },
188
+ validations: partial.validations || definition?.defaultValidations || [],
189
+ relationConfig: partial.relationConfig,
190
+ }
191
+
192
+ dispatch({ type: 'ADD_FIELD', payload: field })
193
+ }, [])
194
+
195
+ const updateField = useCallback(
196
+ (id: string, changes: Partial<SchemaField>) => {
197
+ dispatch({ type: 'UPDATE_FIELD', payload: { id, changes } })
198
+ },
199
+ [],
200
+ )
201
+
202
+ const deleteField = useCallback((id: string) => {
203
+ dispatch({ type: 'DELETE_FIELD', payload: id })
204
+ }, [])
205
+
206
+ const selectField = useCallback((id: string | null) => {
207
+ dispatch({ type: 'SELECT_FIELD', payload: id })
208
+ }, [])
209
+
210
+ const reorderFields = useCallback((fields: SchemaField[]) => {
211
+ dispatch({ type: 'REORDER_FIELDS', payload: fields })
212
+ }, [])
213
+
214
+ const setViewMode = useCallback((mode: ViewMode) => {
215
+ dispatch({ type: 'SET_VIEW_MODE', payload: mode })
216
+ }, [])
217
+
218
+ const updateSchema = useCallback((changes: Partial<SchemaConfig>) => {
219
+ dispatch({ type: 'SET_SCHEMA', payload: changes })
220
+ }, [])
221
+
222
+ const resetState = useCallback((newState?: SchemaBuilderState) => {
223
+ dispatch({ type: 'RESET', payload: newState || DEFAULT_BUILDER_STATE })
224
+ }, [])
225
+
226
+ // Computed values
227
+ const selectedField = useMemo(
228
+ () => state.fields.find((f) => f.id === state.selectedFieldId) ?? null,
229
+ [state.fields, state.selectedFieldId],
230
+ )
231
+
232
+ const generatedCode = useMemo(() => generateSchemaCode(state), [state])
233
+
234
+ const generatedJSON = useMemo(() => generateSchemaJSON(state), [state])
235
+
236
+ return {
237
+ state,
238
+ dispatch,
239
+ addField,
240
+ updateField,
241
+ deleteField,
242
+ selectField,
243
+ reorderFields,
244
+ setViewMode,
245
+ updateSchema,
246
+ resetState,
247
+ selectedField,
248
+ generatedCode,
249
+ generatedJSON,
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create a field from a field type definition
255
+ */
256
+ export function createFieldFromType(
257
+ type: SchemaField['type'],
258
+ displayName: string,
259
+ ): Partial<SchemaField> {
260
+ const definition = getFieldTypeDefinition(type)
261
+ if (!definition) {
262
+ return { type, displayName }
263
+ }
264
+
265
+ return {
266
+ type,
267
+ displayName,
268
+ name: toApiId(displayName),
269
+ tsType: definition.tsType,
270
+ validations: [...definition.defaultValidations],
271
+ ui: { ...definition.defaultUI },
272
+ prop: { required: false, unique: false },
273
+ ...(definition.hasRelationConfig && {
274
+ relationConfig: {
275
+ targetSchema: '',
276
+ relationType: 'manyToOne',
277
+ },
278
+ }),
279
+ }
280
+ }
@@ -0,0 +1,19 @@
1
+ import { useEffect } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+
4
+ /**
5
+ * Playground index - redirects to the editor with 3-column layout
6
+ * The schema list is now integrated into the editor sidebar
7
+ */
8
+ const Playground = () => {
9
+ const navigate = useNavigate()
10
+
11
+ useEffect(() => {
12
+ // Redirect to new schema editor - the schema list is shown in the sidebar
13
+ navigate('/playground/new', { replace: true })
14
+ }, [navigate])
15
+
16
+ return null
17
+ }
18
+
19
+ export default Playground
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Schema Playground Builder Types
3
+ *
4
+ * These types define the state structure for the visual schema builder.
5
+ */
6
+
7
+ /**
8
+ * Supported field types in the schema builder
9
+ */
10
+ export type FieldType =
11
+ | 'text'
12
+ | 'number'
13
+ | 'date'
14
+ | 'boolean'
15
+ | 'select'
16
+ | 'relation'
17
+
18
+ /**
19
+ * Select/dropdown option structure
20
+ */
21
+ export interface SelectOption {
22
+ key: string
23
+ value: string
24
+ }
25
+
26
+ /**
27
+ * Relation field configuration
28
+ */
29
+ export interface RelationConfig {
30
+ targetSchema: string
31
+ relationType: 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany'
32
+ inverseSide?: string
33
+ }
34
+
35
+ /**
36
+ * Validation rule structure (maps to class-validator decorators)
37
+ */
38
+ export interface ValidationRule {
39
+ type: string // e.g., 'IsString', 'IsNotEmpty', 'Length', 'Min', 'Max'
40
+ constraints?: (string | number)[] // e.g., [10, 255] for Length
41
+ message?: string // Custom error message
42
+ }
43
+
44
+ /**
45
+ * UI configuration (maps to @UI() decorator options)
46
+ */
47
+ export interface FieldUIConfig {
48
+ type?: string // UI type override (e.g., 'switch' for boolean)
49
+ label?: string // Display label
50
+ description?: string // Help text
51
+ placeholder?: string // Input placeholder
52
+ tab?: string // Tab name for grouping
53
+ side?: boolean // Show in side panel
54
+ row?: boolean // Half-width layout
55
+ options?: SelectOption[] // For select/radio fields
56
+ }
57
+
58
+ /**
59
+ * Property configuration (maps to @Prop() decorator options)
60
+ */
61
+ export interface FieldPropConfig {
62
+ required?: boolean
63
+ unique?: boolean
64
+ default?: unknown
65
+ intl?: boolean // Enable i18n for this field
66
+ hidden?: boolean // Hide from UI
67
+ readonly?: boolean
68
+ }
69
+
70
+ /**
71
+ * Complete field definition in the builder
72
+ */
73
+ export interface SchemaField {
74
+ id: string // Unique ID for React keys and drag-drop
75
+ name: string // API ID / property name (e.g., 'firstName')
76
+ displayName: string // Human-readable label (e.g., 'First Name')
77
+ type: FieldType // Field type
78
+ tsType: string // TypeScript type (string, number, Date, boolean, etc.)
79
+ prop: FieldPropConfig // @Prop() options
80
+ ui: FieldUIConfig // @UI() options
81
+ validations: ValidationRule[] // @Validators() rules
82
+ relationConfig?: RelationConfig // For relation fields
83
+ }
84
+
85
+ /**
86
+ * Schema-level configuration (maps to @Schema() decorator options)
87
+ */
88
+ export interface SchemaConfig {
89
+ name: string // Class name (e.g., 'Cat')
90
+ apiId?: string // API identifier (e.g., 'cat')
91
+ versioning: boolean // Enable version history
92
+ i18n: boolean // Enable internationalization
93
+ description?: string // Schema description
94
+ }
95
+
96
+ /**
97
+ * View modes for the editor
98
+ */
99
+ export type ViewMode = 'builder' | 'json' | 'code'
100
+
101
+ /**
102
+ * Complete builder state
103
+ */
104
+ export interface SchemaBuilderState {
105
+ schema: SchemaConfig
106
+ fields: SchemaField[]
107
+ selectedFieldId: string | null
108
+ viewMode: ViewMode
109
+ isDirty: boolean
110
+ lastSaved: Date | null
111
+ isNew: boolean // Whether this is a new schema or editing existing
112
+ }
113
+
114
+ /**
115
+ * Reducer action types
116
+ */
117
+ export type SchemaBuilderAction =
118
+ | { type: 'SET_SCHEMA'; payload: Partial<SchemaConfig> }
119
+ | { type: 'ADD_FIELD'; payload: SchemaField }
120
+ | {
121
+ type: 'UPDATE_FIELD'
122
+ payload: { id: string; changes: Partial<SchemaField> }
123
+ }
124
+ | { type: 'DELETE_FIELD'; payload: string }
125
+ | { type: 'REORDER_FIELDS'; payload: SchemaField[] }
126
+ | { type: 'SELECT_FIELD'; payload: string | null }
127
+ | { type: 'SET_VIEW_MODE'; payload: ViewMode }
128
+ | { type: 'MARK_DIRTY' }
129
+ | { type: 'MARK_SAVED' }
130
+ | { type: 'RESET'; payload: SchemaBuilderState }
131
+ | {
132
+ type: 'LOAD_SCHEMA'
133
+ payload: { schema: SchemaConfig; fields: SchemaField[] }
134
+ }
135
+
136
+ /**
137
+ * Default values for a new schema
138
+ */
139
+ export const DEFAULT_SCHEMA_CONFIG: SchemaConfig = {
140
+ name: '',
141
+ versioning: true,
142
+ i18n: true,
143
+ }
144
+
145
+ /**
146
+ * Default builder state
147
+ */
148
+ export const DEFAULT_BUILDER_STATE: SchemaBuilderState = {
149
+ schema: DEFAULT_SCHEMA_CONFIG,
150
+ fields: [],
151
+ selectedFieldId: null,
152
+ viewMode: 'builder',
153
+ isDirty: false,
154
+ lastSaved: null,
155
+ isNew: true,
156
+ }
157
+
158
+ /**
159
+ * Mapping from field type to TypeScript type
160
+ */
161
+ export const FIELD_TYPE_TO_TS_TYPE: Record<FieldType, string> = {
162
+ text: 'string',
163
+ number: 'number',
164
+ date: 'Date',
165
+ boolean: 'boolean',
166
+ select: 'string',
167
+ relation: 'string', // ObjectId reference
168
+ }
169
+
170
+ /**
171
+ * Context type for the schema builder
172
+ */
173
+ export interface SchemaBuilderContextType {
174
+ state: SchemaBuilderState
175
+ dispatch: React.Dispatch<SchemaBuilderAction>
176
+
177
+ // Convenience actions
178
+ addField: (field: Partial<SchemaField>) => void
179
+ updateField: (id: string, changes: Partial<SchemaField>) => void
180
+ deleteField: (id: string) => void
181
+ selectField: (id: string | null) => void
182
+ reorderFields: (fields: SchemaField[]) => void
183
+ setViewMode: (mode: ViewMode) => void
184
+ updateSchema: (changes: Partial<SchemaConfig>) => void
185
+ resetState: (state?: SchemaBuilderState) => void
186
+
187
+ // Computed values
188
+ selectedField: SchemaField | null
189
+ generatedCode: string
190
+ generatedJSON: object
191
+ }