cmssy-cli 0.20.1 → 0.24.1
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/config.d.ts +1 -1
- package/dist/cli.js +136 -23
- package/dist/cli.js.map +1 -1
- package/dist/commands/add-source.d.ts +7 -0
- package/dist/commands/add-source.d.ts.map +1 -0
- package/dist/commands/add-source.js +238 -0
- package/dist/commands/add-source.js.map +1 -0
- package/dist/commands/build.d.ts +1 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +56 -12
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +22 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +652 -410
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +3 -1
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/publish.js +74 -0
- package/dist/commands/publish.js.map +1 -1
- package/dist/dev-ui/app.js +166 -19
- package/dist/dev-ui/index.html +138 -0
- package/dist/dev-ui-react/App.tsx +164 -0
- package/dist/dev-ui-react/__tests__/previewData.test.ts +193 -0
- package/dist/dev-ui-react/components/BlocksList.tsx +232 -0
- package/dist/dev-ui-react/components/Editor.tsx +469 -0
- package/dist/dev-ui-react/components/Preview.tsx +146 -0
- package/dist/dev-ui-react/hooks/useBlocks.ts +80 -0
- package/dist/dev-ui-react/index.html +13 -0
- package/dist/dev-ui-react/main.tsx +8 -0
- package/dist/dev-ui-react/styles.css +856 -0
- package/dist/dev-ui-react/types.ts +45 -0
- package/dist/types/block-config.d.ts +100 -2
- package/dist/types/block-config.d.ts.map +1 -1
- package/dist/types/block-config.js +6 -1
- package/dist/types/block-config.js.map +1 -1
- package/dist/utils/block-config.js +3 -3
- package/dist/utils/block-config.js.map +1 -1
- package/dist/utils/blocks-meta-cache.d.ts +28 -0
- package/dist/utils/blocks-meta-cache.d.ts.map +1 -0
- package/dist/utils/blocks-meta-cache.js +72 -0
- package/dist/utils/blocks-meta-cache.js.map +1 -0
- package/dist/utils/builder.d.ts +3 -0
- package/dist/utils/builder.d.ts.map +1 -1
- package/dist/utils/builder.js +17 -14
- package/dist/utils/builder.js.map +1 -1
- package/dist/utils/field-schema.d.ts +2 -0
- package/dist/utils/field-schema.d.ts.map +1 -1
- package/dist/utils/field-schema.js +21 -4
- package/dist/utils/field-schema.js.map +1 -1
- package/dist/utils/graphql.d.ts +2 -0
- package/dist/utils/graphql.d.ts.map +1 -1
- package/dist/utils/graphql.js +22 -0
- package/dist/utils/graphql.js.map +1 -1
- package/dist/utils/scanner.d.ts +5 -3
- package/dist/utils/scanner.d.ts.map +1 -1
- package/dist/utils/scanner.js +23 -16
- package/dist/utils/scanner.js.map +1 -1
- package/dist/utils/type-generator.d.ts +7 -1
- package/dist/utils/type-generator.d.ts.map +1 -1
- package/dist/utils/type-generator.js +58 -41
- package/dist/utils/type-generator.js.map +1 -1
- package/package.json +8 -3
- package/dist/commands/deploy.d.ts +0 -9
- package/dist/commands/deploy.d.ts.map +0 -1
- package/dist/commands/deploy.js +0 -226
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/push.d.ts +0 -9
- package/dist/commands/push.d.ts.map +0 -1
- package/dist/commands/push.js +0 -199
- package/dist/commands/push.js.map +0 -1
- package/dist/utils/blockforge-config.d.ts +0 -19
- package/dist/utils/blockforge-config.d.ts.map +0 -1
- package/dist/utils/blockforge-config.js +0 -19
- package/dist/utils/blockforge-config.js.map +0 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Block, FieldConfig } from '../types';
|
|
3
|
+
|
|
4
|
+
interface EditorProps {
|
|
5
|
+
block: Block | null;
|
|
6
|
+
loading: boolean;
|
|
7
|
+
previewData: Record<string, unknown>;
|
|
8
|
+
onPreviewDataChange: (data: Record<string, unknown>) => void;
|
|
9
|
+
onNavigateToPage?: (pageSlug: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Editor({
|
|
13
|
+
block,
|
|
14
|
+
loading,
|
|
15
|
+
previewData,
|
|
16
|
+
onPreviewDataChange,
|
|
17
|
+
onNavigateToPage,
|
|
18
|
+
}: EditorProps) {
|
|
19
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
20
|
+
|
|
21
|
+
if (collapsed) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="editor-panel collapsed">
|
|
24
|
+
<div className="editor-header">
|
|
25
|
+
<button className="panel-toggle" onClick={() => setCollapsed(false)} title="Expand">
|
|
26
|
+
<span className="toggle-icon">☰</span>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isTemplate = block?.type === 'template' && block.pages && block.pages.length > 0;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="editor-panel">
|
|
37
|
+
<div className="editor-header">
|
|
38
|
+
<button className="panel-toggle" onClick={() => setCollapsed(true)} title="Collapse">
|
|
39
|
+
<span className="toggle-icon">☰</span>
|
|
40
|
+
</button>
|
|
41
|
+
<div className="editor-header-content">
|
|
42
|
+
<h2>Properties</h2>
|
|
43
|
+
<p>{block?.name || 'No block selected'}</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="editor-content">
|
|
47
|
+
{loading ? (
|
|
48
|
+
<div className="loading">
|
|
49
|
+
<div className="spinner" />
|
|
50
|
+
<span>Loading properties...</span>
|
|
51
|
+
</div>
|
|
52
|
+
) : !block ? (
|
|
53
|
+
<div className="editor-empty">Select a block to edit its properties</div>
|
|
54
|
+
) : isTemplate ? (
|
|
55
|
+
<TemplateEditor block={block} onNavigateToPage={onNavigateToPage} />
|
|
56
|
+
) : block.schema ? (
|
|
57
|
+
<SchemaEditor
|
|
58
|
+
schema={block.schema}
|
|
59
|
+
data={previewData}
|
|
60
|
+
onChange={onPreviewDataChange}
|
|
61
|
+
/>
|
|
62
|
+
) : (
|
|
63
|
+
<div className="editor-empty">No schema defined for this block</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Template editor component
|
|
71
|
+
function TemplateEditor({
|
|
72
|
+
block,
|
|
73
|
+
onNavigateToPage,
|
|
74
|
+
}: {
|
|
75
|
+
block: Block;
|
|
76
|
+
onNavigateToPage?: (pageSlug: string) => void;
|
|
77
|
+
}) {
|
|
78
|
+
const pages = block.pages || [];
|
|
79
|
+
const layoutSlots = block.layoutSlots || [];
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="template-editor">
|
|
83
|
+
<div className="template-info">
|
|
84
|
+
<div className="template-info-badge">Template</div>
|
|
85
|
+
<p className="template-info-desc">{block.description || 'No description'}</p>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="template-section">
|
|
89
|
+
<h4 className="template-section-title">Pages ({pages.length})</h4>
|
|
90
|
+
<div className="template-pages-list">
|
|
91
|
+
{pages.length === 0 ? (
|
|
92
|
+
<div className="editor-empty">No pages defined</div>
|
|
93
|
+
) : (
|
|
94
|
+
pages.map((page) => (
|
|
95
|
+
<div
|
|
96
|
+
key={page.slug}
|
|
97
|
+
className="template-page-item"
|
|
98
|
+
onClick={() => onNavigateToPage?.(page.slug)}
|
|
99
|
+
>
|
|
100
|
+
<div className="template-page-header">
|
|
101
|
+
<span className="template-page-name">{page.name}</span>
|
|
102
|
+
<span className="template-page-blocks">{page.blocksCount} blocks</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="template-page-slug">/{page.slug}</div>
|
|
105
|
+
</div>
|
|
106
|
+
))
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{layoutSlots.length > 0 && (
|
|
112
|
+
<div className="template-section">
|
|
113
|
+
<h4 className="template-section-title">Layout Slots</h4>
|
|
114
|
+
{layoutSlots.map((slot) => (
|
|
115
|
+
<div key={slot.slot} className="template-layout-slot">
|
|
116
|
+
<span className="slot-type">{slot.slot}</span>
|
|
117
|
+
<span className="slot-block">{slot.type}</span>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<div className="template-hint">
|
|
124
|
+
<p>Click on a page to preview it, or use the tabs in the preview header.</p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Schema-based field editor
|
|
131
|
+
function SchemaEditor({
|
|
132
|
+
schema,
|
|
133
|
+
data,
|
|
134
|
+
onChange,
|
|
135
|
+
prefix = '',
|
|
136
|
+
}: {
|
|
137
|
+
schema: Record<string, FieldConfig>;
|
|
138
|
+
data: Record<string, unknown>;
|
|
139
|
+
onChange: (data: Record<string, unknown>) => void;
|
|
140
|
+
prefix?: string;
|
|
141
|
+
}) {
|
|
142
|
+
const updateField = (key: string, value: unknown) => {
|
|
143
|
+
const newData = { ...data, [key]: value };
|
|
144
|
+
onChange(newData);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
{Object.entries(schema).map(([key, field]) => (
|
|
150
|
+
<FieldEditor
|
|
151
|
+
key={prefix + key}
|
|
152
|
+
fieldKey={key}
|
|
153
|
+
field={field}
|
|
154
|
+
value={data[key]}
|
|
155
|
+
onChange={(value) => updateField(key, value)}
|
|
156
|
+
/>
|
|
157
|
+
))}
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Individual field editor
|
|
163
|
+
function FieldEditor({
|
|
164
|
+
fieldKey,
|
|
165
|
+
field,
|
|
166
|
+
value,
|
|
167
|
+
onChange,
|
|
168
|
+
}: {
|
|
169
|
+
fieldKey: string;
|
|
170
|
+
field: FieldConfig;
|
|
171
|
+
value: unknown;
|
|
172
|
+
onChange: (value: unknown) => void;
|
|
173
|
+
}) {
|
|
174
|
+
const required = field.required ? <span className="field-required">*</span> : null;
|
|
175
|
+
|
|
176
|
+
// Boolean field
|
|
177
|
+
if (field.type === 'boolean') {
|
|
178
|
+
return (
|
|
179
|
+
<div className="field-group">
|
|
180
|
+
<label className="field-checkbox-label">
|
|
181
|
+
<input
|
|
182
|
+
type="checkbox"
|
|
183
|
+
checked={Boolean(value ?? field.defaultValue)}
|
|
184
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
185
|
+
/>
|
|
186
|
+
<span>{field.label}</span>
|
|
187
|
+
</label>
|
|
188
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Text fields
|
|
194
|
+
if (field.type === 'singleLine' || field.type === 'text' || field.type === 'string') {
|
|
195
|
+
return (
|
|
196
|
+
<div className="field-group">
|
|
197
|
+
<label className="field-label">
|
|
198
|
+
{field.label}
|
|
199
|
+
{required}
|
|
200
|
+
</label>
|
|
201
|
+
<input
|
|
202
|
+
type="text"
|
|
203
|
+
className="field-input"
|
|
204
|
+
value={String(value ?? field.defaultValue ?? '')}
|
|
205
|
+
placeholder={field.placeholder}
|
|
206
|
+
onChange={(e) => onChange(e.target.value)}
|
|
207
|
+
/>
|
|
208
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Multiline text
|
|
214
|
+
if (field.type === 'multiLine' || field.type === 'richText') {
|
|
215
|
+
return (
|
|
216
|
+
<div className="field-group">
|
|
217
|
+
<label className="field-label">
|
|
218
|
+
{field.label}
|
|
219
|
+
{required}
|
|
220
|
+
</label>
|
|
221
|
+
<textarea
|
|
222
|
+
className="field-input field-textarea"
|
|
223
|
+
value={String(value ?? field.defaultValue ?? '')}
|
|
224
|
+
placeholder={field.placeholder}
|
|
225
|
+
onChange={(e) => onChange(e.target.value)}
|
|
226
|
+
/>
|
|
227
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Number field
|
|
233
|
+
if (field.type === 'number' || field.type === 'numeric') {
|
|
234
|
+
return (
|
|
235
|
+
<div className="field-group">
|
|
236
|
+
<label className="field-label">
|
|
237
|
+
{field.label}
|
|
238
|
+
{required}
|
|
239
|
+
</label>
|
|
240
|
+
<input
|
|
241
|
+
type="number"
|
|
242
|
+
className="field-input"
|
|
243
|
+
value={value !== undefined ? String(value) : String(field.defaultValue ?? '')}
|
|
244
|
+
placeholder={field.placeholder}
|
|
245
|
+
onChange={(e) => onChange(e.target.value ? parseFloat(e.target.value) : '')}
|
|
246
|
+
/>
|
|
247
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Date field
|
|
253
|
+
if (field.type === 'date') {
|
|
254
|
+
return (
|
|
255
|
+
<div className="field-group">
|
|
256
|
+
<label className="field-label">
|
|
257
|
+
{field.label}
|
|
258
|
+
{required}
|
|
259
|
+
</label>
|
|
260
|
+
<input
|
|
261
|
+
type="date"
|
|
262
|
+
className="field-input"
|
|
263
|
+
value={String(value ?? field.defaultValue ?? '')}
|
|
264
|
+
onChange={(e) => onChange(e.target.value)}
|
|
265
|
+
/>
|
|
266
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Link field
|
|
272
|
+
if (field.type === 'link') {
|
|
273
|
+
const linkValue = (value as { url?: string; text?: string; target?: string }) ?? {};
|
|
274
|
+
return (
|
|
275
|
+
<div className="field-group">
|
|
276
|
+
<label className="field-label">
|
|
277
|
+
{field.label}
|
|
278
|
+
{required}
|
|
279
|
+
</label>
|
|
280
|
+
<div className="link-field">
|
|
281
|
+
<input
|
|
282
|
+
type="url"
|
|
283
|
+
className="field-input"
|
|
284
|
+
placeholder="URL"
|
|
285
|
+
value={linkValue.url || ''}
|
|
286
|
+
onChange={(e) => onChange({ ...linkValue, url: e.target.value })}
|
|
287
|
+
style={{ marginBottom: 8 }}
|
|
288
|
+
/>
|
|
289
|
+
<input
|
|
290
|
+
type="text"
|
|
291
|
+
className="field-input"
|
|
292
|
+
placeholder="Link text"
|
|
293
|
+
value={linkValue.text || ''}
|
|
294
|
+
onChange={(e) => onChange({ ...linkValue, text: e.target.value })}
|
|
295
|
+
style={{ marginBottom: 8 }}
|
|
296
|
+
/>
|
|
297
|
+
<label className="field-checkbox-label">
|
|
298
|
+
<input
|
|
299
|
+
type="checkbox"
|
|
300
|
+
checked={linkValue.target === '_blank'}
|
|
301
|
+
onChange={(e) => onChange({ ...linkValue, target: e.target.checked ? '_blank' : '_self' })}
|
|
302
|
+
/>
|
|
303
|
+
<span>Open in new tab</span>
|
|
304
|
+
</label>
|
|
305
|
+
</div>
|
|
306
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Select field
|
|
312
|
+
if (field.type === 'select') {
|
|
313
|
+
return (
|
|
314
|
+
<div className="field-group">
|
|
315
|
+
<label className="field-label">
|
|
316
|
+
{field.label}
|
|
317
|
+
{required}
|
|
318
|
+
</label>
|
|
319
|
+
<select
|
|
320
|
+
className="field-input field-select"
|
|
321
|
+
value={String(value ?? field.defaultValue ?? '')}
|
|
322
|
+
onChange={(e) => onChange(e.target.value)}
|
|
323
|
+
>
|
|
324
|
+
<option value="">Select an option...</option>
|
|
325
|
+
{field.options?.map((opt) => (
|
|
326
|
+
<option key={opt.value} value={opt.value}>
|
|
327
|
+
{opt.label}
|
|
328
|
+
</option>
|
|
329
|
+
))}
|
|
330
|
+
</select>
|
|
331
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Color field
|
|
337
|
+
if (field.type === 'color') {
|
|
338
|
+
const colorValue = String(value ?? field.defaultValue ?? '#000000');
|
|
339
|
+
return (
|
|
340
|
+
<div className="field-group">
|
|
341
|
+
<label className="field-label">
|
|
342
|
+
{field.label}
|
|
343
|
+
{required}
|
|
344
|
+
</label>
|
|
345
|
+
<div className="color-field">
|
|
346
|
+
<input
|
|
347
|
+
type="color"
|
|
348
|
+
className="color-preview"
|
|
349
|
+
value={colorValue}
|
|
350
|
+
onChange={(e) => onChange(e.target.value)}
|
|
351
|
+
/>
|
|
352
|
+
<input
|
|
353
|
+
type="text"
|
|
354
|
+
className="field-input color-input"
|
|
355
|
+
value={colorValue}
|
|
356
|
+
onChange={(e) => onChange(e.target.value)}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Media field (simple URL string)
|
|
365
|
+
if (field.type === 'media') {
|
|
366
|
+
const mediaUrl = (value as string) || '';
|
|
367
|
+
return (
|
|
368
|
+
<div className="field-group">
|
|
369
|
+
<label className="field-label">
|
|
370
|
+
{field.label}
|
|
371
|
+
{required}
|
|
372
|
+
</label>
|
|
373
|
+
<div className="media-field">
|
|
374
|
+
<div className="media-preview">
|
|
375
|
+
{mediaUrl ? (
|
|
376
|
+
<img src={mediaUrl} alt={field.label} />
|
|
377
|
+
) : (
|
|
378
|
+
<div className="media-placeholder">No image</div>
|
|
379
|
+
)}
|
|
380
|
+
</div>
|
|
381
|
+
<input
|
|
382
|
+
type="url"
|
|
383
|
+
className="field-input"
|
|
384
|
+
placeholder="Image URL"
|
|
385
|
+
value={mediaUrl}
|
|
386
|
+
onChange={(e) => onChange(e.target.value)}
|
|
387
|
+
/>
|
|
388
|
+
</div>
|
|
389
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Repeater field
|
|
395
|
+
if (field.type === 'repeater' && field.schema) {
|
|
396
|
+
const items = (value as Record<string, unknown>[]) ?? [];
|
|
397
|
+
const minItems = field.minItems ?? 0;
|
|
398
|
+
const maxItems = field.maxItems ?? 999;
|
|
399
|
+
|
|
400
|
+
const addItem = () => {
|
|
401
|
+
const newItem: Record<string, unknown> = {};
|
|
402
|
+
Object.entries(field.schema!).forEach(([k, f]) => {
|
|
403
|
+
if (f.defaultValue !== undefined) newItem[k] = f.defaultValue;
|
|
404
|
+
});
|
|
405
|
+
onChange([...items, newItem]);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const removeItem = (index: number) => {
|
|
409
|
+
onChange(items.filter((_, i) => i !== index));
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const updateItem = (index: number, newItemData: Record<string, unknown>) => {
|
|
413
|
+
const newItems = [...items];
|
|
414
|
+
newItems[index] = newItemData;
|
|
415
|
+
onChange(newItems);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<div className="field-group">
|
|
420
|
+
<label className="field-label">
|
|
421
|
+
{field.label}
|
|
422
|
+
{required}
|
|
423
|
+
</label>
|
|
424
|
+
<div className="repeater-items">
|
|
425
|
+
{items.length === 0 ? (
|
|
426
|
+
<div style={{ padding: 12, color: '#999', textAlign: 'center' }}>No items yet</div>
|
|
427
|
+
) : (
|
|
428
|
+
items.map((item, index) => (
|
|
429
|
+
<div key={index} className="repeater-item">
|
|
430
|
+
<div className="repeater-item-header">
|
|
431
|
+
<div className="repeater-item-title">Item {index + 1}</div>
|
|
432
|
+
{items.length > minItems && (
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
className="repeater-item-remove"
|
|
436
|
+
onClick={() => removeItem(index)}
|
|
437
|
+
>
|
|
438
|
+
Remove
|
|
439
|
+
</button>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
<SchemaEditor
|
|
443
|
+
schema={field.schema!}
|
|
444
|
+
data={item}
|
|
445
|
+
onChange={(newData) => updateItem(index, newData)}
|
|
446
|
+
prefix={`${fieldKey}.${index}.`}
|
|
447
|
+
/>
|
|
448
|
+
</div>
|
|
449
|
+
))
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
{items.length < maxItems && (
|
|
453
|
+
<button type="button" className="repeater-add" onClick={addItem}>
|
|
454
|
+
+ Add Item
|
|
455
|
+
</button>
|
|
456
|
+
)}
|
|
457
|
+
{field.helpText && <div className="field-help">{field.helpText}</div>}
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Fallback for unsupported types
|
|
463
|
+
return (
|
|
464
|
+
<div className="field-group">
|
|
465
|
+
<label className="field-label">{field.label}</label>
|
|
466
|
+
<div style={{ color: '#999', fontSize: 12 }}>Unsupported field type: {field.type}</div>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useRef, useEffect, memo } from 'react';
|
|
2
|
+
import { Block } from '../types';
|
|
3
|
+
|
|
4
|
+
interface PreviewProps {
|
|
5
|
+
block: Block | null;
|
|
6
|
+
previewData: Record<string, unknown>;
|
|
7
|
+
currentPage?: string;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Preview = memo(function Preview({ block, previewData, currentPage, loading }: PreviewProps) {
|
|
12
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
13
|
+
const iframeLoadedRef = useRef(false);
|
|
14
|
+
const lastSentDataRef = useRef<string>('');
|
|
15
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
16
|
+
|
|
17
|
+
// Send props to iframe with debounce to prevent flickering
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!iframeRef.current?.contentWindow || !iframeLoadedRef.current) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Don't send if previewData is empty (initial state)
|
|
24
|
+
if (!previewData || Object.keys(previewData).length === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Don't send if data hasn't changed
|
|
29
|
+
const dataString = JSON.stringify(previewData);
|
|
30
|
+
if (dataString === lastSentDataRef.current) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Clear previous debounce
|
|
35
|
+
if (debounceRef.current) {
|
|
36
|
+
clearTimeout(debounceRef.current);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Debounce postMessage to reduce flickering
|
|
40
|
+
debounceRef.current = setTimeout(() => {
|
|
41
|
+
if (iframeRef.current?.contentWindow) {
|
|
42
|
+
lastSentDataRef.current = dataString;
|
|
43
|
+
iframeRef.current.contentWindow.postMessage(
|
|
44
|
+
{ type: 'UPDATE_PROPS', props: previewData },
|
|
45
|
+
'*'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}, 150);
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
if (debounceRef.current) {
|
|
52
|
+
clearTimeout(debounceRef.current);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}, [previewData]);
|
|
56
|
+
|
|
57
|
+
// Handle iframe load - mark as loaded but don't send data
|
|
58
|
+
// The iframe already has correct data from the server
|
|
59
|
+
const handleIframeLoad = () => {
|
|
60
|
+
iframeLoadedRef.current = true;
|
|
61
|
+
// Store current data as "sent" so we don't re-send it
|
|
62
|
+
if (previewData && Object.keys(previewData).length > 0) {
|
|
63
|
+
lastSentDataRef.current = JSON.stringify(previewData);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Reset loaded state when URL changes
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
iframeLoadedRef.current = false;
|
|
70
|
+
lastSentDataRef.current = '';
|
|
71
|
+
}, [block?.name, currentPage]);
|
|
72
|
+
|
|
73
|
+
if (!block) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="preview-panel">
|
|
76
|
+
<div className="preview-header">
|
|
77
|
+
<div className="preview-info">
|
|
78
|
+
<div className="preview-title">Preview</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="preview-content">
|
|
82
|
+
<div className="preview-empty">
|
|
83
|
+
<div className="preview-empty-icon">👈</div>
|
|
84
|
+
<p>Select a block to preview</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (loading) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="preview-panel">
|
|
94
|
+
<div className="preview-header">
|
|
95
|
+
<div className="preview-info">
|
|
96
|
+
<div className="preview-title">{block.displayName}</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="preview-content">
|
|
100
|
+
<div className="preview-empty">
|
|
101
|
+
<div className="spinner" />
|
|
102
|
+
<p>Loading preview...</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Guard against invalid block name
|
|
110
|
+
if (!block.name || typeof block.name !== 'string') {
|
|
111
|
+
console.error('[Preview] Invalid block.name:', block.name, 'block:', block);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isTemplate = block.type === 'template' && block.pages && block.pages.length > 0;
|
|
116
|
+
const previewUrl = isTemplate
|
|
117
|
+
? `/preview/template/${block.name}/${currentPage || block.pages?.[0]?.slug || ''}`
|
|
118
|
+
: `/preview/${block.name}`;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="preview-panel">
|
|
122
|
+
<div className="preview-header">
|
|
123
|
+
<div className="preview-info">
|
|
124
|
+
<div className="preview-title">{block.displayName}</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="preview-actions">
|
|
127
|
+
<div className="preview-badge">Ready</div>
|
|
128
|
+
<button className="btn-publish" onClick={() => window.openPublishModal?.()}>
|
|
129
|
+
Publish
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="preview-content">
|
|
134
|
+
<div className={`preview-iframe-wrapper ${isTemplate ? 'template-preview' : ''}`}>
|
|
135
|
+
<iframe
|
|
136
|
+
ref={iframeRef}
|
|
137
|
+
className="preview-iframe"
|
|
138
|
+
src={previewUrl}
|
|
139
|
+
key={previewUrl}
|
|
140
|
+
onLoad={handleIframeLoad}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Block } from '../types';
|
|
3
|
+
|
|
4
|
+
export function useBlocks() {
|
|
5
|
+
const [blocks, setBlocks] = useState<Block[]>([]);
|
|
6
|
+
const [loading, setLoading] = useState(true);
|
|
7
|
+
const [error, setError] = useState<string | null>(null);
|
|
8
|
+
|
|
9
|
+
const loadBlocks = useCallback(async () => {
|
|
10
|
+
try {
|
|
11
|
+
setLoading(true);
|
|
12
|
+
const response = await fetch('/api/blocks');
|
|
13
|
+
if (!response.ok) throw new Error('Failed to load blocks');
|
|
14
|
+
const data = await response.json();
|
|
15
|
+
setBlocks(data);
|
|
16
|
+
setError(null);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
19
|
+
} finally {
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
loadBlocks();
|
|
26
|
+
}, [loadBlocks]);
|
|
27
|
+
|
|
28
|
+
return { blocks, loading, error, refresh: loadBlocks, setBlocks };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useBlockConfig(blockName: string | null, _blockType: string | null) {
|
|
32
|
+
const [config, setConfig] = useState<{
|
|
33
|
+
schema?: Record<string, unknown>;
|
|
34
|
+
previewData?: Record<string, unknown>;
|
|
35
|
+
pages?: Array<{ name: string; slug: string; blocksCount: number }>;
|
|
36
|
+
layoutSlots?: Array<{ slot: string; type: string }>;
|
|
37
|
+
} | null>(null);
|
|
38
|
+
const [loading, setLoading] = useState(false);
|
|
39
|
+
const [error, setError] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!blockName) {
|
|
43
|
+
setConfig(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const loadConfig = async () => {
|
|
48
|
+
setLoading(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Load block config (includes pages/layoutSlots for templates)
|
|
53
|
+
const configResponse = await fetch(`/api/blocks/${blockName}/config`);
|
|
54
|
+
if (!configResponse.ok) throw new Error('Failed to load config');
|
|
55
|
+
const configData = await configResponse.json();
|
|
56
|
+
|
|
57
|
+
// Debug: log raw API response (remove after debugging)
|
|
58
|
+
console.log(`[useBlockConfig] Raw config for "${blockName}":`, configData);
|
|
59
|
+
console.log('[useBlockConfig] previewData from API:', configData.previewData);
|
|
60
|
+
console.log('[useBlockConfig] previewData keys:', Object.keys(configData.previewData || {}));
|
|
61
|
+
console.log('[useBlockConfig] previewData.values:', configData.previewData?.values);
|
|
62
|
+
|
|
63
|
+
setConfig({
|
|
64
|
+
schema: configData.schema,
|
|
65
|
+
previewData: configData.previewData,
|
|
66
|
+
pages: configData.pages,
|
|
67
|
+
layoutSlots: configData.layoutSlots,
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
71
|
+
} finally {
|
|
72
|
+
setLoading(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
loadConfig();
|
|
77
|
+
}, [blockName]);
|
|
78
|
+
|
|
79
|
+
return { config, loading, error };
|
|
80
|
+
}
|