@atlashub/smartstack-cli 3.36.0 → 3.37.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.
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate Documentation with Mock UI for SmartStack Modules
|
|
4
|
+
*
|
|
5
|
+
* This script generates complete documentation pages with embedded mock UI components
|
|
6
|
+
* that visually represent the module's interface without requiring real screenshots.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node generate-doc-with-mock-ui.ts \
|
|
10
|
+
* --module users \
|
|
11
|
+
* --context platform \
|
|
12
|
+
* --application administration \
|
|
13
|
+
* --app-path "D:/01 - projets/SmartStack.app/02-Develop"
|
|
14
|
+
*
|
|
15
|
+
* Output: Complete TSX file with mock UI (e.g., UsersDocPage.tsx)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { glob } from 'glob';
|
|
21
|
+
import { execSync } from 'child_process';
|
|
22
|
+
|
|
23
|
+
interface ModuleInfo {
|
|
24
|
+
module: string; // e.g., "users"
|
|
25
|
+
context: string; // e.g., "platform"
|
|
26
|
+
application: string; // e.g., "administration"
|
|
27
|
+
appPath: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface EntityProperty {
|
|
31
|
+
name: string; // e.g., "Email", "FirstName"
|
|
32
|
+
type: string; // e.g., "string", "bool", "DateTime?"
|
|
33
|
+
isRequired: boolean;
|
|
34
|
+
isNullable: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MockDataField {
|
|
38
|
+
propertyName: string;
|
|
39
|
+
mockValue: string; // Generated mock value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PageStructure {
|
|
43
|
+
hasTable: boolean;
|
|
44
|
+
hasKPIs: boolean;
|
|
45
|
+
hasForm: boolean;
|
|
46
|
+
hasFilters: boolean;
|
|
47
|
+
columns: string[]; // Table column names
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse command line arguments
|
|
52
|
+
*/
|
|
53
|
+
function parseArgs(): ModuleInfo {
|
|
54
|
+
const args = process.argv.slice(2);
|
|
55
|
+
|
|
56
|
+
const getArg = (flag: string): string | null => {
|
|
57
|
+
const index = args.indexOf(flag);
|
|
58
|
+
return index !== -1 && index + 1 < args.length ? args[index + 1] : null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const module = getArg('--module');
|
|
62
|
+
const context = getArg('--context');
|
|
63
|
+
const application = getArg('--application');
|
|
64
|
+
const appPath = getArg('--app-path');
|
|
65
|
+
|
|
66
|
+
if (!module || !context || !application || !appPath) {
|
|
67
|
+
console.error('Usage: node generate-doc-with-mock-ui.ts --module <name> --context <ctx> --application <app> --app-path <path>');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { module, context, application, appPath };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find Domain entity file
|
|
76
|
+
* Tries both singular and plural forms (e.g., users → User.cs)
|
|
77
|
+
*/
|
|
78
|
+
async function findEntityFile(appPath: string, module: string): Promise<string | null> {
|
|
79
|
+
const domainPath = path.join(appPath, 'src/SmartStack.Domain');
|
|
80
|
+
|
|
81
|
+
// Try singular form first (most common: users → User.cs)
|
|
82
|
+
const singularForm = capitalize(singularize(module));
|
|
83
|
+
const singularPattern = `**/${singularForm}.cs`;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
let files = await glob(singularPattern, { cwd: domainPath, absolute: true });
|
|
87
|
+
if (files.length > 0) {
|
|
88
|
+
return files[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fallback to plural form
|
|
92
|
+
const pluralForm = capitalize(module);
|
|
93
|
+
const pluralPattern = `**/${pluralForm}.cs`;
|
|
94
|
+
files = await glob(pluralPattern, { cwd: domainPath, absolute: true });
|
|
95
|
+
return files.length > 0 ? files[0] : null;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error finding entity:', error);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Find React page file
|
|
104
|
+
*/
|
|
105
|
+
async function findPageFile(appPath: string, module: string, context: string, application: string): Promise<string | null> {
|
|
106
|
+
const pagesPath = path.join(appPath, `web/smartstack-web/src/pages/${context}/${application}/${module}`);
|
|
107
|
+
const pattern = `${capitalize(module)}Page.tsx`;
|
|
108
|
+
|
|
109
|
+
const fullPath = path.join(pagesPath, pattern);
|
|
110
|
+
return fs.existsSync(fullPath) ? fullPath : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract entity properties from Domain entity file
|
|
115
|
+
*/
|
|
116
|
+
function extractEntityProperties(entityPath: string): EntityProperty[] {
|
|
117
|
+
const content = fs.readFileSync(entityPath, 'utf-8');
|
|
118
|
+
const lines = content.split('\n');
|
|
119
|
+
const properties: EntityProperty[] = [];
|
|
120
|
+
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
// Match property declarations: public string Email { get; private set; }
|
|
123
|
+
const propMatch = line.match(/public\s+(\w+\??)\s+(\w+)\s*\{\s*get;/);
|
|
124
|
+
if (propMatch) {
|
|
125
|
+
const type = propMatch[1];
|
|
126
|
+
const name = propMatch[2];
|
|
127
|
+
|
|
128
|
+
// Skip base entity properties and navigation properties
|
|
129
|
+
if (['Id', 'CreatedAt', 'UpdatedAt', 'CreatedBy', 'UpdatedBy'].includes(name)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Skip collections (List, IReadOnlyCollection, etc.)
|
|
134
|
+
if (line.includes('List<') || line.includes('IReadOnlyCollection<') || line.includes('ICollection<')) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
properties.push({
|
|
139
|
+
name,
|
|
140
|
+
type,
|
|
141
|
+
isRequired: !type.endsWith('?'),
|
|
142
|
+
isNullable: type.endsWith('?'),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return properties;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Analyze React page structure
|
|
152
|
+
*/
|
|
153
|
+
function analyzePageStructure(pagePath: string): PageStructure {
|
|
154
|
+
const content = fs.readFileSync(pagePath, 'utf-8');
|
|
155
|
+
|
|
156
|
+
const structure: PageStructure = {
|
|
157
|
+
hasTable: content.includes('<table') || content.includes('Table'),
|
|
158
|
+
hasKPIs: content.includes('KPI') || content.includes('stat') || content.includes('metric'),
|
|
159
|
+
hasForm: content.includes('<form') || content.includes('Form') || content.includes('input'),
|
|
160
|
+
hasFilters: content.includes('filter') || content.includes('Filter') || content.includes('search'),
|
|
161
|
+
columns: [],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Try to extract table column names
|
|
165
|
+
const thRegex = /<th[^>]*>([^<]+)<\/th>/g;
|
|
166
|
+
let match;
|
|
167
|
+
while ((match = thRegex.exec(content)) !== null) {
|
|
168
|
+
structure.columns.push(match[1].trim());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return structure;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generate mock value for a property
|
|
176
|
+
*/
|
|
177
|
+
function generateMockValue(property: EntityProperty, index: number): string {
|
|
178
|
+
const type = property.type.replace('?', '');
|
|
179
|
+
|
|
180
|
+
switch (type.toLowerCase()) {
|
|
181
|
+
case 'string':
|
|
182
|
+
if (property.name.toLowerCase().includes('email')) {
|
|
183
|
+
const names = ['john.doe', 'jane.smith', 'bob.johnson', 'alice.williams', 'charlie.brown'];
|
|
184
|
+
return `"${names[index % names.length]}@example.com"`;
|
|
185
|
+
}
|
|
186
|
+
if (property.name.toLowerCase().includes('name')) {
|
|
187
|
+
if (property.name.toLowerCase().includes('first')) {
|
|
188
|
+
const names = ['John', 'Jane', 'Bob', 'Alice', 'Charlie'];
|
|
189
|
+
return `"${names[index % names.length]}"`;
|
|
190
|
+
}
|
|
191
|
+
if (property.name.toLowerCase().includes('last')) {
|
|
192
|
+
const names = ['Doe', 'Smith', 'Johnson', 'Williams', 'Brown'];
|
|
193
|
+
return `"${names[index % names.length]}"`;
|
|
194
|
+
}
|
|
195
|
+
return `"${property.name} ${index + 1}"`;
|
|
196
|
+
}
|
|
197
|
+
if (property.name.toLowerCase().includes('title')) {
|
|
198
|
+
return `"Sample ${property.name} ${index + 1}"`;
|
|
199
|
+
}
|
|
200
|
+
if (property.name.toLowerCase().includes('description')) {
|
|
201
|
+
return `"This is a sample description for item ${index + 1}"`;
|
|
202
|
+
}
|
|
203
|
+
return `"Sample ${property.name}"`;
|
|
204
|
+
|
|
205
|
+
case 'bool':
|
|
206
|
+
case 'boolean':
|
|
207
|
+
return index % 2 === 0 ? 'true' : 'false';
|
|
208
|
+
|
|
209
|
+
case 'int':
|
|
210
|
+
case 'int32':
|
|
211
|
+
case 'long':
|
|
212
|
+
return String((index + 1) * 10);
|
|
213
|
+
|
|
214
|
+
case 'decimal':
|
|
215
|
+
case 'double':
|
|
216
|
+
case 'float':
|
|
217
|
+
return String((index + 1) * 10.5);
|
|
218
|
+
|
|
219
|
+
case 'datetime':
|
|
220
|
+
const dates = ['2026-01-15', '2026-01-20', '2026-02-01', '2026-02-10', '2026-02-15'];
|
|
221
|
+
return `"${dates[index % dates.length]}"`;
|
|
222
|
+
|
|
223
|
+
case 'guid':
|
|
224
|
+
// Generate a fixed mock GUID based on index to keep consistent data
|
|
225
|
+
const mockGuids = [
|
|
226
|
+
'"550e8400-e29b-41d4-a716-446655440000"',
|
|
227
|
+
'"6ba7b810-9dad-11d1-80b4-00c04fd430c8"',
|
|
228
|
+
'"6ba7b811-9dad-11d1-80b4-00c04fd430c9"',
|
|
229
|
+
'"6ba7b812-9dad-11d1-80b4-00c04fd430ca"',
|
|
230
|
+
'"6ba7b813-9dad-11d1-80b4-00c04fd430cb"'
|
|
231
|
+
];
|
|
232
|
+
return mockGuids[index % mockGuids.length];
|
|
233
|
+
|
|
234
|
+
default:
|
|
235
|
+
return property.isNullable ? 'null' : '""';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generate mock data array
|
|
241
|
+
*/
|
|
242
|
+
function generateMockData(properties: EntityProperty[], module: string, count: number = 5): string {
|
|
243
|
+
const mockItems: string[] = [];
|
|
244
|
+
|
|
245
|
+
for (let i = 0; i < count; i++) {
|
|
246
|
+
const fields = properties
|
|
247
|
+
.filter(p => !p.name.includes('Hash') && !p.name.includes('Token')) // Skip sensitive fields
|
|
248
|
+
.map(p => `${p.name.charAt(0).toLowerCase() + p.name.slice(1)}: ${generateMockValue(p, i)}`)
|
|
249
|
+
.join(', ');
|
|
250
|
+
|
|
251
|
+
mockItems.push(` { ${fields} }`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const singularName = module.endsWith('s') ? module.slice(0, -1) : module;
|
|
255
|
+
return `const mock${capitalize(module)} = [\n${mockItems.join(',\n')}\n];`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generate KPI stats based on entity properties
|
|
260
|
+
*/
|
|
261
|
+
function generateKPIStats(properties: EntityProperty[], module: string, count: number = 5): string {
|
|
262
|
+
const stats: string[] = [];
|
|
263
|
+
|
|
264
|
+
// Always add total count
|
|
265
|
+
stats.push(` { label: 'Total', value: '${count * 10}', color: 'var(--text-primary)' }`);
|
|
266
|
+
|
|
267
|
+
// Check for IsActive/Status property
|
|
268
|
+
const activeProperty = properties.find(p =>
|
|
269
|
+
p.name.toLowerCase() === 'isactive' && (p.type.toLowerCase() === 'bool' || p.type.toLowerCase() === 'boolean')
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (activeProperty) {
|
|
273
|
+
const activeCount = Math.floor(count * 10 * 0.6); // 60% active
|
|
274
|
+
const inactiveCount = count * 10 - activeCount;
|
|
275
|
+
stats.push(` { label: 'Active', value: '${activeCount}', color: 'var(--success-text)', border: 'var(--success-border)' }`);
|
|
276
|
+
stats.push(` { label: 'Inactive', value: '${inactiveCount}', color: 'var(--error-text)', border: 'var(--error-border)' }`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check for Status property (enum/string)
|
|
280
|
+
const statusProperty = properties.find(p =>
|
|
281
|
+
p.name.toLowerCase() === 'status' && p.type.toLowerCase() === 'string'
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (statusProperty) {
|
|
285
|
+
stats.push(` { label: 'Pending', value: '${Math.floor(count * 2)}', color: 'var(--warning-text)', border: 'var(--warning-border)' }`);
|
|
286
|
+
stats.push(` { label: 'In Progress', value: '${Math.floor(count * 3)}', color: 'var(--info-text)', border: 'var(--info-border)' }`);
|
|
287
|
+
stats.push(` { label: 'Completed', value: '${Math.floor(count * 5)}', color: 'var(--success-text)', border: 'var(--success-border)' }`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for DateTime property (e.g., CreatedAt, LastLoginAt)
|
|
291
|
+
const dateProperty = properties.find(p =>
|
|
292
|
+
p.type.toLowerCase() === 'datetime' &&
|
|
293
|
+
(p.name.toLowerCase().includes('created') || p.name.toLowerCase().includes('login'))
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if (dateProperty) {
|
|
297
|
+
stats.push(` { label: 'Last 7 days', value: '${Math.floor(count * 1.5)}', color: 'var(--info-text)', border: 'var(--info-border)' }`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return `const ${module}Stats = [\n${stats.join(',\n')}\n];`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generate form fields based on entity properties
|
|
305
|
+
*/
|
|
306
|
+
function generateFormSection(properties: EntityProperty[], module: string): string {
|
|
307
|
+
const ModuleName = capitalize(singularize(module));
|
|
308
|
+
|
|
309
|
+
// Filter properties suitable for form input
|
|
310
|
+
const formProps = properties.filter(p => {
|
|
311
|
+
const name = p.name.toLowerCase();
|
|
312
|
+
// Exclude read-only, computed, and sensitive fields
|
|
313
|
+
return !name.includes('hash') &&
|
|
314
|
+
!name.includes('token') &&
|
|
315
|
+
!name.includes('normalized') &&
|
|
316
|
+
!name.endsWith('id') &&
|
|
317
|
+
!name.includes('createdat') &&
|
|
318
|
+
!name.includes('updatedat') &&
|
|
319
|
+
!name.includes('createdby') &&
|
|
320
|
+
!name.includes('updatedby') &&
|
|
321
|
+
!name.includes('data') &&
|
|
322
|
+
!name.includes('proxy') &&
|
|
323
|
+
p.name !== 'Id';
|
|
324
|
+
}).slice(0, 6); // Limit to 6 most relevant fields
|
|
325
|
+
|
|
326
|
+
const formFields = formProps.map(p => {
|
|
327
|
+
const fieldName = p.name.charAt(0).toLowerCase() + p.name.slice(1);
|
|
328
|
+
const label = p.name.replace(/([A-Z])/g, ' $1').trim();
|
|
329
|
+
const isRequired = p.isRequired;
|
|
330
|
+
|
|
331
|
+
if (p.type.toLowerCase() === 'bool' || p.type.toLowerCase() === 'boolean') {
|
|
332
|
+
return ` <div className="flex items-center gap-2">
|
|
333
|
+
<input type="checkbox" id="${fieldName}" className="w-4 h-4" />
|
|
334
|
+
<label htmlFor="${fieldName}" className="text-sm">${label}</label>
|
|
335
|
+
</div>`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (p.type.toLowerCase() === 'datetime') {
|
|
339
|
+
return ` <div>
|
|
340
|
+
<label className="block text-sm font-medium mb-1">${label}${isRequired ? ' *' : ''}</label>
|
|
341
|
+
<input type="date" className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
|
|
342
|
+
</div>`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (p.name.toLowerCase().includes('email')) {
|
|
346
|
+
return ` <div>
|
|
347
|
+
<label className="block text-sm font-medium mb-1">${label}${isRequired ? ' *' : ''}</label>
|
|
348
|
+
<input type="email" placeholder="user@example.com" className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
|
|
349
|
+
</div>`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (p.name.toLowerCase().includes('description') || p.name.toLowerCase().includes('notes')) {
|
|
353
|
+
return ` <div className="md:col-span-2">
|
|
354
|
+
<label className="block text-sm font-medium mb-1">${label}</label>
|
|
355
|
+
<textarea rows={3} className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
|
|
356
|
+
</div>`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Default text input
|
|
360
|
+
return ` <div>
|
|
361
|
+
<label className="block text-sm font-medium mb-1">${label}${isRequired ? ' *' : ''}</label>
|
|
362
|
+
<input type="text" className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg" />
|
|
363
|
+
</div>`;
|
|
364
|
+
}).join('\n');
|
|
365
|
+
|
|
366
|
+
return ` {/* Section 4: Create Form Example */}
|
|
367
|
+
<section id="create-form" className="card p-6">
|
|
368
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|
369
|
+
<span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">4</span>
|
|
370
|
+
Create ${ModuleName} Form
|
|
371
|
+
</h2>
|
|
372
|
+
<p className="text-[var(--text-secondary)] mb-4">
|
|
373
|
+
Form interface for creating new ${module}.
|
|
374
|
+
</p>
|
|
375
|
+
|
|
376
|
+
<div className="border border-[var(--border-color)] rounded-lg p-6 bg-[var(--bg-secondary)]">
|
|
377
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
378
|
+
${formFields}
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div className="flex items-center gap-3 mt-6 pt-6 border-t border-[var(--border-color)]">
|
|
382
|
+
<button className="px-4 py-2 bg-[var(--color-primary-600)] text-white rounded-lg flex items-center gap-2">
|
|
383
|
+
<Plus className="w-4 h-4" />
|
|
384
|
+
Create ${ModuleName}
|
|
385
|
+
</button>
|
|
386
|
+
<button className="px-4 py-2 border border-[var(--border-color)] rounded-lg">
|
|
387
|
+
Cancel
|
|
388
|
+
</button>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</section>`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Generate KPI section UI
|
|
396
|
+
*/
|
|
397
|
+
function generateKPISection(module: string): string {
|
|
398
|
+
return ` {/* Stats */}
|
|
399
|
+
<div className="p-4 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]">
|
|
400
|
+
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2">
|
|
401
|
+
{${module}Stats.map((stat) => (
|
|
402
|
+
<div
|
|
403
|
+
key={stat.label}
|
|
404
|
+
className="p-3 bg-[var(--bg-primary)] rounded-lg text-center"
|
|
405
|
+
style={{ borderLeft: stat.border ? \`3px solid \${stat.border}\` : undefined }}
|
|
406
|
+
>
|
|
407
|
+
<div className="text-2xl font-bold" style={{ color: stat.color }}>{stat.value}</div>
|
|
408
|
+
<div className="text-xs text-[var(--text-secondary)] mt-1">{stat.label}</div>
|
|
409
|
+
</div>
|
|
410
|
+
))}
|
|
411
|
+
</div>
|
|
412
|
+
</div>`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Generate table mock UI section
|
|
417
|
+
*/
|
|
418
|
+
function generateTableMockUI(properties: EntityProperty[], module: string): string {
|
|
419
|
+
// Smart property selection for table display
|
|
420
|
+
const filteredProps = properties.filter(p => {
|
|
421
|
+
const name = p.name.toLowerCase();
|
|
422
|
+
// Exclude sensitive, internal, and FK properties
|
|
423
|
+
return !name.includes('hash') &&
|
|
424
|
+
!name.includes('token') &&
|
|
425
|
+
!name.includes('password') &&
|
|
426
|
+
!name.endsWith('id') && // Skip foreign keys
|
|
427
|
+
!name.includes('normalized') &&
|
|
428
|
+
!name.includes('data') && // Skip JSON/blob fields
|
|
429
|
+
!name.includes('proxy') &&
|
|
430
|
+
!name.includes('im') &&
|
|
431
|
+
p.name !== 'Sponsors' &&
|
|
432
|
+
p.name !== 'ProxyAddresses' &&
|
|
433
|
+
p.name !== 'ImAddresses';
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Prioritize display properties: name/title > email > status > date
|
|
437
|
+
const priorityProps: EntityProperty[] = [];
|
|
438
|
+
|
|
439
|
+
// Add name/title properties first
|
|
440
|
+
priorityProps.push(...filteredProps.filter(p =>
|
|
441
|
+
p.name.toLowerCase().includes('name') ||
|
|
442
|
+
p.name.toLowerCase().includes('title')
|
|
443
|
+
).slice(0, 2));
|
|
444
|
+
|
|
445
|
+
// Add email if available
|
|
446
|
+
const emailProp = filteredProps.find(p => p.name.toLowerCase().includes('email'));
|
|
447
|
+
if (emailProp && !priorityProps.includes(emailProp)) {
|
|
448
|
+
priorityProps.push(emailProp);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Add status/active properties
|
|
452
|
+
const statusProp = filteredProps.find(p =>
|
|
453
|
+
p.name.toLowerCase().includes('active') ||
|
|
454
|
+
p.name.toLowerCase().includes('status')
|
|
455
|
+
);
|
|
456
|
+
if (statusProp && !priorityProps.includes(statusProp)) {
|
|
457
|
+
priorityProps.push(statusProp);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Fill remaining slots with other properties (max 6 total)
|
|
461
|
+
const remainingProps = filteredProps.filter(p => !priorityProps.includes(p));
|
|
462
|
+
priorityProps.push(...remainingProps.slice(0, 6 - priorityProps.length));
|
|
463
|
+
|
|
464
|
+
const displayProps = priorityProps.slice(0, 6);
|
|
465
|
+
|
|
466
|
+
const headers = displayProps.map(p => `<th className="text-left p-3 font-medium">${p.name}</th>`).join('\n ');
|
|
467
|
+
|
|
468
|
+
const cells = displayProps.map(p => {
|
|
469
|
+
const propName = p.name.charAt(0).toLowerCase() + p.name.slice(1);
|
|
470
|
+
|
|
471
|
+
if (p.type.toLowerCase() === 'bool' || p.type.toLowerCase() === 'boolean') {
|
|
472
|
+
return `<td className="p-3">
|
|
473
|
+
<span className={\`px-2 py-1 text-xs rounded \${item.${propName} ? 'bg-green-500/10 text-green-600' : 'bg-gray-500/10 text-gray-600'}\`}>
|
|
474
|
+
{item.${propName} ? 'Yes' : 'No'}
|
|
475
|
+
</span>
|
|
476
|
+
</td>`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (p.type.toLowerCase() === 'datetime') {
|
|
480
|
+
return `<td className="p-3 text-[var(--text-secondary)]">{new Date(item.${propName}).toLocaleDateString()}</td>`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return `<td className="p-3">{item.${propName}}</td>`;
|
|
484
|
+
}).join('\n ');
|
|
485
|
+
|
|
486
|
+
return ` {/* Table */}
|
|
487
|
+
<div className="overflow-x-auto">
|
|
488
|
+
<table className="w-full text-sm">
|
|
489
|
+
<thead className="bg-[var(--bg-secondary)]">
|
|
490
|
+
<tr>
|
|
491
|
+
${headers}
|
|
492
|
+
<th className="text-right p-3 font-medium">Actions</th>
|
|
493
|
+
</tr>
|
|
494
|
+
</thead>
|
|
495
|
+
<tbody>
|
|
496
|
+
{mock${capitalize(module)}.map((item, idx) => (
|
|
497
|
+
<tr key={idx} className="border-b border-[var(--border-color)] hover:bg-[var(--bg-hover)]">
|
|
498
|
+
${cells}
|
|
499
|
+
<td className="p-3 text-right">
|
|
500
|
+
<div className="flex items-center justify-end gap-1">
|
|
501
|
+
<button className="p-1.5 hover:bg-[var(--bg-secondary)] rounded">
|
|
502
|
+
<Eye className="w-4 h-4" />
|
|
503
|
+
</button>
|
|
504
|
+
<button className="p-1.5 hover:bg-[var(--bg-secondary)] rounded text-[var(--error-text)]">
|
|
505
|
+
<Trash2 className="w-4 h-4" />
|
|
506
|
+
</button>
|
|
507
|
+
</div>
|
|
508
|
+
</td>
|
|
509
|
+
</tr>
|
|
510
|
+
))}
|
|
511
|
+
</tbody>
|
|
512
|
+
</table>
|
|
513
|
+
</div>`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Generate complete TSX documentation file
|
|
518
|
+
*/
|
|
519
|
+
function generateDocumentationTSX(
|
|
520
|
+
moduleInfo: ModuleInfo,
|
|
521
|
+
properties: EntityProperty[],
|
|
522
|
+
apiEndpoints: any[],
|
|
523
|
+
businessRules: any[],
|
|
524
|
+
pageStructure: PageStructure
|
|
525
|
+
): string {
|
|
526
|
+
const { module, context, application } = moduleInfo;
|
|
527
|
+
const ModuleName = capitalize(module);
|
|
528
|
+
|
|
529
|
+
// Generate mock data
|
|
530
|
+
const mockDataCode = generateMockData(properties, module);
|
|
531
|
+
|
|
532
|
+
// Generate KPI stats
|
|
533
|
+
const kpiStatsCode = generateKPIStats(properties, module);
|
|
534
|
+
const kpiSectionUI = generateKPISection(module);
|
|
535
|
+
|
|
536
|
+
// Generate table UI
|
|
537
|
+
const tableMockUI = generateTableMockUI(properties, module);
|
|
538
|
+
|
|
539
|
+
// Generate form section
|
|
540
|
+
const formSectionUI = generateFormSection(properties, module);
|
|
541
|
+
|
|
542
|
+
// Generate API endpoints mock data
|
|
543
|
+
const apiEndpointsCode = `const apiEndpoints = ${JSON.stringify(apiEndpoints, null, 2)};`;
|
|
544
|
+
|
|
545
|
+
return `import { Link } from 'react-router-dom';
|
|
546
|
+
import { useTranslation } from 'react-i18next';
|
|
547
|
+
import {
|
|
548
|
+
ArrowRight,
|
|
549
|
+
Shield,
|
|
550
|
+
Eye,
|
|
551
|
+
Trash2,
|
|
552
|
+
Plus,
|
|
553
|
+
Info,
|
|
554
|
+
MessageSquare,
|
|
555
|
+
HelpCircle,
|
|
556
|
+
} from 'lucide-react';
|
|
557
|
+
|
|
558
|
+
// Mock data for ${ModuleName}
|
|
559
|
+
${mockDataCode}
|
|
560
|
+
|
|
561
|
+
// KPI Stats
|
|
562
|
+
${kpiStatsCode}
|
|
563
|
+
|
|
564
|
+
// API endpoints
|
|
565
|
+
${apiEndpointsCode}
|
|
566
|
+
|
|
567
|
+
export function ${ModuleName}DocPage() {
|
|
568
|
+
const { t } = useTranslation('docs');
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<div className="space-y-8">
|
|
572
|
+
{/* Breadcrumb */}
|
|
573
|
+
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
574
|
+
<Link to="/system/docs" className="hover:text-[var(--color-primary-600)]">Documentation</Link>
|
|
575
|
+
<span>/</span>
|
|
576
|
+
<Link to="/system/docs/user" className="hover:text-[var(--color-primary-600)]">User</Link>
|
|
577
|
+
<span>/</span>
|
|
578
|
+
<Link to="/system/docs/user/${context}" className="hover:text-[var(--color-primary-600)]">${capitalize(context)}</Link>
|
|
579
|
+
<span>/</span>
|
|
580
|
+
<span>${ModuleName}</span>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{/* Header */}
|
|
584
|
+
<div>
|
|
585
|
+
<h1 className="text-3xl font-bold mb-4">
|
|
586
|
+
${ModuleName} Management
|
|
587
|
+
</h1>
|
|
588
|
+
<p className="text-lg text-[var(--text-secondary)]">
|
|
589
|
+
Complete management interface for ${module}
|
|
590
|
+
</p>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
{/* Section 1: Introduction */}
|
|
594
|
+
<section id="introduction" className="card p-6">
|
|
595
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|
596
|
+
<span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">1</span>
|
|
597
|
+
Introduction
|
|
598
|
+
</h2>
|
|
599
|
+
<p className="text-[var(--text-secondary)]">
|
|
600
|
+
The ${ModuleName} module provides comprehensive management capabilities including
|
|
601
|
+
viewing, creating, updating, and deleting ${module}.
|
|
602
|
+
</p>
|
|
603
|
+
</section>
|
|
604
|
+
|
|
605
|
+
{/* Section 2: Access */}
|
|
606
|
+
<section id="access" className="card p-6">
|
|
607
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|
608
|
+
<span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">2</span>
|
|
609
|
+
Access
|
|
610
|
+
</h2>
|
|
611
|
+
|
|
612
|
+
<div className="mb-4">
|
|
613
|
+
<div className="text-sm font-medium mb-2">Navigation</div>
|
|
614
|
+
<div className="flex items-center gap-2 text-sm bg-[var(--bg-secondary)] p-3 rounded-lg">
|
|
615
|
+
<span className="px-2 py-1 bg-[var(--bg-primary)] rounded">${capitalize(context)}</span>
|
|
616
|
+
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)]" />
|
|
617
|
+
<span className="px-2 py-1 bg-[var(--bg-primary)] rounded">${capitalize(application)}</span>
|
|
618
|
+
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)]" />
|
|
619
|
+
<span className="px-2 py-1 bg-[var(--color-primary-600)] text-white rounded">${ModuleName}</span>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<div className="mb-4">
|
|
624
|
+
<div className="text-sm font-medium mb-2">URL</div>
|
|
625
|
+
<code className="block p-3 bg-[var(--bg-secondary)] rounded-lg text-sm font-mono">
|
|
626
|
+
/${context}/${application}/${module}
|
|
627
|
+
</code>
|
|
628
|
+
</div>
|
|
629
|
+
</section>
|
|
630
|
+
|
|
631
|
+
{/* Section 3: Overview - MOCK UI */}
|
|
632
|
+
<section id="overview" className="card p-6">
|
|
633
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|
634
|
+
<span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">3</span>
|
|
635
|
+
Interface Overview
|
|
636
|
+
</h2>
|
|
637
|
+
<p className="text-[var(--text-secondary)] mb-4">
|
|
638
|
+
The main ${module} interface displays all records in a table format with search and filter capabilities.
|
|
639
|
+
</p>
|
|
640
|
+
|
|
641
|
+
{/* Mock UI - Complete Interface */}
|
|
642
|
+
<div className="border border-[var(--border-color)] rounded-lg overflow-hidden">
|
|
643
|
+
{/* Header */}
|
|
644
|
+
<div className="p-4 border-b border-[var(--border-color)] flex items-center justify-between">
|
|
645
|
+
<div>
|
|
646
|
+
<div className="font-bold">${ModuleName} Management</div>
|
|
647
|
+
<div className="text-sm text-[var(--text-secondary)]">Manage all ${module}</div>
|
|
648
|
+
</div>
|
|
649
|
+
<button className="px-4 py-2 bg-[var(--color-primary-600)] text-white rounded-lg flex items-center gap-2 text-sm">
|
|
650
|
+
<Plus className="w-4 h-4" />
|
|
651
|
+
New ${ModuleName}
|
|
652
|
+
</button>
|
|
653
|
+
</div>
|
|
654
|
+
|
|
655
|
+
${kpiSectionUI}
|
|
656
|
+
|
|
657
|
+
{/* Table */}
|
|
658
|
+
${tableMockUI}
|
|
659
|
+
</div>
|
|
660
|
+
</section>
|
|
661
|
+
|
|
662
|
+
${formSectionUI}
|
|
663
|
+
|
|
664
|
+
{/* Section 5: API Reference */}
|
|
665
|
+
<section id="api" className="card p-6">
|
|
666
|
+
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
|
667
|
+
<span className="w-8 h-8 rounded-full bg-[var(--color-primary-600)] text-white flex items-center justify-center text-sm font-bold">5</span>
|
|
668
|
+
API Reference
|
|
669
|
+
</h2>
|
|
670
|
+
|
|
671
|
+
<div className="overflow-x-auto">
|
|
672
|
+
<table className="w-full text-sm">
|
|
673
|
+
<thead>
|
|
674
|
+
<tr className="bg-[var(--bg-secondary)]">
|
|
675
|
+
<th className="text-left py-2 px-3 rounded-tl-lg">Method</th>
|
|
676
|
+
<th className="text-left py-2 px-3">Endpoint</th>
|
|
677
|
+
<th className="text-left py-2 px-3 rounded-tr-lg">Handler</th>
|
|
678
|
+
</tr>
|
|
679
|
+
</thead>
|
|
680
|
+
<tbody>
|
|
681
|
+
{apiEndpoints.map((endpoint, index) => (
|
|
682
|
+
<tr key={index} className="border-b border-[var(--border-color)]">
|
|
683
|
+
<td className="py-2 px-3">
|
|
684
|
+
<span className={\`px-2 py-0.5 rounded text-xs font-medium \${
|
|
685
|
+
endpoint.method === 'GET' ? 'bg-green-500/10 text-green-600' :
|
|
686
|
+
endpoint.method === 'POST' ? 'bg-yellow-500/10 text-yellow-600' :
|
|
687
|
+
endpoint.method === 'PUT' ? 'bg-blue-500/10 text-blue-600' :
|
|
688
|
+
'bg-red-500/10 text-red-600'
|
|
689
|
+
}\`}>
|
|
690
|
+
{endpoint.method}
|
|
691
|
+
</span>
|
|
692
|
+
</td>
|
|
693
|
+
<td className="py-2 px-3 font-mono text-xs">{endpoint.path}</td>
|
|
694
|
+
<td className="py-2 px-3 text-[var(--text-secondary)]">{endpoint.handler}</td>
|
|
695
|
+
</tr>
|
|
696
|
+
))}
|
|
697
|
+
</tbody>
|
|
698
|
+
</table>
|
|
699
|
+
</div>
|
|
700
|
+
</section>
|
|
701
|
+
</div>
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Capitalize first letter
|
|
709
|
+
*/
|
|
710
|
+
function capitalize(str: string): string {
|
|
711
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Simple singularize function for common English plurals
|
|
716
|
+
*/
|
|
717
|
+
function singularize(str: string): string {
|
|
718
|
+
if (str.endsWith('ies')) {
|
|
719
|
+
return str.slice(0, -3) + 'y';
|
|
720
|
+
}
|
|
721
|
+
if (str.endsWith('sses') || str.endsWith('shes') || str.endsWith('ches') || str.endsWith('xes')) {
|
|
722
|
+
return str.slice(0, -2);
|
|
723
|
+
}
|
|
724
|
+
if (str.endsWith('s') && !str.endsWith('ss')) {
|
|
725
|
+
return str.slice(0, -1);
|
|
726
|
+
}
|
|
727
|
+
return str;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Main execution
|
|
732
|
+
*/
|
|
733
|
+
async function main() {
|
|
734
|
+
const moduleInfo = parseArgs();
|
|
735
|
+
const { module, context, application, appPath } = moduleInfo;
|
|
736
|
+
|
|
737
|
+
console.error(`\n🚀 Generating documentation for: ${context}/${application}/${module}\n`);
|
|
738
|
+
|
|
739
|
+
// 1. Find entity file
|
|
740
|
+
console.error('📁 Finding entity file...');
|
|
741
|
+
const entityPath = await findEntityFile(appPath, module);
|
|
742
|
+
if (!entityPath) {
|
|
743
|
+
console.error(`❌ Entity not found for module: ${module}`);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
console.error(`✅ Found: ${entityPath}`);
|
|
747
|
+
|
|
748
|
+
// 2. Extract entity properties
|
|
749
|
+
console.error('\n📊 Extracting entity properties...');
|
|
750
|
+
const properties = extractEntityProperties(entityPath);
|
|
751
|
+
console.error(`✅ Extracted ${properties.length} properties`);
|
|
752
|
+
|
|
753
|
+
// 3. Find page file
|
|
754
|
+
console.error('\n📄 Finding React page...');
|
|
755
|
+
const pagePath = await findPageFile(appPath, module, context, application);
|
|
756
|
+
if (pagePath) {
|
|
757
|
+
console.error(`✅ Found: ${pagePath}`);
|
|
758
|
+
} else {
|
|
759
|
+
console.error(`⚠️ Page not found (will use defaults)`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 4. Analyze page structure
|
|
763
|
+
const pageStructure = pagePath ? analyzePageStructure(pagePath) : {
|
|
764
|
+
hasTable: true,
|
|
765
|
+
hasKPIs: false,
|
|
766
|
+
hasForm: false,
|
|
767
|
+
hasFilters: false,
|
|
768
|
+
columns: [],
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// 5. Extract API endpoints using existing script
|
|
772
|
+
console.error('\n🔌 Extracting API endpoints...');
|
|
773
|
+
let apiEndpoints: any[] = [];
|
|
774
|
+
try {
|
|
775
|
+
const endpointsOutput = execSync(
|
|
776
|
+
`npx tsx "${__dirname}/extract-api-endpoints.ts" --module ${module} --app-path "${appPath}"`,
|
|
777
|
+
{ encoding: 'utf-8' }
|
|
778
|
+
);
|
|
779
|
+
apiEndpoints = JSON.parse(endpointsOutput);
|
|
780
|
+
console.error(`✅ Extracted ${apiEndpoints.length} endpoints`);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.error(`⚠️ Could not extract endpoints`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 6. Extract business rules using existing script
|
|
786
|
+
console.error('\n📋 Extracting business rules...');
|
|
787
|
+
let businessRules: any[] = [];
|
|
788
|
+
try {
|
|
789
|
+
const rulesOutput = execSync(
|
|
790
|
+
`npx tsx "${__dirname}/extract-business-rules.ts" --module ${module} --app-path "${appPath}"`,
|
|
791
|
+
{ encoding: 'utf-8' }
|
|
792
|
+
);
|
|
793
|
+
businessRules = JSON.parse(rulesOutput);
|
|
794
|
+
console.error(`✅ Extracted ${businessRules.length} business rules`);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
console.error(`⚠️ Could not extract business rules`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// 7. Generate documentation TSX
|
|
800
|
+
console.error('\n✨ Generating documentation TSX...');
|
|
801
|
+
const tsx = generateDocumentationTSX(moduleInfo, properties, apiEndpoints, businessRules, pageStructure);
|
|
802
|
+
|
|
803
|
+
// 8. Output
|
|
804
|
+
console.log(tsx);
|
|
805
|
+
console.error('\n✅ Documentation generated successfully!');
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
main().catch((error) => {
|
|
809
|
+
console.error('❌ Fatal error:', error);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
});
|