@lightspeed/crane 2.0.3 → 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 +18 -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 +22 -11
- 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
|
}
|
|
@@ -331,7 +335,7 @@ function processComponent(key: string, component: any, translations: any, sectio
|
|
|
331
335
|
case 'SELECTBOX':
|
|
332
336
|
return { [key]: component.defaults?.value ?? component.value };
|
|
333
337
|
case 'IMAGE':
|
|
334
|
-
return processImage(component, sectionName!, key);
|
|
338
|
+
return processImage(component, sectionName!, key, isApiRender);
|
|
335
339
|
case 'INFO':
|
|
336
340
|
return {
|
|
337
341
|
[key]: {
|
|
@@ -354,13 +358,19 @@ function getContentToRender(
|
|
|
354
358
|
contentTranslations: any,
|
|
355
359
|
showcaseTranslations: any,
|
|
356
360
|
sectionName: string,
|
|
361
|
+
isApiRender: boolean = false,
|
|
357
362
|
): any {
|
|
363
|
+
let deckCardLabel: string | undefined = undefined;
|
|
358
364
|
const parsedContent = Object.entries(content).reduce((acc, [k, c]) => {
|
|
359
|
-
|
|
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) };
|
|
360
369
|
}, {});
|
|
361
370
|
|
|
371
|
+
|
|
362
372
|
const parsedShowcase = Object.entries(showcase).reduce((acc, [k, c]) => {
|
|
363
|
-
return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName) };
|
|
373
|
+
return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName, isApiRender, {deckCardLabel}) };
|
|
364
374
|
}, {});
|
|
365
375
|
|
|
366
376
|
return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
|
|
@@ -476,6 +486,7 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
|
|
|
476
486
|
(contentTranslations as any).default.en,
|
|
477
487
|
(showcaseTranslations as any).default.en,
|
|
478
488
|
sectionName,
|
|
489
|
+
true
|
|
479
490
|
);
|
|
480
491
|
|
|
481
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',
|