@cmssy/cli 0.1.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/LICENSE +21 -0
- package/README.md +649 -0
- package/config.d.ts +2 -0
- package/config.js +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +236 -0
- package/dist/cli.js.map +1 -0
- 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 +7 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +105 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/configure.d.ts +6 -0
- package/dist/commands/configure.d.ts.map +1 -0
- package/dist/commands/configure.js +42 -0
- package/dist/commands/configure.js.map +1 -0
- package/dist/commands/create.d.ts +18 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +444 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +962 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +362 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/migrate.d.ts +2 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +227 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/package.d.ts +7 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +136 -0
- package/dist/commands/package.js.map +1 -0
- package/dist/commands/publish.d.ts +13 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +910 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +208 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/upload.d.ts +7 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +126 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/commands/workspaces.d.ts +2 -0
- package/dist/commands/workspaces.d.ts.map +1 -0
- package/dist/commands/workspaces.js +67 -0
- package/dist/commands/workspaces.js.map +1 -0
- package/dist/dev-ui/app.js +1284 -0
- package/dist/dev-ui/index.html +1511 -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 +315 -0
- package/dist/types/block-config.d.ts.map +1 -0
- package/dist/types/block-config.js +8 -0
- package/dist/types/block-config.js.map +1 -0
- package/dist/utils/block-config.d.ts +10 -0
- package/dist/utils/block-config.d.ts.map +1 -0
- package/dist/utils/block-config.js +199 -0
- package/dist/utils/block-config.js.map +1 -0
- 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 +34 -0
- package/dist/utils/builder.d.ts.map +1 -0
- package/dist/utils/builder.js +140 -0
- package/dist/utils/builder.js.map +1 -0
- package/dist/utils/cmssy-config.d.ts +16 -0
- package/dist/utils/cmssy-config.d.ts.map +1 -0
- package/dist/utils/cmssy-config.js +19 -0
- package/dist/utils/cmssy-config.js.map +1 -0
- package/dist/utils/config.d.ts +9 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +46 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/field-schema.d.ts +12 -0
- package/dist/utils/field-schema.d.ts.map +1 -0
- package/dist/utils/field-schema.js +202 -0
- package/dist/utils/field-schema.js.map +1 -0
- package/dist/utils/graphql.d.ts +8 -0
- package/dist/utils/graphql.d.ts.map +1 -0
- package/dist/utils/graphql.js +118 -0
- package/dist/utils/graphql.js.map +1 -0
- package/dist/utils/publish-helpers.d.ts +35 -0
- package/dist/utils/publish-helpers.d.ts.map +1 -0
- package/dist/utils/publish-helpers.js +141 -0
- package/dist/utils/publish-helpers.js.map +1 -0
- package/dist/utils/scanner.d.ts +36 -0
- package/dist/utils/scanner.d.ts.map +1 -0
- package/dist/utils/scanner.js +140 -0
- package/dist/utils/scanner.js.map +1 -0
- package/dist/utils/type-generator.d.ts +9 -0
- package/dist/utils/type-generator.d.ts.map +1 -0
- package/dist/utils/type-generator.js +85 -0
- package/dist/utils/type-generator.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
// Cmssy Dev Server - Interactive UI
|
|
2
|
+
let currentBlock = null;
|
|
3
|
+
let blocks = [];
|
|
4
|
+
let previewData = {};
|
|
5
|
+
let eventSource = null;
|
|
6
|
+
|
|
7
|
+
// Filters state
|
|
8
|
+
let filters = {
|
|
9
|
+
search: '',
|
|
10
|
+
type: 'all',
|
|
11
|
+
category: '',
|
|
12
|
+
tags: []
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Available categories and tags (populated from blocks)
|
|
16
|
+
let availableCategories = [];
|
|
17
|
+
let availableTags = [];
|
|
18
|
+
|
|
19
|
+
// Initialize app
|
|
20
|
+
async function init() {
|
|
21
|
+
await loadBlocks();
|
|
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);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Load all blocks from API
|
|
47
|
+
async function loadBlocks() {
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch('/api/blocks');
|
|
50
|
+
blocks = await response.json();
|
|
51
|
+
populateFilters();
|
|
52
|
+
renderBlocksList();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Failed to load blocks:', error);
|
|
55
|
+
document.getElementById('blocks-list').innerHTML = `
|
|
56
|
+
<div style="padding: 20px; color: #e53935;">
|
|
57
|
+
Failed to load blocks. Make sure the dev server is running.
|
|
58
|
+
</div>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Populate filter options from loaded blocks
|
|
64
|
+
function populateFilters() {
|
|
65
|
+
// Extract unique categories
|
|
66
|
+
const categoriesSet = new Set();
|
|
67
|
+
const tagsSet = new Set();
|
|
68
|
+
|
|
69
|
+
blocks.forEach(block => {
|
|
70
|
+
if (block.category) {
|
|
71
|
+
categoriesSet.add(block.category);
|
|
72
|
+
}
|
|
73
|
+
if (block.tags && Array.isArray(block.tags)) {
|
|
74
|
+
block.tags.forEach(tag => tagsSet.add(tag));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
availableCategories = Array.from(categoriesSet).sort();
|
|
79
|
+
availableTags = Array.from(tagsSet).sort();
|
|
80
|
+
|
|
81
|
+
// Populate category dropdown
|
|
82
|
+
const categorySelect = document.getElementById('category-filter');
|
|
83
|
+
if (categorySelect) {
|
|
84
|
+
categorySelect.innerHTML = '<option value="">All Categories</option>';
|
|
85
|
+
availableCategories.forEach(cat => {
|
|
86
|
+
const option = document.createElement('option');
|
|
87
|
+
option.value = cat;
|
|
88
|
+
option.textContent = cat;
|
|
89
|
+
categorySelect.appendChild(option);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Populate tags filter
|
|
94
|
+
const tagsContainer = document.getElementById('tags-filter');
|
|
95
|
+
if (tagsContainer) {
|
|
96
|
+
if (availableTags.length === 0) {
|
|
97
|
+
tagsContainer.style.display = 'none';
|
|
98
|
+
} else {
|
|
99
|
+
tagsContainer.style.display = 'flex';
|
|
100
|
+
tagsContainer.innerHTML = availableTags.map(tag => `
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="tag-chip ${filters.tags.includes(tag) ? 'active' : ''}"
|
|
104
|
+
data-tag="${escapeHtml(tag)}"
|
|
105
|
+
onclick="toggleTagFilter('${escapeHtml(tag)}')"
|
|
106
|
+
>${escapeHtml(tag)}</button>
|
|
107
|
+
`).join('');
|
|
108
|
+
|
|
109
|
+
// Add clear button if any filters active
|
|
110
|
+
if (hasActiveFilters()) {
|
|
111
|
+
tagsContainer.innerHTML += `
|
|
112
|
+
<button type="button" class="clear-filters" onclick="clearAllFilters()">
|
|
113
|
+
Clear all
|
|
114
|
+
</button>
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if any filters are active
|
|
122
|
+
function hasActiveFilters() {
|
|
123
|
+
return filters.search !== '' ||
|
|
124
|
+
filters.type !== 'all' ||
|
|
125
|
+
filters.category !== '' ||
|
|
126
|
+
filters.tags.length > 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Filter handlers
|
|
130
|
+
window.handleSearchInput = function(event) {
|
|
131
|
+
filters.search = event.target.value.toLowerCase();
|
|
132
|
+
renderBlocksList();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
window.setTypeFilter = function(type) {
|
|
136
|
+
filters.type = type;
|
|
137
|
+
|
|
138
|
+
// Update tab UI
|
|
139
|
+
document.querySelectorAll('.filter-tab').forEach(tab => {
|
|
140
|
+
tab.classList.toggle('active', tab.dataset.type === type);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
renderBlocksList();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
window.setCategoryFilter = function(category) {
|
|
147
|
+
filters.category = category;
|
|
148
|
+
renderBlocksList();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
window.toggleTagFilter = function(tag) {
|
|
152
|
+
const index = filters.tags.indexOf(tag);
|
|
153
|
+
if (index === -1) {
|
|
154
|
+
filters.tags.push(tag);
|
|
155
|
+
} else {
|
|
156
|
+
filters.tags.splice(index, 1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update tag chip UI
|
|
160
|
+
document.querySelectorAll('.tag-chip').forEach(chip => {
|
|
161
|
+
if (chip.dataset.tag === tag) {
|
|
162
|
+
chip.classList.toggle('active');
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Re-render to update clear button
|
|
167
|
+
populateFilters();
|
|
168
|
+
renderBlocksList();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
window.clearAllFilters = function() {
|
|
172
|
+
filters = {
|
|
173
|
+
search: '',
|
|
174
|
+
type: 'all',
|
|
175
|
+
category: '',
|
|
176
|
+
tags: []
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Reset UI
|
|
180
|
+
document.getElementById('search-input').value = '';
|
|
181
|
+
document.getElementById('category-filter').value = '';
|
|
182
|
+
document.querySelectorAll('.filter-tab').forEach(tab => {
|
|
183
|
+
tab.classList.toggle('active', tab.dataset.type === 'all');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
populateFilters();
|
|
187
|
+
renderBlocksList();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Get filtered blocks based on current filters
|
|
191
|
+
function getFilteredBlocks() {
|
|
192
|
+
return blocks.filter(block => {
|
|
193
|
+
// Search filter
|
|
194
|
+
if (filters.search) {
|
|
195
|
+
const searchLower = filters.search.toLowerCase();
|
|
196
|
+
const matchesSearch =
|
|
197
|
+
(block.displayName || block.name).toLowerCase().includes(searchLower) ||
|
|
198
|
+
(block.description || '').toLowerCase().includes(searchLower) ||
|
|
199
|
+
(block.name || '').toLowerCase().includes(searchLower);
|
|
200
|
+
if (!matchesSearch) return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Type filter
|
|
204
|
+
if (filters.type !== 'all') {
|
|
205
|
+
const blockType = block.type || 'block';
|
|
206
|
+
if (blockType !== filters.type) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Category filter
|
|
212
|
+
if (filters.category && block.category !== filters.category) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Tags filter (match ANY selected tag)
|
|
217
|
+
if (filters.tags.length > 0) {
|
|
218
|
+
const blockTags = block.tags || [];
|
|
219
|
+
const hasAnyTag = filters.tags.some(tag => blockTags.includes(tag));
|
|
220
|
+
if (!hasAnyTag) return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return true;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Render blocks list
|
|
228
|
+
function renderBlocksList() {
|
|
229
|
+
const listEl = document.getElementById('blocks-list');
|
|
230
|
+
const countEl = document.getElementById('blocks-count');
|
|
231
|
+
|
|
232
|
+
if (blocks.length === 0) {
|
|
233
|
+
listEl.innerHTML = '<div class="editor-empty">No blocks found</div>';
|
|
234
|
+
countEl.textContent = 'No blocks';
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Get filtered blocks
|
|
239
|
+
const filteredBlocks = getFilteredBlocks();
|
|
240
|
+
|
|
241
|
+
// Update count with filter info
|
|
242
|
+
if (hasActiveFilters()) {
|
|
243
|
+
countEl.textContent = `${filteredBlocks.length} of ${blocks.length} items`;
|
|
244
|
+
} else {
|
|
245
|
+
countEl.textContent = `${blocks.length} ${blocks.length === 1 ? 'item' : 'items'}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Handle empty filtered results
|
|
249
|
+
if (filteredBlocks.length === 0) {
|
|
250
|
+
listEl.innerHTML = `
|
|
251
|
+
<div class="no-results">
|
|
252
|
+
<div class="no-results-icon">🔍</div>
|
|
253
|
+
<div>No items match your filters</div>
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
style="margin-top: 12px; padding: 6px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px;"
|
|
257
|
+
onclick="clearAllFilters()"
|
|
258
|
+
>Clear filters</button>
|
|
259
|
+
</div>
|
|
260
|
+
`;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Group blocks by category
|
|
265
|
+
const grouped = {};
|
|
266
|
+
filteredBlocks.forEach(block => {
|
|
267
|
+
const cat = block.category || 'Uncategorized';
|
|
268
|
+
if (!grouped[cat]) grouped[cat] = [];
|
|
269
|
+
grouped[cat].push(block);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Sort categories alphabetically (but put Uncategorized last)
|
|
273
|
+
const sortedCategories = Object.keys(grouped).sort((a, b) => {
|
|
274
|
+
if (a === 'Uncategorized') return 1;
|
|
275
|
+
if (b === 'Uncategorized') return -1;
|
|
276
|
+
return a.localeCompare(b);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Render grouped blocks
|
|
280
|
+
listEl.innerHTML = sortedCategories.map(category => `
|
|
281
|
+
<div class="block-category">
|
|
282
|
+
<div class="category-header">
|
|
283
|
+
${escapeHtml(category)}
|
|
284
|
+
<span class="category-count">${grouped[category].length}</span>
|
|
285
|
+
</div>
|
|
286
|
+
${grouped[category].map(block => renderBlockItem(block)).join('')}
|
|
287
|
+
</div>
|
|
288
|
+
`).join('');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Render a single block item
|
|
292
|
+
function renderBlockItem(block) {
|
|
293
|
+
const isTemplate = block.type === 'template';
|
|
294
|
+
const typeBadge = isTemplate
|
|
295
|
+
? '<span class="type-badge template">Template</span>'
|
|
296
|
+
: '';
|
|
297
|
+
|
|
298
|
+
return `
|
|
299
|
+
<div
|
|
300
|
+
class="block-item ${currentBlock?.name === block.name ? 'active' : ''}"
|
|
301
|
+
data-block="${escapeHtml(block.name)}"
|
|
302
|
+
onclick="selectBlock('${escapeHtml(block.name)}')"
|
|
303
|
+
>
|
|
304
|
+
<div class="block-item-header">
|
|
305
|
+
<div class="block-item-name">
|
|
306
|
+
${escapeHtml(block.displayName || block.name)}
|
|
307
|
+
${typeBadge}
|
|
308
|
+
</div>
|
|
309
|
+
<span class="version-badge">v${block.version || '1.0.0'}</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="block-item-footer">
|
|
312
|
+
<span class="block-item-type">${escapeHtml(block.category || 'Block')}</span>
|
|
313
|
+
<span class="status-badge status-local">Local</span>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Select a block (lazy loads config)
|
|
320
|
+
async function selectBlock(blockName) {
|
|
321
|
+
const block = blocks.find(b => b.name === blockName);
|
|
322
|
+
if (!block) return;
|
|
323
|
+
|
|
324
|
+
currentBlock = block;
|
|
325
|
+
updateUrlWithBlock(blockName);
|
|
326
|
+
renderBlocksList();
|
|
327
|
+
|
|
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
|
|
339
|
+
try {
|
|
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();
|
|
370
|
+
} catch (error) {
|
|
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;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Update UI
|
|
382
|
+
document.getElementById('preview-title').textContent = block.displayName || block.name;
|
|
383
|
+
|
|
384
|
+
// Show publish button
|
|
385
|
+
const publishBtn = document.getElementById('publish-btn');
|
|
386
|
+
if (publishBtn) {
|
|
387
|
+
publishBtn.style.display = 'block';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Render preview
|
|
391
|
+
renderPreview();
|
|
392
|
+
|
|
393
|
+
// Render editor form
|
|
394
|
+
renderEditor();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Render preview iframe
|
|
398
|
+
function renderPreview() {
|
|
399
|
+
if (!currentBlock) return;
|
|
400
|
+
|
|
401
|
+
const previewContent = document.getElementById('preview-content');
|
|
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
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Render editor form
|
|
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
|
+
|
|
440
|
+
if (!currentBlock || !currentBlock.schema) {
|
|
441
|
+
editorContent.innerHTML = `
|
|
442
|
+
<div class="editor-empty">No schema defined for this block</div>
|
|
443
|
+
`;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const fields = Object.entries(currentBlock.schema);
|
|
448
|
+
|
|
449
|
+
editorContent.innerHTML = fields.map(([key, field]) =>
|
|
450
|
+
renderField(key, field, previewData[key])
|
|
451
|
+
).join('');
|
|
452
|
+
|
|
453
|
+
// Attach event listeners
|
|
454
|
+
attachFieldListeners();
|
|
455
|
+
}
|
|
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
|
+
|
|
515
|
+
// Render a single field based on type
|
|
516
|
+
function renderField(key, field, value) {
|
|
517
|
+
const required = field.required ? '<span class="field-required">*</span>' : '';
|
|
518
|
+
const helpText = field.helpText ? `<div class="field-help">${field.helpText}</div>` : '';
|
|
519
|
+
|
|
520
|
+
let inputHtml = '';
|
|
521
|
+
|
|
522
|
+
switch (field.type) {
|
|
523
|
+
case 'singleLine':
|
|
524
|
+
case 'text':
|
|
525
|
+
case 'string':
|
|
526
|
+
inputHtml = `
|
|
527
|
+
<input
|
|
528
|
+
type="text"
|
|
529
|
+
class="field-input"
|
|
530
|
+
data-field="${key}"
|
|
531
|
+
value="${escapeHtml(value || field.defaultValue || '')}"
|
|
532
|
+
placeholder="${field.placeholder || ''}"
|
|
533
|
+
${field.required ? 'required' : ''}
|
|
534
|
+
/>
|
|
535
|
+
`;
|
|
536
|
+
break;
|
|
537
|
+
|
|
538
|
+
case 'multiLine':
|
|
539
|
+
inputHtml = `
|
|
540
|
+
<textarea
|
|
541
|
+
class="field-input field-textarea"
|
|
542
|
+
data-field="${key}"
|
|
543
|
+
placeholder="${field.placeholder || ''}"
|
|
544
|
+
${field.required ? 'required' : ''}
|
|
545
|
+
>${escapeHtml(value || field.defaultValue || '')}</textarea>
|
|
546
|
+
`;
|
|
547
|
+
break;
|
|
548
|
+
|
|
549
|
+
case 'richText':
|
|
550
|
+
inputHtml = `
|
|
551
|
+
<textarea
|
|
552
|
+
class="field-input field-textarea"
|
|
553
|
+
data-field="${key}"
|
|
554
|
+
placeholder="${field.placeholder || 'Enter rich text...'}"
|
|
555
|
+
${field.required ? 'required' : ''}
|
|
556
|
+
style="min-height: 120px;"
|
|
557
|
+
>${escapeHtml(value || field.defaultValue || '')}</textarea>
|
|
558
|
+
`;
|
|
559
|
+
break;
|
|
560
|
+
|
|
561
|
+
case 'number':
|
|
562
|
+
inputHtml = `
|
|
563
|
+
<input
|
|
564
|
+
type="number"
|
|
565
|
+
class="field-input"
|
|
566
|
+
data-field="${key}"
|
|
567
|
+
value="${value !== undefined ? value : (field.defaultValue || '')}"
|
|
568
|
+
placeholder="${field.placeholder || ''}"
|
|
569
|
+
${field.required ? 'required' : ''}
|
|
570
|
+
/>
|
|
571
|
+
`;
|
|
572
|
+
break;
|
|
573
|
+
|
|
574
|
+
case 'boolean':
|
|
575
|
+
inputHtml = `
|
|
576
|
+
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
577
|
+
<input
|
|
578
|
+
type="checkbox"
|
|
579
|
+
class="field-checkbox"
|
|
580
|
+
data-field="${key}"
|
|
581
|
+
${value || field.defaultValue ? 'checked' : ''}
|
|
582
|
+
/>
|
|
583
|
+
<span>${field.label}</span>
|
|
584
|
+
</label>
|
|
585
|
+
`;
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
case 'date':
|
|
589
|
+
inputHtml = `
|
|
590
|
+
<input
|
|
591
|
+
type="date"
|
|
592
|
+
class="field-input"
|
|
593
|
+
data-field="${key}"
|
|
594
|
+
value="${value || field.defaultValue || ''}"
|
|
595
|
+
${field.required ? 'required' : ''}
|
|
596
|
+
/>
|
|
597
|
+
`;
|
|
598
|
+
break;
|
|
599
|
+
|
|
600
|
+
case 'link':
|
|
601
|
+
inputHtml = `
|
|
602
|
+
<input
|
|
603
|
+
type="url"
|
|
604
|
+
class="field-input"
|
|
605
|
+
data-field="${key}"
|
|
606
|
+
value="${escapeHtml(value || field.defaultValue || '')}"
|
|
607
|
+
placeholder="${field.placeholder || 'https://...'}"
|
|
608
|
+
${field.required ? 'required' : ''}
|
|
609
|
+
/>
|
|
610
|
+
`;
|
|
611
|
+
break;
|
|
612
|
+
|
|
613
|
+
case 'color':
|
|
614
|
+
const colorValue = value || field.defaultValue || '#000000';
|
|
615
|
+
inputHtml = `
|
|
616
|
+
<div class="color-field">
|
|
617
|
+
<input
|
|
618
|
+
type="color"
|
|
619
|
+
class="color-preview"
|
|
620
|
+
data-field="${key}"
|
|
621
|
+
value="${colorValue}"
|
|
622
|
+
/>
|
|
623
|
+
<input
|
|
624
|
+
type="text"
|
|
625
|
+
class="field-input color-input"
|
|
626
|
+
data-field="${key}-text"
|
|
627
|
+
value="${colorValue}"
|
|
628
|
+
placeholder="#000000"
|
|
629
|
+
/>
|
|
630
|
+
</div>
|
|
631
|
+
`;
|
|
632
|
+
break;
|
|
633
|
+
|
|
634
|
+
case 'select':
|
|
635
|
+
const currentValue = value || field.defaultValue || '';
|
|
636
|
+
inputHtml = `
|
|
637
|
+
<select class="field-input field-select" data-field="${key}" ${field.required ? 'required' : ''}>
|
|
638
|
+
<option value="">Select an option...</option>
|
|
639
|
+
${field.options.map(opt => {
|
|
640
|
+
const optValue = typeof opt === 'string' ? opt : opt.value;
|
|
641
|
+
const optLabel = typeof opt === 'string' ? opt : opt.label;
|
|
642
|
+
return `<option value="${escapeHtml(optValue)}" ${currentValue === optValue ? 'selected' : ''}>${escapeHtml(optLabel)}</option>`;
|
|
643
|
+
}).join('')}
|
|
644
|
+
</select>
|
|
645
|
+
`;
|
|
646
|
+
break;
|
|
647
|
+
|
|
648
|
+
case 'media':
|
|
649
|
+
const mediaValue = value || field.defaultValue || {};
|
|
650
|
+
inputHtml = `
|
|
651
|
+
<div class="media-field">
|
|
652
|
+
<div class="media-preview">
|
|
653
|
+
${mediaValue.url ?
|
|
654
|
+
`<img src="${escapeHtml(mediaValue.url)}" alt="${escapeHtml(mediaValue.alt || '')}"/>` :
|
|
655
|
+
'<div class="media-placeholder">No image</div>'
|
|
656
|
+
}
|
|
657
|
+
</div>
|
|
658
|
+
<div class="media-input-group">
|
|
659
|
+
<input
|
|
660
|
+
type="url"
|
|
661
|
+
class="field-input"
|
|
662
|
+
data-field="${key}.url"
|
|
663
|
+
value="${escapeHtml(mediaValue.url || '')}"
|
|
664
|
+
placeholder="Image URL"
|
|
665
|
+
style="margin-bottom: 8px;"
|
|
666
|
+
/>
|
|
667
|
+
<input
|
|
668
|
+
type="text"
|
|
669
|
+
class="field-input"
|
|
670
|
+
data-field="${key}.alt"
|
|
671
|
+
value="${escapeHtml(mediaValue.alt || '')}"
|
|
672
|
+
placeholder="Alt text"
|
|
673
|
+
/>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
`;
|
|
677
|
+
break;
|
|
678
|
+
|
|
679
|
+
case 'repeater':
|
|
680
|
+
inputHtml = renderRepeaterField(key, field, value || field.defaultValue || []);
|
|
681
|
+
break;
|
|
682
|
+
|
|
683
|
+
default:
|
|
684
|
+
inputHtml = `<div style="color: #999;">Unsupported field type: ${field.type}</div>`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Special case for boolean - don't show separate label
|
|
688
|
+
if (field.type === 'boolean') {
|
|
689
|
+
return `
|
|
690
|
+
<div class="field-group">
|
|
691
|
+
${inputHtml}
|
|
692
|
+
${helpText}
|
|
693
|
+
</div>
|
|
694
|
+
`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return `
|
|
698
|
+
<div class="field-group">
|
|
699
|
+
<label class="field-label">
|
|
700
|
+
${field.label}${required}
|
|
701
|
+
</label>
|
|
702
|
+
${inputHtml}
|
|
703
|
+
${helpText}
|
|
704
|
+
</div>
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Render repeater field
|
|
709
|
+
function renderRepeaterField(key, field, items) {
|
|
710
|
+
const minItems = field.minItems || 0;
|
|
711
|
+
const maxItems = field.maxItems || 999;
|
|
712
|
+
|
|
713
|
+
const itemsHtml = items.map((item, index) => {
|
|
714
|
+
const nestedFields = Object.entries(field.schema || {}).map(([nestedKey, nestedField]) => {
|
|
715
|
+
return renderField(`${key}.${index}.${nestedKey}`, nestedField, item[nestedKey]);
|
|
716
|
+
}).join('');
|
|
717
|
+
|
|
718
|
+
return `
|
|
719
|
+
<div class="repeater-item" data-repeater-item="${key}.${index}">
|
|
720
|
+
<div class="repeater-item-header">
|
|
721
|
+
<div class="repeater-item-title">Item ${index + 1}</div>
|
|
722
|
+
${items.length > minItems ? `
|
|
723
|
+
<button
|
|
724
|
+
type="button"
|
|
725
|
+
class="repeater-item-remove"
|
|
726
|
+
onclick="removeRepeaterItem('${key}', ${index})"
|
|
727
|
+
>Remove</button>
|
|
728
|
+
` : ''}
|
|
729
|
+
</div>
|
|
730
|
+
${nestedFields}
|
|
731
|
+
</div>
|
|
732
|
+
`;
|
|
733
|
+
}).join('');
|
|
734
|
+
|
|
735
|
+
return `
|
|
736
|
+
<div class="repeater-items" data-repeater="${key}">
|
|
737
|
+
${itemsHtml || '<div style="padding: 12px; color: #999; text-align: center;">No items yet</div>'}
|
|
738
|
+
</div>
|
|
739
|
+
${items.length < maxItems ? `
|
|
740
|
+
<button
|
|
741
|
+
type="button"
|
|
742
|
+
class="repeater-add"
|
|
743
|
+
onclick="addRepeaterItem('${key}')"
|
|
744
|
+
>+ Add Item</button>
|
|
745
|
+
` : ''}
|
|
746
|
+
`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Get nested field definition from schema using dot notation path
|
|
750
|
+
function getNestedFieldDefinition(key) {
|
|
751
|
+
const parts = key.split('.');
|
|
752
|
+
let fieldDef = null;
|
|
753
|
+
let schema = currentBlock.schema;
|
|
754
|
+
|
|
755
|
+
for (let i = 0; i < parts.length; i++) {
|
|
756
|
+
const part = parts[i];
|
|
757
|
+
|
|
758
|
+
// Skip numeric indices (they reference array items, not schema keys)
|
|
759
|
+
if (/^\d+$/.test(part)) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (schema && schema[part]) {
|
|
764
|
+
fieldDef = schema[part];
|
|
765
|
+
// If this is a repeater, its nested fields are in .schema
|
|
766
|
+
if (fieldDef.type === 'repeater' && fieldDef.schema) {
|
|
767
|
+
schema = fieldDef.schema;
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return fieldDef;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Get nested value from previewData using dot notation path
|
|
778
|
+
function getNestedValue(key) {
|
|
779
|
+
const parts = key.split('.');
|
|
780
|
+
let value = previewData;
|
|
781
|
+
|
|
782
|
+
for (const part of parts) {
|
|
783
|
+
if (value === undefined || value === null) return undefined;
|
|
784
|
+
value = value[part];
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Set nested value in previewData using dot notation path
|
|
791
|
+
function setNestedValue(key, newValue) {
|
|
792
|
+
const parts = key.split('.');
|
|
793
|
+
let obj = previewData;
|
|
794
|
+
|
|
795
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
796
|
+
const part = parts[i];
|
|
797
|
+
const nextPart = parts[i + 1];
|
|
798
|
+
|
|
799
|
+
if (obj[part] === undefined) {
|
|
800
|
+
// Create array if next part is numeric, otherwise object
|
|
801
|
+
obj[part] = /^\d+$/.test(nextPart) ? [] : {};
|
|
802
|
+
}
|
|
803
|
+
obj = obj[part];
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
obj[parts[parts.length - 1]] = newValue;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Add repeater item
|
|
810
|
+
window.addRepeaterItem = function(key) {
|
|
811
|
+
// Get field definition (supports nested paths like "plans.0.features")
|
|
812
|
+
const field = getNestedFieldDefinition(key);
|
|
813
|
+
if (!field || !field.schema) {
|
|
814
|
+
console.error('Could not find repeater field definition for:', key);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Get current items at the nested path
|
|
819
|
+
const currentItems = getNestedValue(key) || [];
|
|
820
|
+
|
|
821
|
+
// Create new empty item with defaults
|
|
822
|
+
const newItem = {};
|
|
823
|
+
Object.keys(field.schema).forEach(nestedKey => {
|
|
824
|
+
const nestedField = field.schema[nestedKey];
|
|
825
|
+
if (nestedField.type === 'repeater') {
|
|
826
|
+
newItem[nestedKey] = []; // Nested repeaters start empty
|
|
827
|
+
} else {
|
|
828
|
+
newItem[nestedKey] = nestedField.defaultValue !== undefined ? nestedField.defaultValue : '';
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// Set the updated array at the nested path
|
|
833
|
+
setNestedValue(key, [...currentItems, newItem]);
|
|
834
|
+
|
|
835
|
+
// Re-render editor and save
|
|
836
|
+
renderEditor();
|
|
837
|
+
savePreviewData();
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// Remove repeater item
|
|
841
|
+
window.removeRepeaterItem = function(key, index) {
|
|
842
|
+
// Get current items at the nested path
|
|
843
|
+
const currentItems = getNestedValue(key) || [];
|
|
844
|
+
|
|
845
|
+
// Filter out the item at the given index
|
|
846
|
+
const newItems = currentItems.filter((_, i) => i !== index);
|
|
847
|
+
|
|
848
|
+
// Set the updated array at the nested path
|
|
849
|
+
setNestedValue(key, newItems);
|
|
850
|
+
|
|
851
|
+
// Re-render editor and save
|
|
852
|
+
renderEditor();
|
|
853
|
+
savePreviewData();
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// Attach event listeners to form fields
|
|
857
|
+
function attachFieldListeners() {
|
|
858
|
+
const inputs = document.querySelectorAll('[data-field]');
|
|
859
|
+
inputs.forEach(input => {
|
|
860
|
+
const eventType = input.type === 'checkbox' ? 'change' : 'input';
|
|
861
|
+
input.addEventListener(eventType, handleFieldChange);
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Handle field value change
|
|
866
|
+
function handleFieldChange(event) {
|
|
867
|
+
const input = event.target;
|
|
868
|
+
const fieldPath = input.dataset.field;
|
|
869
|
+
|
|
870
|
+
let value;
|
|
871
|
+
if (input.type === 'checkbox') {
|
|
872
|
+
value = input.checked;
|
|
873
|
+
} else if (input.type === 'number') {
|
|
874
|
+
value = input.value ? parseFloat(input.value) : '';
|
|
875
|
+
} else {
|
|
876
|
+
value = input.value;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Use generic nested setter to handle any depth (supports nested repeaters)
|
|
880
|
+
setNestedValue(fieldPath, value);
|
|
881
|
+
|
|
882
|
+
// Sync color picker with text input
|
|
883
|
+
if (fieldPath.endsWith('-text')) {
|
|
884
|
+
const colorKey = fieldPath.replace('-text', '');
|
|
885
|
+
const colorInput = document.querySelector(`[data-field="${colorKey}"]`);
|
|
886
|
+
if (colorInput) colorInput.value = value;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Debounce save (quick debounce since we're using postMessage for instant updates)
|
|
890
|
+
clearTimeout(window.saveTimeout);
|
|
891
|
+
window.saveTimeout = setTimeout(() => savePreviewData(), 200);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Save preview data to server
|
|
895
|
+
async function savePreviewData() {
|
|
896
|
+
if (!currentBlock) return;
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
// Update preview iframe immediately (no reload/blink)
|
|
900
|
+
const iframe = document.getElementById('preview-iframe');
|
|
901
|
+
if (iframe && iframe.contentWindow) {
|
|
902
|
+
iframe.contentWindow.postMessage({
|
|
903
|
+
type: 'UPDATE_PROPS',
|
|
904
|
+
props: previewData
|
|
905
|
+
}, '*');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Save to server in background
|
|
909
|
+
const response = await fetch(`/api/preview/${currentBlock.name}`, {
|
|
910
|
+
method: 'POST',
|
|
911
|
+
headers: { 'Content-Type': 'application/json' },
|
|
912
|
+
body: JSON.stringify(previewData)
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
if (response.ok) {
|
|
916
|
+
document.getElementById('preview-status').textContent = 'Saved';
|
|
917
|
+
setTimeout(() => {
|
|
918
|
+
document.getElementById('preview-status').textContent = 'Ready';
|
|
919
|
+
}, 1000);
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.error('Failed to save preview data:', error);
|
|
923
|
+
document.getElementById('preview-status').textContent = 'Error';
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Setup Server-Sent Events for hot reload
|
|
928
|
+
function setupSSE() {
|
|
929
|
+
eventSource = new EventSource('/events');
|
|
930
|
+
|
|
931
|
+
eventSource.onmessage = async (event) => {
|
|
932
|
+
const data = JSON.parse(event.data);
|
|
933
|
+
|
|
934
|
+
if (data.type === 'reload') {
|
|
935
|
+
// If config changed, reload blocks list to update schema
|
|
936
|
+
if (data.configChanged && currentBlock && data.block === currentBlock.name) {
|
|
937
|
+
console.log('Config changed, reloading block data...');
|
|
938
|
+
await loadBlocks();
|
|
939
|
+
// Re-select current block to refresh properties sidebar
|
|
940
|
+
await selectBlock(currentBlock.name);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Reload preview iframe
|
|
944
|
+
const iframe = document.getElementById('preview-iframe');
|
|
945
|
+
if (iframe && (!data.block || data.block === currentBlock?.name)) {
|
|
946
|
+
iframe.src = iframe.src; // Force reload
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (data.type === 'newBlock') {
|
|
951
|
+
console.log('New block detected:', data.block);
|
|
952
|
+
await loadBlocks();
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
eventSource.onerror = () => {
|
|
957
|
+
console.error('SSE connection lost. Reconnecting...');
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Utility: Escape HTML
|
|
962
|
+
function escapeHtml(text) {
|
|
963
|
+
const div = document.createElement('div');
|
|
964
|
+
div.textContent = text;
|
|
965
|
+
return div.innerHTML;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Publish functionality
|
|
969
|
+
let workspacesCache = null;
|
|
970
|
+
|
|
971
|
+
async function loadWorkspaces() {
|
|
972
|
+
const select = document.getElementById('publish-workspace-id');
|
|
973
|
+
const errorDiv = document.getElementById('workspace-error');
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
// Use cached workspaces if available
|
|
977
|
+
if (workspacesCache) {
|
|
978
|
+
populateWorkspaceSelect(workspacesCache);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const response = await fetch('/api/workspaces');
|
|
983
|
+
|
|
984
|
+
if (!response.ok) {
|
|
985
|
+
const errorData = await response.json();
|
|
986
|
+
throw new Error(errorData.message || 'Failed to load workspaces');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const workspaces = await response.json();
|
|
990
|
+
workspacesCache = workspaces;
|
|
991
|
+
|
|
992
|
+
populateWorkspaceSelect(workspaces);
|
|
993
|
+
errorDiv.style.display = 'none';
|
|
994
|
+
} catch (error) {
|
|
995
|
+
console.error('Failed to load workspaces:', error);
|
|
996
|
+
select.innerHTML = '<option value="">Failed to load workspaces</option>';
|
|
997
|
+
errorDiv.textContent = error.message || 'Failed to load workspaces. Check your API token configuration.';
|
|
998
|
+
errorDiv.style.display = 'block';
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function populateWorkspaceSelect(workspaces) {
|
|
1003
|
+
const select = document.getElementById('publish-workspace-id');
|
|
1004
|
+
|
|
1005
|
+
if (workspaces.length === 0) {
|
|
1006
|
+
select.innerHTML = '<option value="">No workspaces found</option>';
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
select.innerHTML = '<option value="">Select a workspace</option>';
|
|
1011
|
+
workspaces.forEach(ws => {
|
|
1012
|
+
const option = document.createElement('option');
|
|
1013
|
+
option.value = ws.id;
|
|
1014
|
+
const roleName = ws.myRole?.name || 'member';
|
|
1015
|
+
option.textContent = `${ws.name} (${roleName})`;
|
|
1016
|
+
select.appendChild(option);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
window.openPublishModal = async function() {
|
|
1021
|
+
if (!currentBlock) return;
|
|
1022
|
+
|
|
1023
|
+
const modal = document.getElementById('publish-modal');
|
|
1024
|
+
const blockName = document.getElementById('publish-block-name');
|
|
1025
|
+
const localVersion = document.getElementById('publish-local-version');
|
|
1026
|
+
const publishedVersionRow = document.getElementById('publish-published-version-row');
|
|
1027
|
+
|
|
1028
|
+
blockName.textContent = currentBlock.displayName || currentBlock.name;
|
|
1029
|
+
localVersion.textContent = `v${currentBlock.version || '1.0.0'}`;
|
|
1030
|
+
|
|
1031
|
+
// Hide published version initially
|
|
1032
|
+
publishedVersionRow.style.display = 'none';
|
|
1033
|
+
|
|
1034
|
+
// Reset form
|
|
1035
|
+
document.getElementById('publish-target-marketplace').checked = true;
|
|
1036
|
+
document.getElementById('publish-workspace-id').value = '';
|
|
1037
|
+
document.getElementById('publish-version-bump').value = '';
|
|
1038
|
+
|
|
1039
|
+
// Load workspaces (will be shown when workspace target is selected)
|
|
1040
|
+
await loadWorkspaces();
|
|
1041
|
+
|
|
1042
|
+
// Show/hide workspace input
|
|
1043
|
+
toggleWorkspaceInput();
|
|
1044
|
+
|
|
1045
|
+
modal.classList.add('active');
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
window.closePublishModal = function() {
|
|
1049
|
+
const modal = document.getElementById('publish-modal');
|
|
1050
|
+
modal.classList.remove('active');
|
|
1051
|
+
|
|
1052
|
+
// Reset to form view
|
|
1053
|
+
document.getElementById('publish-form').style.display = 'block';
|
|
1054
|
+
document.getElementById('publish-progress').style.display = 'none';
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// Simple semver increment helper
|
|
1058
|
+
function incrementVersion(version, type) {
|
|
1059
|
+
const parts = version.split('.').map(Number);
|
|
1060
|
+
if (parts.length !== 3) return version;
|
|
1061
|
+
|
|
1062
|
+
const [major, minor, patch] = parts;
|
|
1063
|
+
|
|
1064
|
+
switch (type) {
|
|
1065
|
+
case 'patch':
|
|
1066
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
1067
|
+
case 'minor':
|
|
1068
|
+
return `${major}.${minor + 1}.0`;
|
|
1069
|
+
case 'major':
|
|
1070
|
+
return `${major + 1}.0.0`;
|
|
1071
|
+
default:
|
|
1072
|
+
return version;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function fetchPublishedVersion(workspaceId) {
|
|
1077
|
+
if (!currentBlock || !workspaceId) return;
|
|
1078
|
+
|
|
1079
|
+
const publishedVersionRow = document.getElementById('publish-published-version-row');
|
|
1080
|
+
const publishedVersionSpan = document.getElementById('publish-published-version');
|
|
1081
|
+
const versionBumpSelect = document.getElementById('publish-version-bump');
|
|
1082
|
+
|
|
1083
|
+
try {
|
|
1084
|
+
const response = await fetch(`/api/blocks/${currentBlock.name}/published-version?workspaceId=${workspaceId}`);
|
|
1085
|
+
const data = await response.json();
|
|
1086
|
+
|
|
1087
|
+
if (data.published && data.version) {
|
|
1088
|
+
publishedVersionSpan.textContent = `v${data.version}`;
|
|
1089
|
+
publishedVersionRow.style.display = 'block';
|
|
1090
|
+
|
|
1091
|
+
// Update version bump options with calculated versions
|
|
1092
|
+
const currentVer = data.version;
|
|
1093
|
+
const patchVer = incrementVersion(currentVer, 'patch');
|
|
1094
|
+
const minorVer = incrementVersion(currentVer, 'minor');
|
|
1095
|
+
const majorVer = incrementVersion(currentVer, 'major');
|
|
1096
|
+
|
|
1097
|
+
versionBumpSelect.innerHTML = `
|
|
1098
|
+
<option value="">No change</option>
|
|
1099
|
+
<option value="patch">Patch (${currentVer} → ${patchVer})</option>
|
|
1100
|
+
<option value="minor">Minor (${currentVer} → ${minorVer})</option>
|
|
1101
|
+
<option value="major">Major (${currentVer} → ${majorVer})</option>
|
|
1102
|
+
`;
|
|
1103
|
+
} else {
|
|
1104
|
+
publishedVersionSpan.textContent = 'Not published yet';
|
|
1105
|
+
publishedVersionRow.style.display = 'block';
|
|
1106
|
+
|
|
1107
|
+
// Use local version for initial publish
|
|
1108
|
+
const localVer = currentBlock.version || '1.0.0';
|
|
1109
|
+
versionBumpSelect.innerHTML = `
|
|
1110
|
+
<option value="">Use current version (${localVer})</option>
|
|
1111
|
+
<option value="patch">Patch (${localVer} → ${incrementVersion(localVer, 'patch')})</option>
|
|
1112
|
+
<option value="minor">Minor (${localVer} → ${incrementVersion(localVer, 'minor')})</option>
|
|
1113
|
+
<option value="major">Major (${localVer} → ${incrementVersion(localVer, 'major')})</option>
|
|
1114
|
+
`;
|
|
1115
|
+
}
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
console.error('Failed to fetch published version:', error);
|
|
1118
|
+
publishedVersionRow.style.display = 'none';
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
window.handleWorkspaceChange = function() {
|
|
1123
|
+
const workspaceId = document.getElementById('publish-workspace-id').value;
|
|
1124
|
+
if (workspaceId) {
|
|
1125
|
+
fetchPublishedVersion(workspaceId);
|
|
1126
|
+
} else {
|
|
1127
|
+
// Hide published version when no workspace selected
|
|
1128
|
+
document.getElementById('publish-published-version-row').style.display = 'none';
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
window.toggleWorkspaceInput = function() {
|
|
1133
|
+
const target = document.querySelector('input[name="publish-target"]:checked').value;
|
|
1134
|
+
const workspaceGroup = document.getElementById('workspace-id-group');
|
|
1135
|
+
const publishedVersionRow = document.getElementById('publish-published-version-row');
|
|
1136
|
+
|
|
1137
|
+
if (target === 'workspace') {
|
|
1138
|
+
workspaceGroup.style.display = 'block';
|
|
1139
|
+
} else {
|
|
1140
|
+
workspaceGroup.style.display = 'none';
|
|
1141
|
+
publishedVersionRow.style.display = 'none';
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
window.startPublish = async function() {
|
|
1146
|
+
if (!currentBlock) return;
|
|
1147
|
+
|
|
1148
|
+
const target = document.querySelector('input[name="publish-target"]:checked').value;
|
|
1149
|
+
const workspaceId = document.getElementById('publish-workspace-id').value;
|
|
1150
|
+
const versionBump = document.getElementById('publish-version-bump').value;
|
|
1151
|
+
|
|
1152
|
+
// Validate workspace ID if needed
|
|
1153
|
+
if (target === 'workspace' && !workspaceId) {
|
|
1154
|
+
alert('Please select a workspace from the dropdown');
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Show loading state
|
|
1159
|
+
document.getElementById('publish-form').style.display = 'none';
|
|
1160
|
+
const progressDiv = document.getElementById('publish-progress');
|
|
1161
|
+
progressDiv.style.display = 'block';
|
|
1162
|
+
progressDiv.innerHTML = `
|
|
1163
|
+
<div style="text-align: center; padding: 40px;">
|
|
1164
|
+
<div class="spinner" style="margin: 0 auto 16px;"></div>
|
|
1165
|
+
<div style="color: #666;">Publishing...</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
`;
|
|
1168
|
+
|
|
1169
|
+
try {
|
|
1170
|
+
const response = await fetch(`/api/blocks/${currentBlock.name}/publish`, {
|
|
1171
|
+
method: 'POST',
|
|
1172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1173
|
+
body: JSON.stringify({ target, workspaceId, versionBump })
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
const result = await response.json();
|
|
1177
|
+
|
|
1178
|
+
if (result.success) {
|
|
1179
|
+
// Update local block version
|
|
1180
|
+
currentBlock.version = result.version;
|
|
1181
|
+
renderBlocksList();
|
|
1182
|
+
|
|
1183
|
+
progressDiv.innerHTML = `
|
|
1184
|
+
<div style="text-align: center; padding: 40px;">
|
|
1185
|
+
<div style="font-size: 48px; margin-bottom: 16px;">✓</div>
|
|
1186
|
+
<div style="font-size: 18px; font-weight: 600; color: #22c55e; margin-bottom: 8px;">
|
|
1187
|
+
${escapeHtml(result.message)}
|
|
1188
|
+
</div>
|
|
1189
|
+
${result.version ? `<div style="color: #666;">Version: ${result.version}</div>` : ''}
|
|
1190
|
+
<button class="btn btn-primary" onclick="closePublishModal()" style="margin-top: 24px;">Done</button>
|
|
1191
|
+
</div>
|
|
1192
|
+
`;
|
|
1193
|
+
} else {
|
|
1194
|
+
throw new Error(result.error || 'Publish failed');
|
|
1195
|
+
}
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
console.error('Publish failed:', error);
|
|
1198
|
+
progressDiv.innerHTML = `
|
|
1199
|
+
<div style="text-align: center; padding: 40px;">
|
|
1200
|
+
<div style="font-size: 48px; margin-bottom: 16px; color: #ef4444;">✗</div>
|
|
1201
|
+
<div style="font-size: 18px; font-weight: 600; color: #ef4444; margin-bottom: 8px;">Publish Failed</div>
|
|
1202
|
+
<div style="color: #666; margin-bottom: 24px;">${escapeHtml(error.message)}</div>
|
|
1203
|
+
<button class="btn btn-secondary" onclick="closePublishModal()">Close</button>
|
|
1204
|
+
</div>
|
|
1205
|
+
`;
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
// Panel Toggle Functionality - Collapsed Sidebar
|
|
1210
|
+
let leftPanelCollapsed = false;
|
|
1211
|
+
let rightPanelCollapsed = false;
|
|
1212
|
+
|
|
1213
|
+
window.toggleLeftPanel = function() {
|
|
1214
|
+
const container = document.getElementById('container');
|
|
1215
|
+
const toggleBtn = document.getElementById('toggle-left');
|
|
1216
|
+
|
|
1217
|
+
leftPanelCollapsed = !leftPanelCollapsed;
|
|
1218
|
+
|
|
1219
|
+
if (leftPanelCollapsed) {
|
|
1220
|
+
container.classList.add('left-collapsed');
|
|
1221
|
+
toggleBtn.setAttribute('title', 'Expand panel (Ctrl+B)');
|
|
1222
|
+
} else {
|
|
1223
|
+
container.classList.remove('left-collapsed');
|
|
1224
|
+
toggleBtn.setAttribute('title', 'Collapse panel (Ctrl+B)');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Save preference
|
|
1228
|
+
localStorage.setItem('leftPanelCollapsed', leftPanelCollapsed);
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
window.toggleRightPanel = function() {
|
|
1232
|
+
const container = document.getElementById('container');
|
|
1233
|
+
const toggleBtn = document.getElementById('toggle-right');
|
|
1234
|
+
|
|
1235
|
+
rightPanelCollapsed = !rightPanelCollapsed;
|
|
1236
|
+
|
|
1237
|
+
if (rightPanelCollapsed) {
|
|
1238
|
+
container.classList.add('right-collapsed');
|
|
1239
|
+
toggleBtn.setAttribute('title', 'Expand panel (Ctrl+E)');
|
|
1240
|
+
} else {
|
|
1241
|
+
container.classList.remove('right-collapsed');
|
|
1242
|
+
toggleBtn.setAttribute('title', 'Collapse panel (Ctrl+E)');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Save preference
|
|
1246
|
+
localStorage.setItem('rightPanelCollapsed', rightPanelCollapsed);
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
// Restore panel states from localStorage
|
|
1250
|
+
function restorePanelStates() {
|
|
1251
|
+
const savedLeftState = localStorage.getItem('leftPanelCollapsed');
|
|
1252
|
+
const savedRightState = localStorage.getItem('rightPanelCollapsed');
|
|
1253
|
+
|
|
1254
|
+
if (savedLeftState === 'true') {
|
|
1255
|
+
leftPanelCollapsed = false; // Set to false so toggle will flip it
|
|
1256
|
+
toggleLeftPanel();
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (savedRightState === 'true') {
|
|
1260
|
+
rightPanelCollapsed = false; // Set to false so toggle will flip it
|
|
1261
|
+
toggleRightPanel();
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Keyboard shortcuts
|
|
1266
|
+
document.addEventListener('keydown', (event) => {
|
|
1267
|
+
// Ctrl+B (or Cmd+B on Mac) - Toggle left panel
|
|
1268
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
|
|
1269
|
+
event.preventDefault();
|
|
1270
|
+
toggleLeftPanel();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Ctrl+E (or Cmd+E on Mac) - Toggle right panel
|
|
1274
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
|
|
1275
|
+
event.preventDefault();
|
|
1276
|
+
toggleRightPanel();
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// Start the app
|
|
1281
|
+
init();
|
|
1282
|
+
|
|
1283
|
+
// Restore panel states after init
|
|
1284
|
+
setTimeout(restorePanelStates, 100);
|