@lightspeed/crane 2.0.2 → 2.0.4
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/CHANGELOG.md +25 -0
- package/dist/cli.mjs +37 -21
- package/package.json +3 -2
- package/template/headers/example-header/showcases/2.ts +0 -3
- package/template/page-templates/example-template/pages/custom.ts +6 -0
- package/template/preview/shared/api-routes.ts +337 -11
- package/template/preview/shared/preview.ts +30 -14
- package/template/preview/shared/utils.ts +0 -1
- package/template/preview/vite.config.js +5 -0
- package/template/reference/sections/about-us/settings/content.ts +28 -21
- package/template/reference/sections/about-us/settings/design.ts +1 -1
- package/template/reference/sections/about-us/showcases/1.ts +1 -0
- package/template/reference/sections/about-us/showcases/2.ts +1 -0
- package/template/reference/sections/intro-slider/settings/content.ts +5 -1
- package/template/reference/sections/intro-slider/settings/design.ts +1 -1
- package/template/reference/sections/intro-slider/showcases/1.ts +1 -0
- package/template/reference/sections/intro-slider/showcases/2.ts +1 -0
- package/template/sections/example-section/settings/content.ts +15 -13
- package/template/sections/example-section/showcases/1.ts +0 -1
- package/template/sections/example-section/showcases/2.ts +0 -1
- package/template/sections/example-section/showcases/3.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightspeed/crane",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": "bin/crane.js",
|
|
6
6
|
"main": "./dist/app.mjs",
|
|
@@ -64,8 +64,9 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@jridgewell/sourcemap-codec": "^1.5.4",
|
|
67
|
-
"@lightspeed/crane-api": "1.0
|
|
67
|
+
"@lightspeed/crane-api": "1.1.0",
|
|
68
68
|
"@lightspeed/eslint-config-crane": "1.1.3",
|
|
69
|
+
"@types/micromatch": "^4.0.8",
|
|
69
70
|
"@types/prompts": "^2.4.2",
|
|
70
71
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
71
72
|
"adm-zip": "^0.5.16",
|
|
@@ -3,9 +3,54 @@ import type { IncomingMessage, ServerResponse } from 'http';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { URL } from 'url';
|
|
5
5
|
|
|
6
|
+
import type {
|
|
7
|
+
ContentEditor,
|
|
8
|
+
ContentSettings,
|
|
9
|
+
DeckContentEditor,
|
|
10
|
+
DesignSettings,
|
|
11
|
+
LayoutSettings,
|
|
12
|
+
} from '@lightspeed/crane-api';
|
|
13
|
+
|
|
6
14
|
import { getShowcaseData } from './preview';
|
|
7
15
|
import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
|
|
8
16
|
|
|
17
|
+
/**
|
|
18
|
+
* AppBlock type definition for block-config response
|
|
19
|
+
*/
|
|
20
|
+
interface AppBlock {
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
name: string;
|
|
24
|
+
contentEditors: ContentSettings;
|
|
25
|
+
designEditors: DesignSettings;
|
|
26
|
+
layouts: LayoutSettings[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Layout config item for the config.layoutConfigList
|
|
31
|
+
*/
|
|
32
|
+
interface LayoutConfigItem {
|
|
33
|
+
name: string;
|
|
34
|
+
contentEditorConfig: {
|
|
35
|
+
// mainEditors contains all fields from each editor definition
|
|
36
|
+
// with type transformed and translations extracted to English
|
|
37
|
+
mainEditors: Record<string, unknown>[];
|
|
38
|
+
buttons: unknown[];
|
|
39
|
+
};
|
|
40
|
+
designEditorConfig: {
|
|
41
|
+
// mainEditors contains all fields from each editor definition
|
|
42
|
+
// with type transformed and translations extracted to English
|
|
43
|
+
mainEditors: Record<string, unknown>[];
|
|
44
|
+
customEditors: unknown[];
|
|
45
|
+
buttons: unknown[];
|
|
46
|
+
designEditorGroups: unknown[];
|
|
47
|
+
};
|
|
48
|
+
defaults: Record<string, unknown>;
|
|
49
|
+
deprecated: boolean;
|
|
50
|
+
svgIcon: string;
|
|
51
|
+
svgIconText: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
9
54
|
|
|
10
55
|
/**
|
|
11
56
|
* Extract query parameter from URL
|
|
@@ -52,6 +97,282 @@ function setNoCacheHeaders(res: ServerResponse): void {
|
|
|
52
97
|
res.setHeader('Surrogate-Control', 'no-store');
|
|
53
98
|
}
|
|
54
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Check if an object is a translation object (has 'en' key with string value)
|
|
102
|
+
*/
|
|
103
|
+
function isTranslationObject(obj: unknown): obj is Record<string, string> {
|
|
104
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false;
|
|
105
|
+
const record = obj as Record<string, unknown>;
|
|
106
|
+
return 'en' in record && typeof record.en === 'string';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Recursively process a value and extract English translations where applicable
|
|
111
|
+
* - If the value is a translation object (has 'en' key), return the English value
|
|
112
|
+
* - If the value is an array, process each element
|
|
113
|
+
* - If the value is an object, process each property
|
|
114
|
+
* - Otherwise, return the value as-is
|
|
115
|
+
*/
|
|
116
|
+
function extractEnglishTranslations(value: unknown): unknown {
|
|
117
|
+
if (value === null || value === undefined) return value;
|
|
118
|
+
|
|
119
|
+
// Check if it's a translation object
|
|
120
|
+
if (isTranslationObject(value)) {
|
|
121
|
+
return value.en;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle arrays
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
return value.map(item => extractEnglishTranslations(item));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle objects (but not translation objects, which we already handled)
|
|
130
|
+
if (typeof value === 'object') {
|
|
131
|
+
const result: Record<string, unknown> = {};
|
|
132
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
133
|
+
result[key] = extractEnglishTranslations(val);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Return primitives as-is
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Transform editor type to the expected format
|
|
144
|
+
* INPUTBOX -> TEXT_EDITOR
|
|
145
|
+
* TEXTAREA -> MULTILINE_TEXT_EDITOR
|
|
146
|
+
* All other types -> {TYPE}_EDITOR (e.g., DECK -> DECK_EDITOR)
|
|
147
|
+
*/
|
|
148
|
+
function transformEditorType(type: string): string {
|
|
149
|
+
const upperType = type.toUpperCase();
|
|
150
|
+
switch (upperType) {
|
|
151
|
+
case 'INPUTBOX':
|
|
152
|
+
return 'TEXT_EDITOR';
|
|
153
|
+
case 'TEXTAREA':
|
|
154
|
+
return 'MULTILINE_TEXT_EDITOR';
|
|
155
|
+
case 'BUTTON':
|
|
156
|
+
return 'ACTION_LINK_EDITOR';
|
|
157
|
+
default:
|
|
158
|
+
return `${upperType}_EDITOR`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function processEditorExceptDeck(setting: ContentEditor, fieldName: string) {
|
|
163
|
+
const result: Record<string, unknown> = {
|
|
164
|
+
field: fieldName,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
for (const [key, value] of Object.entries(setting)) {
|
|
168
|
+
if (key === 'type') {
|
|
169
|
+
result.type = transformEditorType(value as string);
|
|
170
|
+
} else {
|
|
171
|
+
result[key] = extractEnglishTranslations(value);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Process a DECK editor: transform cards.defaultCardContent.settings into editors array
|
|
179
|
+
*/
|
|
180
|
+
function processDeckEditor(editor: DeckContentEditor, fieldName: string): Record<string, unknown> {
|
|
181
|
+
const result: Record<string, unknown> = {
|
|
182
|
+
field: fieldName,
|
|
183
|
+
type: 'DECK_EDITOR',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Process cards to extract editors and defaultCard
|
|
187
|
+
const cards = editor.cards as Record<string, unknown> | undefined;
|
|
188
|
+
if (cards && cards.defaultCardContent) {
|
|
189
|
+
const defaultCardContent = cards.defaultCardContent as Record<string, unknown>;
|
|
190
|
+
const settings = defaultCardContent.settings as ContentSettings;
|
|
191
|
+
|
|
192
|
+
// Create defaultCard with label and editors
|
|
193
|
+
const defaultCard: Record<string, unknown> = {
|
|
194
|
+
label: extractEnglishTranslations(defaultCardContent.label),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Create editors array from settings and add to defaultCard
|
|
198
|
+
if (settings) {
|
|
199
|
+
const editors: Record<string, unknown>[] = [];
|
|
200
|
+
for (const [settingFieldName, settingEditor] of Object.entries(settings)) {
|
|
201
|
+
const processedEditor: Record<string, unknown> = processEditorExceptDeck(settingEditor, settingFieldName);
|
|
202
|
+
editors.push(processedEditor);
|
|
203
|
+
}
|
|
204
|
+
defaultCard.editors = editors;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
result.defaultCard = defaultCard;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Copy other fields (maxCards, addButtonLabel, label, etc.) but skip 'cards' and 'type'
|
|
211
|
+
for (const [key, value] of Object.entries(editor)) {
|
|
212
|
+
if (key === 'type' || key === 'cards') {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
result[key] = extractEnglishTranslations(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Process an editor definition: keep all fields, transform type, extract English translations
|
|
223
|
+
* For DECK type, special processing is applied to create editors array from cards.defaultCardContent.settings
|
|
224
|
+
*/
|
|
225
|
+
function processEditor(editor: ContentEditor, fieldName: string): Record<string, unknown> {
|
|
226
|
+
const editorType = (editor.type as string || '').toUpperCase();
|
|
227
|
+
|
|
228
|
+
// Special handling for DECK type
|
|
229
|
+
if (editorType === 'DECK') {
|
|
230
|
+
return processDeckEditor(editor as DeckContentEditor, fieldName);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return processEditorExceptDeck(editor, fieldName);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build layout config from appBlock data
|
|
239
|
+
* Creates the config.layoutConfigList structure from appBlock's layouts, contentEditors, and designEditors
|
|
240
|
+
*/
|
|
241
|
+
function buildLayoutConfig(appBlock: AppBlock): { type: string; layoutConfigList: LayoutConfigItem[] } {
|
|
242
|
+
const { layouts, contentEditors, designEditors } = appBlock;
|
|
243
|
+
|
|
244
|
+
// contentEditors and designEditors are objects with field names as keys
|
|
245
|
+
const contentEditorKeys = Object.keys(contentEditors);
|
|
246
|
+
const designEditorKeys = Object.keys(designEditors);
|
|
247
|
+
|
|
248
|
+
const layoutConfigList = layouts.map((layout, index) => {
|
|
249
|
+
const { layoutId, selectedContentSettings, selectedDesignSettings } = layout;
|
|
250
|
+
|
|
251
|
+
const contentFieldsToInclude = selectedContentSettings.length === 0
|
|
252
|
+
? contentEditorKeys
|
|
253
|
+
: selectedContentSettings;
|
|
254
|
+
|
|
255
|
+
const contentMainEditors = contentFieldsToInclude
|
|
256
|
+
.filter((fieldName) => {
|
|
257
|
+
const editor = contentEditors[fieldName];
|
|
258
|
+
return editor && editor.type;
|
|
259
|
+
})
|
|
260
|
+
.map((fieldName) => {
|
|
261
|
+
const editor = contentEditors[fieldName];
|
|
262
|
+
return processEditor(editor as ContentEditor, fieldName);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Determine which design fields to include
|
|
266
|
+
const designFieldsToInclude = selectedDesignSettings.length === 0
|
|
267
|
+
? designEditorKeys
|
|
268
|
+
: selectedDesignSettings.map(s => s.fieldName);
|
|
269
|
+
|
|
270
|
+
// Build design mainEditors - include all design editor types with all their fields
|
|
271
|
+
const designMainEditors = designFieldsToInclude
|
|
272
|
+
.filter((fieldName) => {
|
|
273
|
+
const editor = designEditors[fieldName];
|
|
274
|
+
return editor && editor.type;
|
|
275
|
+
})
|
|
276
|
+
.map((fieldName) => {
|
|
277
|
+
const editor = designEditors[fieldName];
|
|
278
|
+
return processEditor(editor as ContentEditor, fieldName);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Build defaults from design editors (extract English translations)
|
|
282
|
+
const defaults: Record<string, unknown> = {};
|
|
283
|
+
designFieldsToInclude.forEach((fieldName) => {
|
|
284
|
+
const editor = designEditors[fieldName];
|
|
285
|
+
if (editor && editor.defaults) {
|
|
286
|
+
defaults[fieldName] = extractEnglishTranslations(editor.defaults);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Apply layout-specific design overrides from selectedDesignSettings
|
|
291
|
+
selectedDesignSettings.forEach((setting) => {
|
|
292
|
+
if (typeof setting === 'object' && setting.fieldName && setting.defaults) {
|
|
293
|
+
defaults[setting.fieldName] = {
|
|
294
|
+
...(defaults[setting.fieldName] as Record<string, unknown> || {}),
|
|
295
|
+
...(extractEnglishTranslations(setting.defaults) as Record<string, unknown>),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
name: layoutId,
|
|
302
|
+
contentEditorConfig: {
|
|
303
|
+
mainEditors: contentMainEditors,
|
|
304
|
+
buttons: [],
|
|
305
|
+
},
|
|
306
|
+
designEditorConfig: {
|
|
307
|
+
mainEditors: designMainEditors,
|
|
308
|
+
customEditors: [],
|
|
309
|
+
buttons: [],
|
|
310
|
+
designEditorGroups: [],
|
|
311
|
+
},
|
|
312
|
+
defaults,
|
|
313
|
+
deprecated: false,
|
|
314
|
+
svgIcon: 'CustomAutomaticLayoutIcon',
|
|
315
|
+
svgIconText: String(index + 1),
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
type: 'CUSTOM',
|
|
321
|
+
layoutConfigList,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Handle GET /api/v1/block-config-full
|
|
327
|
+
* Fetches appBlock from /api/v1/block-config and adds config.layoutConfigList
|
|
328
|
+
*/
|
|
329
|
+
async function handleGetBlockConfigFull(
|
|
330
|
+
req: IncomingMessage,
|
|
331
|
+
res: ServerResponse,
|
|
332
|
+
sectionName: string,
|
|
333
|
+
typeParam: string,
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
try {
|
|
336
|
+
// Fetch appBlock from the CLI's /api/v1/block-config endpoint
|
|
337
|
+
const host = req.headers.host || 'localhost:5173';
|
|
338
|
+
const protocol = 'http';
|
|
339
|
+
const blockConfigUrl = `${protocol}://${host}/api/v1/block-config?section=${encodeURIComponent(sectionName)}&type=${encodeURIComponent(typeParam)}`;
|
|
340
|
+
|
|
341
|
+
const response = await fetch(blockConfigUrl);
|
|
342
|
+
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
const errorText = await response.text();
|
|
345
|
+
res.statusCode = response.status;
|
|
346
|
+
res.setHeader('Content-Type', 'application/json');
|
|
347
|
+
setNoCacheHeaders(res);
|
|
348
|
+
res.end(errorText);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const appBlock = await response.json() as AppBlock;
|
|
353
|
+
|
|
354
|
+
// Build the config with layoutConfigList
|
|
355
|
+
const config = buildLayoutConfig(appBlock);
|
|
356
|
+
|
|
357
|
+
// Return appBlock with config added
|
|
358
|
+
res.statusCode = 200;
|
|
359
|
+
res.setHeader('Content-Type', 'application/json');
|
|
360
|
+
setNoCacheHeaders(res);
|
|
361
|
+
res.end(JSON.stringify({
|
|
362
|
+
config,
|
|
363
|
+
}, null, 2));
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('[API Routes] ❌ Error fetching block config:', error);
|
|
366
|
+
res.statusCode = 500;
|
|
367
|
+
res.setHeader('Content-Type', 'application/json');
|
|
368
|
+
setNoCacheHeaders(res);
|
|
369
|
+
res.end(JSON.stringify({
|
|
370
|
+
error: 'Failed to fetch block config',
|
|
371
|
+
message: error instanceof Error ? error.message : String(error),
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
55
376
|
/**
|
|
56
377
|
* Get all available showcases for a section from the dist folder
|
|
57
378
|
*/
|
|
@@ -67,15 +388,8 @@ function getAvailableShowcases(sectionName: string, distPath: string): string[]
|
|
|
67
388
|
const showcases = fs.readdirSync(showcasesPath, { withFileTypes: true })
|
|
68
389
|
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.mjs'))
|
|
69
390
|
.map(dirent => dirent.name.replace('.mjs', ''))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const aNum = parseInt(a, 10);
|
|
73
|
-
const bNum = parseInt(b, 10);
|
|
74
|
-
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
75
|
-
return aNum - bNum;
|
|
76
|
-
}
|
|
77
|
-
return a.localeCompare(b);
|
|
78
|
-
});
|
|
391
|
+
// Filter out non-showcase files like 'translations'
|
|
392
|
+
.filter(name => name !== 'translations');
|
|
79
393
|
|
|
80
394
|
return showcases;
|
|
81
395
|
} catch (error) {
|
|
@@ -124,7 +438,6 @@ async function handleGetTile(
|
|
|
124
438
|
});
|
|
125
439
|
|
|
126
440
|
const allShowcaseData = await Promise.all(showcaseDataPromises);
|
|
127
|
-
|
|
128
441
|
for (const { sectionName, showcaseId, content, design } of allShowcaseData) {
|
|
129
442
|
responseData = updateTilesSection(responseData, sectionName, showcaseId, content, design);
|
|
130
443
|
}
|
|
@@ -159,7 +472,14 @@ async function handleUpdateTile(
|
|
|
159
472
|
): Promise<void> {
|
|
160
473
|
const requestBody = await readRequestBody(req);
|
|
161
474
|
try {
|
|
162
|
-
//
|
|
475
|
+
// If URL matches tile/custom-* pattern, return request body as-is
|
|
476
|
+
if (originalUrl && /\/tile\/custom-[a-zA-Z0-9]+/.test(originalUrl)) {
|
|
477
|
+
res.statusCode = 200;
|
|
478
|
+
res.setHeader('Content-Type', 'application/json');
|
|
479
|
+
setNoCacheHeaders(res);
|
|
480
|
+
res.end(JSON.stringify(requestBody, null, 2));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
163
483
|
|
|
164
484
|
if (!requestBody || !Array.isArray(requestBody.tiles)) {
|
|
165
485
|
res.statusCode = 400;
|
|
@@ -583,6 +903,12 @@ export function handleApiRequest(
|
|
|
583
903
|
return;
|
|
584
904
|
}
|
|
585
905
|
|
|
906
|
+
if (url.startsWith('/api/v1/block-config-full')) {
|
|
907
|
+
const typeParam = getQueryParam(url, 'type') || 'SECTION';
|
|
908
|
+
handleGetBlockConfigFull(req, res, sectionName, typeParam);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
586
912
|
if (url.startsWith('/api/v1/client-js')) {
|
|
587
913
|
handleGetClientJs(req, res, sectionName);
|
|
588
914
|
return;
|
|
@@ -264,9 +264,12 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
|
|
|
264
264
|
return overridenDesign;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
function processImage(component: any, sectionName: string, key: string): Record<string, any> {
|
|
268
|
-
const assetLocation =
|
|
269
|
-
|
|
267
|
+
function processImage(component: any, sectionName: string, key: string, isApiRender: boolean = false): Record<string, any> {
|
|
268
|
+
const assetLocation = isApiRender ?
|
|
269
|
+
`http://${distFolderPath}/sections/${sectionName}/assets/` :
|
|
270
|
+
`${distFolderPath}/sections/${sectionName}/assets/`;
|
|
271
|
+
|
|
272
|
+
const set = component.defaults?.imageData?.set || component.imageData?.set;
|
|
270
273
|
const newSet = {
|
|
271
274
|
'cropped-webp-100x200': { url: assetLocation + set.MOBILE_WEBP_LOW_RES.url },
|
|
272
275
|
'cropped-webp-1000x2000': { url: assetLocation + set.MOBILE_WEBP_HI_RES.url },
|
|
@@ -282,7 +285,8 @@ function processImage(component: any, sectionName: string, key: string): Record<
|
|
|
282
285
|
};
|
|
283
286
|
}
|
|
284
287
|
|
|
285
|
-
function processComponent(key: string, component: any, translations: any, sectionName?: string
|
|
288
|
+
function processComponent(key: string, component: any, translations: any, sectionName?: string,
|
|
289
|
+
isApiRender: boolean = false, extraParams?: Record<string, any>): any {
|
|
286
290
|
if (!component?.type) return {};
|
|
287
291
|
switch (component.type) {
|
|
288
292
|
case 'INPUTBOX':
|
|
@@ -297,16 +301,16 @@ function processComponent(key: string, component: any, translations: any, sectio
|
|
|
297
301
|
if (defaultSettings) {
|
|
298
302
|
const result: Record<string, any> = {};
|
|
299
303
|
Object.entries(defaultSettings).forEach(([k, c]) => {
|
|
300
|
-
Object.assign(result, processComponent(k, c, translations));
|
|
304
|
+
Object.assign(result, processComponent(k, c, translations, sectionName, isApiRender));
|
|
301
305
|
});
|
|
302
306
|
return { [key]: result };
|
|
303
307
|
} else {
|
|
304
|
-
const cards = component.cards.map((card: any) => {
|
|
308
|
+
const cards = component.cards.map((card: any, index: number) => {
|
|
305
309
|
const cardContent: Record<string, any> = {};
|
|
306
310
|
Object.entries(card.settings).forEach(([k, c]) => {
|
|
307
|
-
Object.assign(cardContent, processComponent(k, c, translations, sectionName));
|
|
311
|
+
Object.assign(cardContent, processComponent(k, c, translations, sectionName, isApiRender));
|
|
308
312
|
});
|
|
309
|
-
return { settings: cardContent };
|
|
313
|
+
return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel};
|
|
310
314
|
});
|
|
311
315
|
return { [key]: { cards } };
|
|
312
316
|
}
|
|
@@ -321,12 +325,17 @@ function processComponent(key: string, component: any, translations: any, sectio
|
|
|
321
325
|
},
|
|
322
326
|
};
|
|
323
327
|
}
|
|
324
|
-
case 'TOGGLE':
|
|
325
|
-
return {
|
|
328
|
+
case 'TOGGLE': {
|
|
329
|
+
return {
|
|
330
|
+
[key]: {
|
|
331
|
+
enabled: component.defaults?.enabled ?? component.enabled
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
326
335
|
case 'SELECTBOX':
|
|
327
|
-
return { [key]: component.defaults.value };
|
|
336
|
+
return { [key]: component.defaults?.value ?? component.value };
|
|
328
337
|
case 'IMAGE':
|
|
329
|
-
return processImage(component, sectionName!, key);
|
|
338
|
+
return processImage(component, sectionName!, key, isApiRender);
|
|
330
339
|
case 'INFO':
|
|
331
340
|
return {
|
|
332
341
|
[key]: {
|
|
@@ -349,13 +358,19 @@ function getContentToRender(
|
|
|
349
358
|
contentTranslations: any,
|
|
350
359
|
showcaseTranslations: any,
|
|
351
360
|
sectionName: string,
|
|
361
|
+
isApiRender: boolean = false,
|
|
352
362
|
): any {
|
|
363
|
+
let deckCardLabel: string | undefined = undefined;
|
|
353
364
|
const parsedContent = Object.entries(content).reduce((acc, [k, c]) => {
|
|
354
|
-
|
|
365
|
+
if ((c as any).type === 'DECK') {
|
|
366
|
+
deckCardLabel = contentTranslations[(c as any).cards?.defaultCardContent?.label];
|
|
367
|
+
}
|
|
368
|
+
return { ...acc, ...processComponent(k, c, contentTranslations, sectionName, isApiRender) };
|
|
355
369
|
}, {});
|
|
356
370
|
|
|
371
|
+
|
|
357
372
|
const parsedShowcase = Object.entries(showcase).reduce((acc, [k, c]) => {
|
|
358
|
-
return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName) };
|
|
373
|
+
return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName, isApiRender, {deckCardLabel}) };
|
|
359
374
|
}, {});
|
|
360
375
|
|
|
361
376
|
return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
|
|
@@ -471,6 +486,7 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
|
|
|
471
486
|
(contentTranslations as any).default.en,
|
|
472
487
|
(showcaseTranslations as any).default.en,
|
|
473
488
|
sectionName,
|
|
489
|
+
true
|
|
474
490
|
);
|
|
475
491
|
|
|
476
492
|
return {
|
|
@@ -98,6 +98,11 @@ export default defineConfig({
|
|
|
98
98
|
|
|
99
99
|
// Handle API routes
|
|
100
100
|
if (url.startsWith('/api/v1/')) {
|
|
101
|
+
// Skip block-config route (but not block-config-full) - handled by CLI middleware
|
|
102
|
+
if (url.startsWith('/api/v1/block-config') && !url.startsWith('/api/v1/block-config-full')) {
|
|
103
|
+
return next();
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
// Extract auth header from request
|
|
102
107
|
const authHeader = req.headers['authorization'] || '';
|
|
103
108
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export default {
|
|
2
2
|
title: {
|
|
3
3
|
type: 'INPUTBOX',
|
|
4
4
|
label: '$label.section_title.label',
|
|
@@ -17,15 +17,17 @@ const content: ContentEditor = {
|
|
|
17
17
|
type: 'INPUTBOX',
|
|
18
18
|
label: '$label.stat_value.label',
|
|
19
19
|
placeholder: '$label.stat_value.placeholder',
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
defaults: {
|
|
21
|
+
text: '$label.stat_value.default.text',
|
|
22
|
+
},
|
|
22
23
|
},
|
|
23
24
|
stat_caption: {
|
|
24
25
|
type: 'INPUTBOX',
|
|
25
26
|
label: '$label.stat_caption.label',
|
|
26
27
|
placeholder: '$label.stat_caption.placeholder',
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
defaults: {
|
|
29
|
+
text: '$label.stat_caption.default.text',
|
|
30
|
+
},
|
|
29
31
|
},
|
|
30
32
|
},
|
|
31
33
|
},
|
|
@@ -34,28 +36,33 @@ const content: ContentEditor = {
|
|
|
34
36
|
button: {
|
|
35
37
|
type: 'BUTTON',
|
|
36
38
|
label: '$label.button_content.label',
|
|
37
|
-
|
|
39
|
+
defaults: {
|
|
40
|
+
title: '$label.button_content.title',
|
|
41
|
+
buttonType: 'HYPER_LINK',
|
|
42
|
+
link: 'https://www.example.com',
|
|
43
|
+
},
|
|
38
44
|
},
|
|
39
45
|
image: {
|
|
40
46
|
type: 'IMAGE',
|
|
41
47
|
label: '$label.image_content.label',
|
|
42
48
|
defaults: {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
imageData: {
|
|
50
|
+
set: {
|
|
51
|
+
MOBILE_WEBP_LOW_RES: {
|
|
52
|
+
url: 'our_company_in_numbers.jpg',
|
|
53
|
+
},
|
|
54
|
+
MOBILE_WEBP_HI_RES: {
|
|
55
|
+
url: 'our_company_in_numbers.jpg',
|
|
56
|
+
},
|
|
57
|
+
WEBP_LOW_RES: {
|
|
58
|
+
url: 'our_company_in_numbers.jpg',
|
|
59
|
+
},
|
|
60
|
+
WEBP_HI_2X_RES: {
|
|
61
|
+
url: 'our_company_in_numbers.jpg',
|
|
62
|
+
},
|
|
55
63
|
},
|
|
64
|
+
borderInfo: {},
|
|
56
65
|
},
|
|
57
|
-
borderInfo: {},
|
|
58
66
|
},
|
|
59
67
|
},
|
|
60
|
-
};
|
|
61
|
-
export default content;
|
|
68
|
+
} as const;
|
|
@@ -17,7 +17,11 @@ export default {
|
|
|
17
17
|
section_button: {
|
|
18
18
|
type: 'BUTTON',
|
|
19
19
|
label: '$label.section_button.label',
|
|
20
|
-
|
|
20
|
+
defaults: {
|
|
21
|
+
title: '$label.section_button.title',
|
|
22
|
+
buttonType: 'HYPER_LINK',
|
|
23
|
+
link: 'https://www.example.com',
|
|
24
|
+
},
|
|
21
25
|
},
|
|
22
26
|
slider: {
|
|
23
27
|
type: 'DECK',
|