cmssy-cli 0.21.0 → 0.25.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/config.d.ts +1 -1
- package/dist/cli.js +131 -30
- package/dist/cli.js.map +1 -1
- 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 +31 -4
- 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/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
package/dist/dev-ui/app.js
CHANGED
|
@@ -20,6 +20,27 @@ let availableTags = [];
|
|
|
20
20
|
async function init() {
|
|
21
21
|
await loadBlocks();
|
|
22
22
|
setupSSE();
|
|
23
|
+
|
|
24
|
+
// Check for block parameter in URL
|
|
25
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
26
|
+
const blockParam = urlParams.get('block');
|
|
27
|
+
if (blockParam) {
|
|
28
|
+
const block = blocks.find(b => b.name === blockParam);
|
|
29
|
+
if (block) {
|
|
30
|
+
await selectBlock(blockParam);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Update URL with current block (without page reload)
|
|
36
|
+
function updateUrlWithBlock(blockName) {
|
|
37
|
+
const url = new URL(window.location);
|
|
38
|
+
if (blockName) {
|
|
39
|
+
url.searchParams.set('block', blockName);
|
|
40
|
+
} else {
|
|
41
|
+
url.searchParams.delete('block');
|
|
42
|
+
}
|
|
43
|
+
window.history.replaceState({}, '', url);
|
|
23
44
|
}
|
|
24
45
|
|
|
25
46
|
// Load all blocks from API
|
|
@@ -295,26 +316,70 @@ function renderBlockItem(block) {
|
|
|
295
316
|
`;
|
|
296
317
|
}
|
|
297
318
|
|
|
298
|
-
// Select a block
|
|
319
|
+
// Select a block (lazy loads config)
|
|
299
320
|
async function selectBlock(blockName) {
|
|
300
321
|
const block = blocks.find(b => b.name === blockName);
|
|
301
322
|
if (!block) return;
|
|
302
323
|
|
|
303
324
|
currentBlock = block;
|
|
304
|
-
|
|
325
|
+
updateUrlWithBlock(blockName);
|
|
326
|
+
renderBlocksList();
|
|
305
327
|
|
|
306
|
-
//
|
|
328
|
+
// Show loading state in editor
|
|
329
|
+
document.getElementById('preview-title').textContent = block.displayName || block.name;
|
|
330
|
+
document.getElementById('editor-subtitle').textContent = block.name;
|
|
331
|
+
document.getElementById('editor-content').innerHTML = `
|
|
332
|
+
<div class="loading">
|
|
333
|
+
<div class="spinner"></div>
|
|
334
|
+
<span>Loading properties...</span>
|
|
335
|
+
</div>
|
|
336
|
+
`;
|
|
337
|
+
|
|
338
|
+
// Lazy load full config and preview data
|
|
307
339
|
try {
|
|
308
|
-
const response = await fetch(`/api/
|
|
309
|
-
|
|
340
|
+
const response = await fetch(`/api/blocks/${blockName}/config`);
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
throw new Error('Failed to load block config');
|
|
343
|
+
}
|
|
344
|
+
const config = await response.json();
|
|
345
|
+
|
|
346
|
+
// Update block with loaded config
|
|
347
|
+
block.schema = config.schema;
|
|
348
|
+
block.category = config.category;
|
|
349
|
+
block.tags = config.tags;
|
|
350
|
+
block.displayName = config.displayName;
|
|
351
|
+
block.description = config.description;
|
|
352
|
+
previewData = config.previewData || {};
|
|
353
|
+
|
|
354
|
+
// For templates, also load pages info
|
|
355
|
+
if (block.type === 'template') {
|
|
356
|
+
try {
|
|
357
|
+
const pagesResponse = await fetch(`/api/templates/${blockName}/pages`);
|
|
358
|
+
if (pagesResponse.ok) {
|
|
359
|
+
const templateData = await pagesResponse.json();
|
|
360
|
+
block.pages = templateData.pages;
|
|
361
|
+
block.layoutSlots = templateData.layoutSlots;
|
|
362
|
+
}
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.warn('Could not load template pages:', err);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Update filters if we got new category/tags
|
|
369
|
+
populateFilters();
|
|
310
370
|
} catch (error) {
|
|
311
|
-
console.error('Failed to load
|
|
312
|
-
|
|
371
|
+
console.error('Failed to load block config:', error);
|
|
372
|
+
document.getElementById('editor-content').innerHTML = `
|
|
373
|
+
<div class="editor-empty" style="color: #e53935;">
|
|
374
|
+
Failed to load block config.<br>
|
|
375
|
+
<small>Check console for details.</small>
|
|
376
|
+
</div>
|
|
377
|
+
`;
|
|
378
|
+
return;
|
|
313
379
|
}
|
|
314
380
|
|
|
315
381
|
// Update UI
|
|
316
382
|
document.getElementById('preview-title').textContent = block.displayName || block.name;
|
|
317
|
-
document.getElementById('editor-subtitle').textContent = block.name;
|
|
318
383
|
|
|
319
384
|
// Show publish button
|
|
320
385
|
const publishBtn = document.getElementById('publish-btn');
|
|
@@ -334,27 +399,51 @@ function renderPreview() {
|
|
|
334
399
|
if (!currentBlock) return;
|
|
335
400
|
|
|
336
401
|
const previewContent = document.getElementById('preview-content');
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
402
|
+
const isTemplate = currentBlock.type === 'template' && currentBlock.pages && currentBlock.pages.length > 0;
|
|
403
|
+
|
|
404
|
+
if (isTemplate) {
|
|
405
|
+
// Template preview - show page selector and full page preview
|
|
406
|
+
const firstPage = currentBlock.pages[0];
|
|
407
|
+
previewContent.innerHTML = `
|
|
408
|
+
<div class="preview-iframe-wrapper template-preview">
|
|
409
|
+
<iframe
|
|
410
|
+
class="preview-iframe"
|
|
411
|
+
src="/preview/template/${currentBlock.name}/${firstPage.slug}"
|
|
412
|
+
id="preview-iframe"
|
|
413
|
+
></iframe>
|
|
414
|
+
</div>
|
|
415
|
+
`;
|
|
416
|
+
} else {
|
|
417
|
+
// Block preview - single block
|
|
418
|
+
previewContent.innerHTML = `
|
|
419
|
+
<div class="preview-iframe-wrapper">
|
|
420
|
+
<iframe
|
|
421
|
+
class="preview-iframe"
|
|
422
|
+
src="/preview/${currentBlock.name}"
|
|
423
|
+
id="preview-iframe"
|
|
424
|
+
></iframe>
|
|
425
|
+
</div>
|
|
426
|
+
`;
|
|
427
|
+
}
|
|
346
428
|
}
|
|
347
429
|
|
|
348
430
|
// Render editor form
|
|
349
431
|
function renderEditor() {
|
|
432
|
+
const editorContent = document.getElementById('editor-content');
|
|
433
|
+
|
|
434
|
+
// For templates, show pages overview
|
|
435
|
+
if (currentBlock && currentBlock.type === 'template' && currentBlock.pages) {
|
|
436
|
+
editorContent.innerHTML = renderTemplateEditor();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
350
440
|
if (!currentBlock || !currentBlock.schema) {
|
|
351
|
-
|
|
441
|
+
editorContent.innerHTML = `
|
|
352
442
|
<div class="editor-empty">No schema defined for this block</div>
|
|
353
443
|
`;
|
|
354
444
|
return;
|
|
355
445
|
}
|
|
356
446
|
|
|
357
|
-
const editorContent = document.getElementById('editor-content');
|
|
358
447
|
const fields = Object.entries(currentBlock.schema);
|
|
359
448
|
|
|
360
449
|
editorContent.innerHTML = fields.map(([key, field]) =>
|
|
@@ -365,6 +454,64 @@ function renderEditor() {
|
|
|
365
454
|
attachFieldListeners();
|
|
366
455
|
}
|
|
367
456
|
|
|
457
|
+
// Render template editor with pages list
|
|
458
|
+
function renderTemplateEditor() {
|
|
459
|
+
const pages = currentBlock.pages || [];
|
|
460
|
+
const layoutSlots = currentBlock.layoutSlots || [];
|
|
461
|
+
|
|
462
|
+
const pagesHtml = pages.map((page, index) => `
|
|
463
|
+
<div class="template-page-item" onclick="navigateToTemplatePage('${page.slug}')">
|
|
464
|
+
<div class="template-page-header">
|
|
465
|
+
<span class="template-page-name">${escapeHtml(page.name)}</span>
|
|
466
|
+
<span class="template-page-blocks">${page.blocksCount} blocks</span>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="template-page-slug">/${page.slug}</div>
|
|
469
|
+
</div>
|
|
470
|
+
`).join('');
|
|
471
|
+
|
|
472
|
+
const layoutHtml = layoutSlots.length > 0 ? `
|
|
473
|
+
<div class="template-section">
|
|
474
|
+
<h4 class="template-section-title">Layout Slots</h4>
|
|
475
|
+
${layoutSlots.map(slot => `
|
|
476
|
+
<div class="template-layout-slot">
|
|
477
|
+
<span class="slot-type">${slot.slot}</span>
|
|
478
|
+
<span class="slot-block">${slot.type}</span>
|
|
479
|
+
</div>
|
|
480
|
+
`).join('')}
|
|
481
|
+
</div>
|
|
482
|
+
` : '';
|
|
483
|
+
|
|
484
|
+
return `
|
|
485
|
+
<div class="template-editor">
|
|
486
|
+
<div class="template-info">
|
|
487
|
+
<div class="template-info-badge">Template</div>
|
|
488
|
+
<p class="template-info-desc">${escapeHtml(currentBlock.description || 'No description')}</p>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<div class="template-section">
|
|
492
|
+
<h4 class="template-section-title">Pages (${pages.length})</h4>
|
|
493
|
+
<div class="template-pages-list">
|
|
494
|
+
${pagesHtml || '<div class="editor-empty">No pages defined</div>'}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
${layoutHtml}
|
|
499
|
+
|
|
500
|
+
<div class="template-hint">
|
|
501
|
+
<p>Click on a page to preview it, or use the tabs in the preview header.</p>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Navigate to template page in preview
|
|
508
|
+
window.navigateToTemplatePage = function(pageSlug) {
|
|
509
|
+
const iframe = document.getElementById('preview-iframe');
|
|
510
|
+
if (iframe && currentBlock) {
|
|
511
|
+
iframe.src = `/preview/template/${currentBlock.name}/${pageSlug}`;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
368
515
|
// Render a single field based on type
|
|
369
516
|
function renderField(key, field, value) {
|
|
370
517
|
const required = field.required ? '<span class="field-required">*</span>' : '';
|
package/dist/dev-ui/index.html
CHANGED
|
@@ -302,6 +302,9 @@
|
|
|
302
302
|
display: flex;
|
|
303
303
|
flex-wrap: wrap;
|
|
304
304
|
gap: 6px;
|
|
305
|
+
max-height: 120px;
|
|
306
|
+
overflow-y: auto;
|
|
307
|
+
padding-right: 4px;
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
.tag-chip {
|
|
@@ -1118,6 +1121,141 @@
|
|
|
1118
1121
|
.editor-content {
|
|
1119
1122
|
transition: opacity 0.2s ease;
|
|
1120
1123
|
}
|
|
1124
|
+
|
|
1125
|
+
/* Template Editor Styles */
|
|
1126
|
+
.template-editor {
|
|
1127
|
+
padding: 0;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.template-info {
|
|
1131
|
+
padding: 16px;
|
|
1132
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1133
|
+
color: white;
|
|
1134
|
+
border-radius: 8px;
|
|
1135
|
+
margin-bottom: 20px;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.template-info-badge {
|
|
1139
|
+
display: inline-block;
|
|
1140
|
+
background: rgba(255,255,255,0.2);
|
|
1141
|
+
padding: 4px 10px;
|
|
1142
|
+
border-radius: 12px;
|
|
1143
|
+
font-size: 11px;
|
|
1144
|
+
font-weight: 600;
|
|
1145
|
+
text-transform: uppercase;
|
|
1146
|
+
letter-spacing: 0.5px;
|
|
1147
|
+
margin-bottom: 8px;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.template-info-desc {
|
|
1151
|
+
margin: 0;
|
|
1152
|
+
font-size: 13px;
|
|
1153
|
+
opacity: 0.9;
|
|
1154
|
+
line-height: 1.5;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.template-section {
|
|
1158
|
+
margin-bottom: 24px;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.template-section-title {
|
|
1162
|
+
font-size: 12px;
|
|
1163
|
+
font-weight: 600;
|
|
1164
|
+
text-transform: uppercase;
|
|
1165
|
+
letter-spacing: 0.5px;
|
|
1166
|
+
color: #666;
|
|
1167
|
+
margin: 0 0 12px 0;
|
|
1168
|
+
padding-bottom: 8px;
|
|
1169
|
+
border-bottom: 1px solid #eee;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.template-pages-list {
|
|
1173
|
+
display: flex;
|
|
1174
|
+
flex-direction: column;
|
|
1175
|
+
gap: 8px;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.template-page-item {
|
|
1179
|
+
padding: 12px 16px;
|
|
1180
|
+
background: #f8f9fa;
|
|
1181
|
+
border: 1px solid #e9ecef;
|
|
1182
|
+
border-radius: 8px;
|
|
1183
|
+
cursor: pointer;
|
|
1184
|
+
transition: all 0.2s;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.template-page-item:hover {
|
|
1188
|
+
background: #e9ecef;
|
|
1189
|
+
border-color: #667eea;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.template-page-header {
|
|
1193
|
+
display: flex;
|
|
1194
|
+
justify-content: space-between;
|
|
1195
|
+
align-items: center;
|
|
1196
|
+
margin-bottom: 4px;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.template-page-name {
|
|
1200
|
+
font-weight: 500;
|
|
1201
|
+
font-size: 14px;
|
|
1202
|
+
color: #333;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.template-page-blocks {
|
|
1206
|
+
font-size: 11px;
|
|
1207
|
+
color: #666;
|
|
1208
|
+
background: #e9ecef;
|
|
1209
|
+
padding: 2px 8px;
|
|
1210
|
+
border-radius: 10px;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.template-page-slug {
|
|
1214
|
+
font-size: 12px;
|
|
1215
|
+
color: #999;
|
|
1216
|
+
font-family: monospace;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.template-layout-slot {
|
|
1220
|
+
display: flex;
|
|
1221
|
+
justify-content: space-between;
|
|
1222
|
+
align-items: center;
|
|
1223
|
+
padding: 10px 14px;
|
|
1224
|
+
background: #f8f9fa;
|
|
1225
|
+
border-radius: 6px;
|
|
1226
|
+
margin-bottom: 8px;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.slot-type {
|
|
1230
|
+
font-size: 12px;
|
|
1231
|
+
font-weight: 500;
|
|
1232
|
+
text-transform: capitalize;
|
|
1233
|
+
color: #667eea;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.slot-block {
|
|
1237
|
+
font-size: 12px;
|
|
1238
|
+
color: #666;
|
|
1239
|
+
font-family: monospace;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.template-hint {
|
|
1243
|
+
padding: 12px;
|
|
1244
|
+
background: #fff3cd;
|
|
1245
|
+
border-radius: 6px;
|
|
1246
|
+
margin-top: 20px;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.template-hint p {
|
|
1250
|
+
margin: 0;
|
|
1251
|
+
font-size: 12px;
|
|
1252
|
+
color: #856404;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/* Template preview style */
|
|
1256
|
+
.template-preview {
|
|
1257
|
+
background: #1a1a2e;
|
|
1258
|
+
}
|
|
1121
1259
|
</style>
|
|
1122
1260
|
</head>
|
|
1123
1261
|
<body>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { BlocksList } from './components/BlocksList';
|
|
3
|
+
import { Preview } from './components/Preview';
|
|
4
|
+
import { Editor } from './components/Editor';
|
|
5
|
+
import { useBlocks, useBlockConfig } from './hooks/useBlocks';
|
|
6
|
+
import { Block } from './types';
|
|
7
|
+
import './styles.css';
|
|
8
|
+
|
|
9
|
+
export function App() {
|
|
10
|
+
const { blocks, loading: blocksLoading } = useBlocks();
|
|
11
|
+
const [selectedBlock, setSelectedBlock] = useState<Block | null>(null);
|
|
12
|
+
const [previewData, setPreviewData] = useState<Record<string, unknown>>({});
|
|
13
|
+
const [currentPage, setCurrentPage] = useState<string | undefined>();
|
|
14
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
15
|
+
// Keep reference to complete config data for merging during save
|
|
16
|
+
const configDataRef = useRef<Record<string, unknown>>({});
|
|
17
|
+
|
|
18
|
+
const { config, loading: configLoading } = useBlockConfig(
|
|
19
|
+
selectedBlock?.name || null,
|
|
20
|
+
selectedBlock?.type || null
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Update selected block with loaded config
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (selectedBlock && config) {
|
|
26
|
+
const updatedBlock: Block = {
|
|
27
|
+
...selectedBlock,
|
|
28
|
+
schema: config.schema as Block['schema'],
|
|
29
|
+
pages: config.pages,
|
|
30
|
+
layoutSlots: config.layoutSlots,
|
|
31
|
+
};
|
|
32
|
+
setSelectedBlock(updatedBlock);
|
|
33
|
+
|
|
34
|
+
// Store complete config data in ref for merging during save
|
|
35
|
+
const configData = config.previewData || {};
|
|
36
|
+
configDataRef.current = configData;
|
|
37
|
+
|
|
38
|
+
// Set previewData from config (complete data)
|
|
39
|
+
setPreviewData(configData);
|
|
40
|
+
|
|
41
|
+
// Set first page for templates
|
|
42
|
+
if (config.pages && config.pages.length > 0) {
|
|
43
|
+
setCurrentPage(config.pages[0].slug);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}, [config, selectedBlock?.name]);
|
|
47
|
+
|
|
48
|
+
// Handle block selection
|
|
49
|
+
const handleSelectBlock = useCallback((block: Block) => {
|
|
50
|
+
setSelectedBlock(block);
|
|
51
|
+
setPreviewData({});
|
|
52
|
+
setCurrentPage(undefined);
|
|
53
|
+
setIsDirty(false);
|
|
54
|
+
configDataRef.current = {};
|
|
55
|
+
|
|
56
|
+
// Update URL based on type
|
|
57
|
+
const url = new URL(window.location.href);
|
|
58
|
+
url.searchParams.delete('block');
|
|
59
|
+
url.searchParams.delete('template');
|
|
60
|
+
if (block.type === 'template') {
|
|
61
|
+
url.searchParams.set('template', block.name);
|
|
62
|
+
} else {
|
|
63
|
+
url.searchParams.set('block', block.name);
|
|
64
|
+
}
|
|
65
|
+
window.history.replaceState({}, '', url.toString());
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
// Handle preview data change - just update state, effect handles saving
|
|
69
|
+
const handlePreviewDataChange = useCallback(
|
|
70
|
+
(newData: Record<string, unknown>) => {
|
|
71
|
+
setPreviewData(newData);
|
|
72
|
+
setIsDirty(true);
|
|
73
|
+
},
|
|
74
|
+
[]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Debounced save effect - saves when data changes and is dirty
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
// Don't save if loading, not dirty, no block selected, or config not loaded yet
|
|
80
|
+
// configDataRef must have data to prevent losing fields
|
|
81
|
+
if (configLoading || !isDirty || !selectedBlock || Object.keys(configDataRef.current).length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timeoutId = setTimeout(async () => {
|
|
87
|
+
try {
|
|
88
|
+
// Merge user edits with complete config data to never lose fields
|
|
89
|
+
const dataToSave = { ...configDataRef.current, ...previewData };
|
|
90
|
+
|
|
91
|
+
await fetch(`/api/preview/${selectedBlock.name}`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify(dataToSave),
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
});
|
|
97
|
+
setIsDirty(false);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof Error && error.name !== 'AbortError') {
|
|
100
|
+
console.error('Failed to save preview data:', error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, 500);
|
|
104
|
+
|
|
105
|
+
// Cleanup: cancel timeout and abort fetch on unmount or dependency change
|
|
106
|
+
return () => {
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
controller.abort();
|
|
109
|
+
};
|
|
110
|
+
}, [previewData, selectedBlock, configLoading, isDirty]);
|
|
111
|
+
|
|
112
|
+
// Handle template page navigation
|
|
113
|
+
const handleNavigateToPage = useCallback((pageSlug: string) => {
|
|
114
|
+
setCurrentPage(pageSlug);
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
// Load block/template from URL on mount (runs once when blocks are loaded)
|
|
118
|
+
const urlLoadedRef = useRef(false);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (blocks.length === 0 || urlLoadedRef.current) return;
|
|
121
|
+
|
|
122
|
+
const params = new URLSearchParams(window.location.search);
|
|
123
|
+
const blockName = params.get('block');
|
|
124
|
+
const templateName = params.get('template');
|
|
125
|
+
|
|
126
|
+
if (templateName) {
|
|
127
|
+
const template = blocks.find((b) => b.name === templateName && b.type === 'template');
|
|
128
|
+
if (template) {
|
|
129
|
+
urlLoadedRef.current = true;
|
|
130
|
+
handleSelectBlock(template);
|
|
131
|
+
}
|
|
132
|
+
} else if (blockName) {
|
|
133
|
+
const block = blocks.find((b) => b.name === blockName);
|
|
134
|
+
if (block) {
|
|
135
|
+
urlLoadedRef.current = true;
|
|
136
|
+
handleSelectBlock(block);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}, [blocks, handleSelectBlock]);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="container">
|
|
143
|
+
<BlocksList
|
|
144
|
+
blocks={blocks}
|
|
145
|
+
selectedBlock={selectedBlock}
|
|
146
|
+
onSelectBlock={handleSelectBlock}
|
|
147
|
+
loading={blocksLoading}
|
|
148
|
+
/>
|
|
149
|
+
<Preview
|
|
150
|
+
block={selectedBlock}
|
|
151
|
+
previewData={previewData}
|
|
152
|
+
currentPage={currentPage}
|
|
153
|
+
loading={configLoading}
|
|
154
|
+
/>
|
|
155
|
+
<Editor
|
|
156
|
+
block={selectedBlock}
|
|
157
|
+
loading={configLoading}
|
|
158
|
+
previewData={previewData}
|
|
159
|
+
onPreviewDataChange={handlePreviewDataChange}
|
|
160
|
+
onNavigateToPage={handleNavigateToPage}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|