@loom-framework/core 0.1.0-alpha.80 → 0.1.0-alpha.81
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/dist/cli/commands/generate-dashboard.d.ts +9 -0
- package/dist/cli/commands/generate-dashboard.d.ts.map +1 -0
- package/dist/cli/commands/generate-dashboard.js +452 -0
- package/dist/cli/commands/generate-dashboard.js.map +1 -0
- package/dist/cli/commands/generate-page.d.ts.map +1 -1
- package/dist/cli/commands/generate-page.js +1 -124
- package/dist/cli/commands/generate-page.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/helpers/app-tsx-wiring.d.ts +17 -0
- package/dist/cli/helpers/app-tsx-wiring.d.ts.map +1 -0
- package/dist/cli/helpers/app-tsx-wiring.js +132 -0
- package/dist/cli/helpers/app-tsx-wiring.js.map +1 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/loom-skill/SKILL.md +17 -4
- package/templates/loom-skill/references/dashboard.md +161 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom generate dashboard <name>
|
|
3
|
+
*
|
|
4
|
+
* Reads dashboard.config.json from the project root,
|
|
5
|
+
* generates a Dashboard page with G2 charts.
|
|
6
|
+
*/
|
|
7
|
+
import type { Command } from 'commander';
|
|
8
|
+
export declare function registerGenerateDashboardCommand(program: Command): void;
|
|
9
|
+
//# sourceMappingURL=generate-dashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-dashboard.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/generate-dashboard.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiZzC,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2FvE"}
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom generate dashboard <name>
|
|
3
|
+
*
|
|
4
|
+
* Reads dashboard.config.json from the project root,
|
|
5
|
+
* generates a Dashboard page with G2 charts.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { loadConfig, getModelSchema, loadDashboardConfig } from '../../index.js';
|
|
11
|
+
import { resolveProjectRoot } from '../utils.js';
|
|
12
|
+
import { toPascalCase } from '../helpers/naming.js';
|
|
13
|
+
import { wireAppTsxAutomatic } from '../helpers/app-tsx-wiring.js';
|
|
14
|
+
/** Build variable name for chart data: e.g. "subjectPieData" */
|
|
15
|
+
function buildChartDataVarName(widget) {
|
|
16
|
+
const prefix = widget.groupBy ? toPascalCase(widget.groupBy) : '';
|
|
17
|
+
const suffix = toPascalCase(widget.type);
|
|
18
|
+
return `${prefix.charAt(0).toLowerCase() + prefix.slice(1)}${suffix}Data`;
|
|
19
|
+
}
|
|
20
|
+
/** Build useChartData() call string */
|
|
21
|
+
function buildUseChartDataCall(widget, modelName, pascalModelName, isBooleanField) {
|
|
22
|
+
const listVar = `${pascalModelName.charAt(0).toLowerCase() + pascalModelName.slice(1)}List`;
|
|
23
|
+
const varName = buildChartDataVarName(widget);
|
|
24
|
+
const options = [];
|
|
25
|
+
if (widget.type === 'stat') {
|
|
26
|
+
// Stats don't use useChartData
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
// 2D charts use useCrossGroupChartData
|
|
30
|
+
const is2D = ['heatmap', 'stacked_bar', 'grouped_bar'].includes(widget.type) && widget.crossGroupBy;
|
|
31
|
+
// Treemap uses useTreemapChartData
|
|
32
|
+
const isTreemap = widget.type === 'treemap';
|
|
33
|
+
if (widget.groupBy) {
|
|
34
|
+
if (isBooleanField) {
|
|
35
|
+
options.push(`groupByBoolean: '${widget.groupBy}'`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
options.push(`groupBy: '${widget.groupBy}'`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (widget.crossGroupBy) {
|
|
42
|
+
options.push(`crossGroupBy: '${widget.crossGroupBy}'`);
|
|
43
|
+
}
|
|
44
|
+
if (widget.aggregate && widget.aggregate !== 'count') {
|
|
45
|
+
options.push(`aggregate: '${widget.aggregate}'`);
|
|
46
|
+
}
|
|
47
|
+
if (widget.field) {
|
|
48
|
+
options.push(`field: '${widget.field}'`);
|
|
49
|
+
}
|
|
50
|
+
if (widget.interval) {
|
|
51
|
+
options.push(`interval: '${widget.interval}'`);
|
|
52
|
+
}
|
|
53
|
+
if (widget.filter) {
|
|
54
|
+
options.push(`filter: ${JSON.stringify(widget.filter)}`);
|
|
55
|
+
}
|
|
56
|
+
const optionsStr = options.length > 0 ? `{ ${options.join(', ')} }` : '{}';
|
|
57
|
+
if (is2D) {
|
|
58
|
+
return `const ${varName} = useCrossGroupChartData(${listVar}, ${optionsStr});`;
|
|
59
|
+
}
|
|
60
|
+
if (isTreemap) {
|
|
61
|
+
return `const ${varName} = useTreemapChartData(${listVar}, ${optionsStr});`;
|
|
62
|
+
}
|
|
63
|
+
return `const ${varName} = useChartData(${listVar}, ${optionsStr});`;
|
|
64
|
+
}
|
|
65
|
+
/** Build G2 Spec object literal code */
|
|
66
|
+
function buildG2Spec(widget) {
|
|
67
|
+
const varName = buildChartDataVarName(widget);
|
|
68
|
+
switch (widget.type) {
|
|
69
|
+
case 'pie':
|
|
70
|
+
return `{
|
|
71
|
+
type: 'interval',
|
|
72
|
+
data: ${varName},
|
|
73
|
+
encode: { y: 'value', color: 'name' },
|
|
74
|
+
coordinate: { type: 'theta' },
|
|
75
|
+
transform: [{ type: 'stackY' }],
|
|
76
|
+
labels: [{ text: 'name', position: 'outside' }],
|
|
77
|
+
}`;
|
|
78
|
+
case 'ring':
|
|
79
|
+
return `{
|
|
80
|
+
type: 'interval',
|
|
81
|
+
data: ${varName},
|
|
82
|
+
encode: { y: 'value', color: 'name' },
|
|
83
|
+
coordinate: { type: 'theta', innerRadius: 0.6 },
|
|
84
|
+
transform: [{ type: 'stackY' }],
|
|
85
|
+
labels: [{ text: 'name', position: 'outside' }],
|
|
86
|
+
}`;
|
|
87
|
+
case 'bar':
|
|
88
|
+
return `{
|
|
89
|
+
type: 'interval',
|
|
90
|
+
data: ${varName},
|
|
91
|
+
encode: { x: 'value', y: 'name', color: 'name' },
|
|
92
|
+
coordinate: { transform: [{ type: 'transpose' }] },
|
|
93
|
+
}`;
|
|
94
|
+
case 'stacked_bar':
|
|
95
|
+
if (widget.crossGroupBy) {
|
|
96
|
+
return `{
|
|
97
|
+
type: 'interval',
|
|
98
|
+
data: ${varName},
|
|
99
|
+
encode: { x: 'group', y: 'value', color: 'name' },
|
|
100
|
+
coordinate: { transform: [{ type: 'transpose' }] },
|
|
101
|
+
transform: [{ type: 'stackY' }],
|
|
102
|
+
}`;
|
|
103
|
+
}
|
|
104
|
+
return `{
|
|
105
|
+
type: 'interval',
|
|
106
|
+
data: ${varName},
|
|
107
|
+
encode: { x: 'value', y: 'name', color: 'name' },
|
|
108
|
+
coordinate: { transform: [{ type: 'transpose' }] },
|
|
109
|
+
transform: [{ type: 'stackY' }],
|
|
110
|
+
}`;
|
|
111
|
+
case 'grouped_bar':
|
|
112
|
+
if (widget.crossGroupBy) {
|
|
113
|
+
return `{
|
|
114
|
+
type: 'interval',
|
|
115
|
+
data: ${varName},
|
|
116
|
+
encode: { x: 'group', y: 'value', color: 'name' },
|
|
117
|
+
coordinate: { transform: [{ type: 'transpose' }] },
|
|
118
|
+
transform: [{ type: 'dodgeX' }],
|
|
119
|
+
}`;
|
|
120
|
+
}
|
|
121
|
+
return `{
|
|
122
|
+
type: 'interval',
|
|
123
|
+
data: ${varName},
|
|
124
|
+
encode: { x: 'value', y: 'name', color: 'name' },
|
|
125
|
+
coordinate: { transform: [{ type: 'transpose' }] },
|
|
126
|
+
transform: [{ type: 'dodgeX' }],
|
|
127
|
+
}`;
|
|
128
|
+
case 'line':
|
|
129
|
+
return `{
|
|
130
|
+
type: 'line',
|
|
131
|
+
data: ${varName},
|
|
132
|
+
encode: { x: 'name', y: 'value' },
|
|
133
|
+
}`;
|
|
134
|
+
case 'area':
|
|
135
|
+
return `{
|
|
136
|
+
type: 'area',
|
|
137
|
+
data: ${varName},
|
|
138
|
+
encode: { x: 'name', y: 'value' },
|
|
139
|
+
}`;
|
|
140
|
+
case 'scatter':
|
|
141
|
+
return `{
|
|
142
|
+
type: 'point',
|
|
143
|
+
data: ${varName},
|
|
144
|
+
encode: { x: 'name', y: 'value', color: 'name', size: 4 },
|
|
145
|
+
}`;
|
|
146
|
+
case 'radar':
|
|
147
|
+
return `{
|
|
148
|
+
type: 'line',
|
|
149
|
+
data: ${varName},
|
|
150
|
+
encode: { x: 'name', y: 'value', color: 'name' },
|
|
151
|
+
coordinate: { type: 'polar' },
|
|
152
|
+
}`;
|
|
153
|
+
case 'funnel':
|
|
154
|
+
return `{
|
|
155
|
+
type: 'interval',
|
|
156
|
+
data: ${varName},
|
|
157
|
+
encode: { x: 'name', y: 'value', color: 'name' },
|
|
158
|
+
coordinate: { transform: [{ type: 'transpose' }] },
|
|
159
|
+
transform: [{ type: 'stackY' }],
|
|
160
|
+
scale: { y: { type: 'identity' } },
|
|
161
|
+
}`;
|
|
162
|
+
case 'treemap':
|
|
163
|
+
return `{
|
|
164
|
+
type: 'treemap',
|
|
165
|
+
data: ${varName},
|
|
166
|
+
encode: { value: 'value' },
|
|
167
|
+
}`;
|
|
168
|
+
case 'gauge':
|
|
169
|
+
return `{
|
|
170
|
+
type: 'gauge',
|
|
171
|
+
data: ${varName}.length ? [${varName}[0].value] : [0],
|
|
172
|
+
scale: { color: { range: ['#5B8FF9', '#E8E8E8'] } },
|
|
173
|
+
}`;
|
|
174
|
+
case 'liquid':
|
|
175
|
+
return `{
|
|
176
|
+
type: 'liquid',
|
|
177
|
+
data: ${varName}.length ? [${varName}[0].value] : [0],
|
|
178
|
+
style: { outline: { border: 4, distance: 8 } },
|
|
179
|
+
}`;
|
|
180
|
+
case 'heatmap':
|
|
181
|
+
if (widget.crossGroupBy) {
|
|
182
|
+
return `{
|
|
183
|
+
type: 'cell',
|
|
184
|
+
data: ${varName},
|
|
185
|
+
encode: { x: 'name', y: 'group', color: 'value' },
|
|
186
|
+
style: { inset: 1 },
|
|
187
|
+
}`;
|
|
188
|
+
}
|
|
189
|
+
return `{
|
|
190
|
+
type: 'cell',
|
|
191
|
+
data: ${varName},
|
|
192
|
+
encode: { x: 'name', y: 'name', color: 'value' },
|
|
193
|
+
style: { inset: 1 },
|
|
194
|
+
}`;
|
|
195
|
+
default:
|
|
196
|
+
return `{
|
|
197
|
+
type: 'interval',
|
|
198
|
+
data: ${varName},
|
|
199
|
+
encode: { x: 'name', y: 'value', color: 'name' },
|
|
200
|
+
}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Build stat card code */
|
|
204
|
+
function statCardCode(widget, modelName, pascalModelName) {
|
|
205
|
+
const listVar = `${pascalModelName.charAt(0).toLowerCase() + pascalModelName.slice(1)}List`;
|
|
206
|
+
if (widget.aggregate === 'ratio') {
|
|
207
|
+
// Ratio: filtered count / total count, displayed as percentage
|
|
208
|
+
if (widget.filter && Object.keys(widget.filter).length > 0) {
|
|
209
|
+
const filterExpr = Object.entries(widget.filter)
|
|
210
|
+
.map(([key, val]) => {
|
|
211
|
+
if (typeof val === 'boolean')
|
|
212
|
+
return `d.${key} === ${val}`;
|
|
213
|
+
if (typeof val === 'string')
|
|
214
|
+
return `d.${key} === '${val}'`;
|
|
215
|
+
return `d.${key} === ${JSON.stringify(val)}`;
|
|
216
|
+
})
|
|
217
|
+
.join(' && ');
|
|
218
|
+
return `<Statistic title="${widget.title}" value={${listVar}.length ? Math.round(${listVar}.filter(d => ${filterExpr}).length / ${listVar}.length * 1000) / 10 : 0} suffix="%" precision={1} />`;
|
|
219
|
+
}
|
|
220
|
+
return `<Statistic title="${widget.title}" value={100} suffix="%" />`;
|
|
221
|
+
}
|
|
222
|
+
if (widget.aggregate === 'count' || !widget.aggregate) {
|
|
223
|
+
if (widget.filter && Object.keys(widget.filter).length > 0) {
|
|
224
|
+
// Filtered count — use inline filter in JSX
|
|
225
|
+
const filterExpr = Object.entries(widget.filter)
|
|
226
|
+
.map(([key, val]) => {
|
|
227
|
+
if (typeof val === 'boolean')
|
|
228
|
+
return `d.${key} === ${val}`;
|
|
229
|
+
if (typeof val === 'string')
|
|
230
|
+
return `d.${key} === '${val}'`;
|
|
231
|
+
return `d.${key} === ${JSON.stringify(val)}`;
|
|
232
|
+
})
|
|
233
|
+
.join(' && ');
|
|
234
|
+
return `<Statistic title="${widget.title}" value={${listVar}.filter(d => ${filterExpr}).length} />`;
|
|
235
|
+
}
|
|
236
|
+
return `<Statistic title="${widget.title}" value={${listVar}.length} />`;
|
|
237
|
+
}
|
|
238
|
+
// sum/avg/min/max on a field
|
|
239
|
+
const agg = widget.aggregate;
|
|
240
|
+
const field = widget.field || 'value';
|
|
241
|
+
if (agg === 'sum') {
|
|
242
|
+
return `<Statistic title="${widget.title}" value={${listVar}.reduce((s, d) => s + (Number(d.${field}) || 0), 0)} precision={1} />`;
|
|
243
|
+
}
|
|
244
|
+
if (agg === 'avg') {
|
|
245
|
+
return `<Statistic title="${widget.title}" value={${listVar}.length ? ${listVar}.reduce((s, d) => s + (Number(d.${field}) || 0), 0) / ${listVar}.length : 0} precision={1} />`;
|
|
246
|
+
}
|
|
247
|
+
if (agg === 'min') {
|
|
248
|
+
return `<Statistic title="${widget.title}" value={${listVar}.length ? Math.min(...${listVar}.map(d => Number(d.${field}) || 0)) : 0} />`;
|
|
249
|
+
}
|
|
250
|
+
if (agg === 'max') {
|
|
251
|
+
return `<Statistic title="${widget.title}" value={${listVar}.length ? Math.max(...${listVar}.map(d => Number(d.${field}) || 0)) : 0} />`;
|
|
252
|
+
}
|
|
253
|
+
return `<Statistic title="${widget.title}" value={${listVar}.length} />`;
|
|
254
|
+
}
|
|
255
|
+
/** Detect boolean fields for groupByBoolean usage in generated code */
|
|
256
|
+
function detectBooleanFields(models) {
|
|
257
|
+
const booleanFields = new Set();
|
|
258
|
+
for (const model of models) {
|
|
259
|
+
for (const field of model.fields) {
|
|
260
|
+
if (field.type === 'boolean') {
|
|
261
|
+
booleanFields.add(`${model.name}.${field.name}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return booleanFields;
|
|
266
|
+
}
|
|
267
|
+
/** Generate the full Dashboard page TSX */
|
|
268
|
+
function dashboardPageTemplate(dashboard, models) {
|
|
269
|
+
const pascalName = toPascalCase(dashboard.name);
|
|
270
|
+
const pageLabel = dashboard.description || pascalName;
|
|
271
|
+
const booleanFields = detectBooleanFields(models);
|
|
272
|
+
// Collect unique model names used in widgets
|
|
273
|
+
const usedModels = new Set();
|
|
274
|
+
for (const row of dashboard.layout) {
|
|
275
|
+
for (const widget of row.row) {
|
|
276
|
+
usedModels.add(widget.model);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Also include all declared models
|
|
280
|
+
for (const modelName of dashboard.models) {
|
|
281
|
+
usedModels.add(modelName);
|
|
282
|
+
}
|
|
283
|
+
// Build useData hooks for each model
|
|
284
|
+
const useDataHooks = Array.from(usedModels).map((modelName) => {
|
|
285
|
+
const pascalModel = toPascalCase(modelName);
|
|
286
|
+
const listVar = `${pascalModel.charAt(0).toLowerCase() + pascalModel.slice(1)}List`;
|
|
287
|
+
return `const { list: ${listVar} } = useData('${modelName}');`;
|
|
288
|
+
}).join('\n ');
|
|
289
|
+
// Build useChartData calls — collect all chart (non-stat) widget data hooks
|
|
290
|
+
const chartDataHooks = [];
|
|
291
|
+
for (const row of dashboard.layout) {
|
|
292
|
+
for (const widget of row.row) {
|
|
293
|
+
if (widget.type !== 'stat') {
|
|
294
|
+
const pascalModel = toPascalCase(widget.model);
|
|
295
|
+
const isBoolean = booleanFields.has(`${widget.model}.${widget.groupBy}`);
|
|
296
|
+
const hookCall = buildUseChartDataCall(widget, widget.model, pascalModel, isBoolean);
|
|
297
|
+
if (hookCall) {
|
|
298
|
+
chartDataHooks.push(hookCall);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Build layout rows JSX
|
|
304
|
+
const layoutJsx = dashboard.layout.map((layoutRow) => {
|
|
305
|
+
const colCount = layoutRow.row.length;
|
|
306
|
+
const defaultSpan = Math.floor(24 / colCount);
|
|
307
|
+
const cols = layoutRow.row.map((widget) => {
|
|
308
|
+
const span = widget.span || defaultSpan;
|
|
309
|
+
if (widget.type === 'stat') {
|
|
310
|
+
const pascalModel = toPascalCase(widget.model);
|
|
311
|
+
const statCode = statCardCode(widget, widget.model, pascalModel);
|
|
312
|
+
return ` <Col span={${span}}>
|
|
313
|
+
<Card styles={{ body: { textAlign: 'center' } }}>
|
|
314
|
+
${statCode}
|
|
315
|
+
</Card>
|
|
316
|
+
</Col>`;
|
|
317
|
+
}
|
|
318
|
+
// Chart widget
|
|
319
|
+
const varName = buildChartDataVarName(widget);
|
|
320
|
+
const spec = buildG2Spec(widget);
|
|
321
|
+
return ` <Col span={${span}}>
|
|
322
|
+
<Card title="${widget.title}" styles={{ body: { padding: '8px 16px 16px' } }}>
|
|
323
|
+
<DashboardChart spec={${spec}} />
|
|
324
|
+
</Card>
|
|
325
|
+
</Col>`;
|
|
326
|
+
}).join('\n');
|
|
327
|
+
return ` <Row gutter={16}>
|
|
328
|
+
${cols}
|
|
329
|
+
</Row>`;
|
|
330
|
+
}).join('\n');
|
|
331
|
+
// Build TypeScript interfaces for each model
|
|
332
|
+
const interfaces = models.map((model) => {
|
|
333
|
+
const pascalModel = toPascalCase(model.name);
|
|
334
|
+
const fieldDefs = model.fields.map((f) => {
|
|
335
|
+
const tsType = f.type === 'string[]' ? 'string[]'
|
|
336
|
+
: f.type === 'number[]' ? 'number[]'
|
|
337
|
+
: f.type === 'json' ? 'Record<string, unknown>'
|
|
338
|
+
: f.type === 'number' ? 'number'
|
|
339
|
+
: f.type === 'boolean' ? 'boolean'
|
|
340
|
+
: f.type === 'date' ? 'string'
|
|
341
|
+
: 'string';
|
|
342
|
+
return ` ${f.name}${f.required ? '' : '?'}: ${tsType};`;
|
|
343
|
+
}).join('\n');
|
|
344
|
+
return `interface ${pascalModel}Record {\n id: string;\n${fieldDefs}\n}`;
|
|
345
|
+
}).join('\n\n');
|
|
346
|
+
return `import React from 'react';
|
|
347
|
+
import { Row, Col, Card, Statistic } from 'antd';
|
|
348
|
+
import { useData, useChartData, useCrossGroupChartData, useTreemapChartData, DashboardChart } from '@loom-framework/frontend-antd';
|
|
349
|
+
|
|
350
|
+
${interfaces}
|
|
351
|
+
|
|
352
|
+
export function ${pascalName}Page(): React.ReactElement {
|
|
353
|
+
${useDataHooks}
|
|
354
|
+
${chartDataHooks.join('\n ')}
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
358
|
+
${layoutJsx}
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export default ${pascalName}Page;
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
export function registerGenerateDashboardCommand(program) {
|
|
367
|
+
program
|
|
368
|
+
.command('dashboard <name>')
|
|
369
|
+
.description('Generate a Dashboard page from dashboard.config.json')
|
|
370
|
+
.action(async (name) => {
|
|
371
|
+
try {
|
|
372
|
+
const projectRoot = await resolveProjectRoot();
|
|
373
|
+
const pascalName = toPascalCase(name);
|
|
374
|
+
// 1. Load dashboard.config.json
|
|
375
|
+
const dashboard = await loadDashboardConfig(projectRoot);
|
|
376
|
+
if (!dashboard) {
|
|
377
|
+
console.error(chalk.red('No dashboard.config.json found in project root.'));
|
|
378
|
+
console.error(chalk.dim('Create one first — see the Loom skill Dashboard section for guidance.'));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
if (dashboard.name !== name) {
|
|
382
|
+
console.error(chalk.red(`Dashboard name mismatch: command expects "${name}" but config has "${dashboard.name}"`));
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
// 2. Load loom.config.ts and validate referenced models
|
|
386
|
+
const config = await loadConfig(projectRoot);
|
|
387
|
+
const modelSchemas = [];
|
|
388
|
+
for (const modelName of dashboard.models) {
|
|
389
|
+
const schema = getModelSchema(config, modelName);
|
|
390
|
+
if (!schema) {
|
|
391
|
+
console.error(chalk.red(`Model "${modelName}" referenced in dashboard.config.json not found in loom.config.ts`));
|
|
392
|
+
console.error(chalk.dim(`Available models: ${config.data.models.map(m => m.name).join(', ')}`));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
modelSchemas.push(schema);
|
|
396
|
+
}
|
|
397
|
+
// 3. Generate Dashboard page
|
|
398
|
+
const pageDir = path.join(projectRoot, 'frontend', 'src', 'components', 'pages');
|
|
399
|
+
const pagePath = path.join(pageDir, `${pascalName}.tsx`);
|
|
400
|
+
// Check if page already exists
|
|
401
|
+
try {
|
|
402
|
+
await fs.access(pagePath);
|
|
403
|
+
console.error(chalk.red(`Page "${pascalName}" already exists at ${pagePath}`));
|
|
404
|
+
console.error(chalk.dim('Delete the existing page first if you want to regenerate.'));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// File does not exist, proceed
|
|
409
|
+
}
|
|
410
|
+
const template = dashboardPageTemplate(dashboard, modelSchemas);
|
|
411
|
+
await fs.mkdir(pageDir, { recursive: true });
|
|
412
|
+
await fs.writeFile(pagePath, template, 'utf-8');
|
|
413
|
+
console.log(chalk.green('Dashboard page created successfully!'));
|
|
414
|
+
console.log();
|
|
415
|
+
console.log(chalk.bold(' Page:'), `${pascalName}Page`);
|
|
416
|
+
console.log(chalk.bold(' Path:'), path.join('frontend', 'src', 'components', 'pages', `${pascalName}.tsx`));
|
|
417
|
+
console.log(chalk.bold(' Models:'), dashboard.models.join(', '));
|
|
418
|
+
console.log(chalk.bold(' Widgets:'), dashboard.layout.reduce((sum, row) => sum + row.row.length, 0).toString());
|
|
419
|
+
// 4. Auto-wire App.tsx
|
|
420
|
+
const navLabel = dashboard.description || pascalName;
|
|
421
|
+
const wired = await wireAppTsxAutomatic(projectRoot, pascalName, dashboard.name, navLabel);
|
|
422
|
+
if (wired) {
|
|
423
|
+
console.log(chalk.green(' App.tsx wired automatically'));
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
console.log(chalk.yellow(' Could not auto-wire App.tsx — add the page import, navItem, and switch case manually'));
|
|
427
|
+
}
|
|
428
|
+
// 5. Check if @antv/g2 is installed
|
|
429
|
+
const frontendPkgPath = path.join(projectRoot, 'frontend', 'package.json');
|
|
430
|
+
try {
|
|
431
|
+
const pkgContent = await fs.readFile(frontendPkgPath, 'utf-8');
|
|
432
|
+
const pkg = JSON.parse(pkgContent);
|
|
433
|
+
const hasG2 = (pkg.dependencies && pkg.dependencies['@antv/g2']) ||
|
|
434
|
+
(pkg.devDependencies && pkg.devDependencies['@antv/g2']);
|
|
435
|
+
if (!hasG2) {
|
|
436
|
+
console.log();
|
|
437
|
+
console.log(chalk.yellow(' @antv/g2 is not installed in frontend/.'));
|
|
438
|
+
console.log(chalk.dim(' Run: cd frontend && pnpm add @antv/g2'));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// No package.json found, skip check
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
447
|
+
console.error(chalk.red('Failed to create dashboard:'), message);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
//# sourceMappingURL=generate-dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-dashboard.js","sourceRoot":"","sources":["../../../src/cli/commands/generate-dashboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-page.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/generate-page.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"generate-page.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/generate-page.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAybzC,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwFlE"}
|
|
@@ -11,6 +11,7 @@ import { loadConfig, getModelSchema, generateCapabilities } from '../../index.js
|
|
|
11
11
|
import { resolveProjectRoot } from '../utils.js';
|
|
12
12
|
import { toPascalCase } from '../helpers/naming.js';
|
|
13
13
|
import { fieldToFormItem } from '../helpers/field-template.js';
|
|
14
|
+
import { wireAppTsxAutomatic, wireSkillManagementPage } from '../helpers/app-tsx-wiring.js';
|
|
14
15
|
/** Semantic color mapping for common enum values */
|
|
15
16
|
const SEMANTIC_COLORS = {
|
|
16
17
|
'active': 'green', 'inactive': 'default', 'pending': 'orange',
|
|
@@ -514,128 +515,4 @@ export function registerGeneratePageCommand(program) {
|
|
|
514
515
|
}
|
|
515
516
|
});
|
|
516
517
|
}
|
|
517
|
-
/**
|
|
518
|
-
* Auto-wire App.tsx: add import, navItem, and switch case for a new page.
|
|
519
|
-
* Returns true if successful, false if App.tsx structure is unexpected.
|
|
520
|
-
*/
|
|
521
|
-
async function wireAppTsxAutomatic(projectRoot, pascalName, modelKey, modelLabel) {
|
|
522
|
-
const appPath = path.join(projectRoot, 'frontend', 'src', 'App.tsx');
|
|
523
|
-
let content;
|
|
524
|
-
try {
|
|
525
|
-
content = await fs.readFile(appPath, 'utf-8');
|
|
526
|
-
}
|
|
527
|
-
catch {
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
530
|
-
// Check if this page is already wired (avoid duplicates)
|
|
531
|
-
if (content.includes(`from './components/pages/${pascalName}'`) || content.includes(`<${pascalName}Page`)) {
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
// Also check if navItem with same key already exists
|
|
535
|
-
if (content.includes(`key: '${modelKey}'`)) {
|
|
536
|
-
return false;
|
|
537
|
-
}
|
|
538
|
-
let modified = content;
|
|
539
|
-
// 1. Add import after the last import line
|
|
540
|
-
const importLine = `import ${pascalName}Page from './components/pages/${pascalName}';`;
|
|
541
|
-
const lastImportEnd = modified.lastIndexOf('\n', modified.indexOf('\nexport'));
|
|
542
|
-
if (lastImportEnd === -1)
|
|
543
|
-
return false;
|
|
544
|
-
modified = modified.slice(0, lastImportEnd + 1) + importLine + '\n' + modified.slice(lastImportEnd + 1);
|
|
545
|
-
// 2. Add navItem (before the closing ] of navItems array)
|
|
546
|
-
const navItemLine = ` { key: '${modelKey}', label: '${modelLabel}' },`;
|
|
547
|
-
const navItemsEnd = modified.indexOf('// Add more nav items');
|
|
548
|
-
if (navItemsEnd !== -1) {
|
|
549
|
-
// Insert before the comment placeholder
|
|
550
|
-
const lineStart = modified.lastIndexOf('\n', navItemsEnd) + 1;
|
|
551
|
-
modified = modified.slice(0, lineStart) + navItemLine + '\n' + modified.slice(lineStart);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
// Fallback: find the closing ] of navItems array
|
|
555
|
-
const navArrayMatch = modified.match(/const navItems\s*=\s*\[/);
|
|
556
|
-
if (!navArrayMatch)
|
|
557
|
-
return false;
|
|
558
|
-
const arrayStart = navArrayMatch.index + navArrayMatch[0].length;
|
|
559
|
-
// Find the matching ]
|
|
560
|
-
let depth = 1;
|
|
561
|
-
let pos = arrayStart;
|
|
562
|
-
while (pos < modified.length && depth > 0) {
|
|
563
|
-
if (modified[pos] === '[')
|
|
564
|
-
depth++;
|
|
565
|
-
else if (modified[pos] === ']')
|
|
566
|
-
depth--;
|
|
567
|
-
pos++;
|
|
568
|
-
}
|
|
569
|
-
const closingBracket = pos - 1;
|
|
570
|
-
modified = modified.slice(0, closingBracket) + navItemLine + '\n' + modified.slice(closingBracket);
|
|
571
|
-
}
|
|
572
|
-
// 3. Add switch case — before skill-management case if present, otherwise before default
|
|
573
|
-
const caseLine = ` case '${modelKey}': return <${pascalName}Page />;`;
|
|
574
|
-
const skillCaseIdx = modified.indexOf("case 'skill-management':");
|
|
575
|
-
const insertAnchor = skillCaseIdx !== -1 ? skillCaseIdx : modified.indexOf('default:');
|
|
576
|
-
if (insertAnchor === -1)
|
|
577
|
-
return false;
|
|
578
|
-
const insertLineStart = modified.lastIndexOf('\n', insertAnchor) + 1;
|
|
579
|
-
modified = modified.slice(0, insertLineStart) + caseLine + '\n' + modified.slice(insertLineStart);
|
|
580
|
-
// 4. Ensure baseUrl="" on AppShell (prevent /api/v1/api/v1 double prefix)
|
|
581
|
-
if (modified.includes('<AppShell') && !modified.includes('baseUrl=')) {
|
|
582
|
-
modified = modified.replace('<AppShell', '<AppShell\n baseUrl=""');
|
|
583
|
-
}
|
|
584
|
-
await fs.writeFile(appPath, modified, 'utf-8');
|
|
585
|
-
return true;
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Auto-wire the SkillManagementPage into App.tsx.
|
|
589
|
-
* Called once when the first CRUD page is generated.
|
|
590
|
-
* Returns true if successful, false if already wired or structure is unexpected.
|
|
591
|
-
*/
|
|
592
|
-
async function wireSkillManagementPage(projectRoot) {
|
|
593
|
-
const appPath = path.join(projectRoot, 'frontend', 'src', 'App.tsx');
|
|
594
|
-
let content;
|
|
595
|
-
try {
|
|
596
|
-
content = await fs.readFile(appPath, 'utf-8');
|
|
597
|
-
}
|
|
598
|
-
catch {
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
// Check if already wired (avoid duplicates)
|
|
602
|
-
if (content.includes('SkillManagementPage')) {
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
let modified = content;
|
|
606
|
-
// 1. Add import after the last import line
|
|
607
|
-
const importLine = `import { SkillManagementPage } from '@loom-framework/frontend-antd';`;
|
|
608
|
-
const lastImportEnd = modified.lastIndexOf('\n', modified.indexOf('\nexport'));
|
|
609
|
-
if (lastImportEnd === -1)
|
|
610
|
-
return false;
|
|
611
|
-
modified = modified.slice(0, lastImportEnd + 1) + importLine + '\n' + modified.slice(lastImportEnd + 1);
|
|
612
|
-
// 2. Add navItem — always at the end of the array so it stays below all model pages
|
|
613
|
-
const navItemLine = ` { key: 'skill-management', label: 'Skill 管理' },`;
|
|
614
|
-
const navArrayMatch = modified.match(/const navItems\s*=\s*\[/);
|
|
615
|
-
if (!navArrayMatch)
|
|
616
|
-
return false;
|
|
617
|
-
const arrayStart = navArrayMatch.index + navArrayMatch[0].length;
|
|
618
|
-
let depth = 1;
|
|
619
|
-
let pos = arrayStart;
|
|
620
|
-
while (pos < modified.length && depth > 0) {
|
|
621
|
-
if (modified[pos] === '[')
|
|
622
|
-
depth++;
|
|
623
|
-
else if (modified[pos] === ']')
|
|
624
|
-
depth--;
|
|
625
|
-
pos++;
|
|
626
|
-
}
|
|
627
|
-
const closingBracket = pos - 1;
|
|
628
|
-
// Insert before the line containing the closing bracket
|
|
629
|
-
const lineStart = modified.lastIndexOf('\n', closingBracket) + 1;
|
|
630
|
-
modified = modified.slice(0, lineStart) + navItemLine + '\n' + modified.slice(lineStart);
|
|
631
|
-
// 3. Add switch case before default
|
|
632
|
-
const caseLine = ` case 'skill-management': return <SkillManagementPage />;`;
|
|
633
|
-
const defaultCase = modified.indexOf('default:');
|
|
634
|
-
if (defaultCase === -1)
|
|
635
|
-
return false;
|
|
636
|
-
const defaultLineStart = modified.lastIndexOf('\n', defaultCase) + 1;
|
|
637
|
-
modified = modified.slice(0, defaultLineStart) + caseLine + '\n' + modified.slice(defaultLineStart);
|
|
638
|
-
await fs.writeFile(appPath, modified, 'utf-8');
|
|
639
|
-
return true;
|
|
640
|
-
}
|
|
641
518
|
//# sourceMappingURL=generate-page.js.map
|