@team-codeme/crud-forge 1.0.1 → 1.0.2
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/README.md +4 -4
- package/Todo.md +1 -1
- package/cli.js +1 -0
- package/issues/README.md +17 -0
- package/issues/closed/ISSUE-001-fuse-user-management-404.md +44 -0
- package/issues/closed/ISSUE-002-list-UI-issues.md +99 -0
- package/issues/closed/ISSUE-003-list-UI-issues.md +70 -0
- package/issues/closed/ISSUE-004-re-generator-issue.md +110 -0
- package/issues/open/.gitkeep +0 -0
- package/issues/templates/issue.md +50 -0
- package/package.json +3 -3
- package/src/commands/generate.js +46 -15
- package/src/generator/auth-pack.js +111 -7
- package/src/generator/entity-layout.js +9 -5
- package/src/generator/index.js +125 -45
- package/src/generator/project-output.js +3 -2
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.ts.ejs +3 -2
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs +3 -3
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs +13 -11
- package/src/templates/angular/iam/user-list.component.html.ejs +4 -4
- package/src/templates/angular/iam/user-list.component.ts.ejs +11 -2
- package/src/templates/angular/shared/base/base-list.component.ts.ejs +27 -11
- package/src/utils/merger.js +2 -1
- package/test/list-state.js +72 -1
- package/test/regeneration-isolation.js +94 -0
|
@@ -347,6 +347,76 @@ function buildScreenAccessCatalog({ entities = [], folderName = 'micro-functions
|
|
|
347
347
|
return { csv: `${csv}\n`, screens, rows };
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
function parseCsvLine(line) {
|
|
351
|
+
const cells = [];
|
|
352
|
+
let current = '';
|
|
353
|
+
let inQuotes = false;
|
|
354
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
355
|
+
const ch = line[i];
|
|
356
|
+
if (inQuotes) {
|
|
357
|
+
if (ch === '"' && line[i + 1] === '"') {
|
|
358
|
+
current += '"';
|
|
359
|
+
i += 1;
|
|
360
|
+
} else if (ch === '"') {
|
|
361
|
+
inQuotes = false;
|
|
362
|
+
} else {
|
|
363
|
+
current += ch;
|
|
364
|
+
}
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (ch === '"') {
|
|
368
|
+
inQuotes = true;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (ch === ',') {
|
|
372
|
+
cells.push(current);
|
|
373
|
+
current = '';
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
current += ch;
|
|
377
|
+
}
|
|
378
|
+
cells.push(current);
|
|
379
|
+
return cells;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function parseScreenComponentsCsv(text) {
|
|
383
|
+
const lines = String(text || '')
|
|
384
|
+
.split(/\r?\n/)
|
|
385
|
+
.map((line) => line.trim())
|
|
386
|
+
.filter(Boolean);
|
|
387
|
+
if (lines.length < 2) return [];
|
|
388
|
+
const rows = [];
|
|
389
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
390
|
+
const cells = parseCsvLine(lines[i]);
|
|
391
|
+
if (cells.length < 5) continue;
|
|
392
|
+
rows.push({
|
|
393
|
+
screenName: cells[0] || '',
|
|
394
|
+
screenId: cells[1] || '',
|
|
395
|
+
componentName: cells[2] || '',
|
|
396
|
+
componentId: cells[3] || '',
|
|
397
|
+
componentType: cells[4] || '',
|
|
398
|
+
description: cells[5] || '',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return rows;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function screensFromCsvRows(rows) {
|
|
405
|
+
const seen = new Set();
|
|
406
|
+
const screens = [];
|
|
407
|
+
for (const row of rows) {
|
|
408
|
+
const type = String(row.componentType || '').toUpperCase();
|
|
409
|
+
const isScreen = type === 'SCREEN' || row.componentId === row.screenId;
|
|
410
|
+
if (!isScreen || !row.screenId || seen.has(row.screenId)) continue;
|
|
411
|
+
seen.add(row.screenId);
|
|
412
|
+
screens.push({
|
|
413
|
+
id: row.screenId,
|
|
414
|
+
label: row.screenName || row.screenId.split('/').pop()?.replace(/-/g, ' ') || row.screenId,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return screens;
|
|
418
|
+
}
|
|
419
|
+
|
|
350
420
|
async function generateIamModule({ outputDir, templatesDir, locals, writeOrPreview, folderName, config, entities = [] }) {
|
|
351
421
|
const iamRoot = path.join(outputDir, 'src/app/core/user-management');
|
|
352
422
|
const catalog = buildScreenAccessCatalog({
|
|
@@ -354,10 +424,40 @@ async function generateIamModule({ outputDir, templatesDir, locals, writeOrPrevi
|
|
|
354
424
|
folderName: folderName || 'micro-functions',
|
|
355
425
|
iam: locals.iam || {},
|
|
356
426
|
});
|
|
357
|
-
|
|
427
|
+
|
|
428
|
+
const csvPath = path.join(outputDir, 'public/assets/screen-components.csv');
|
|
429
|
+
let finalRows = catalog.rows;
|
|
430
|
+
let finalScreens = catalog.screens;
|
|
431
|
+
|
|
432
|
+
if (fs.existsSync(csvPath)) {
|
|
433
|
+
try {
|
|
434
|
+
const existingCsvText = fs.readFileSync(csvPath, 'utf8');
|
|
435
|
+
const existingRows = parseScreenComponentsCsv(existingCsvText);
|
|
436
|
+
const newScreenIds = new Set(catalog.screens.map((s) => s.id));
|
|
437
|
+
const preservedRows = existingRows.filter((r) => !newScreenIds.has(r.screenId));
|
|
438
|
+
if (preservedRows.length > 0) {
|
|
439
|
+
finalRows = [...preservedRows, ...catalog.rows];
|
|
440
|
+
finalScreens = screensFromCsvRows(finalRows);
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
/* ignore and use fresh catalog */
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const header = 'screenName,screenId,componentName,componentId,componentType,description';
|
|
448
|
+
const mergedCsv = [
|
|
449
|
+
header,
|
|
450
|
+
...finalRows.map((row) =>
|
|
451
|
+
[row.screenName, row.screenId, row.componentName, row.componentId, row.componentType, row.description]
|
|
452
|
+
.map(csvEscape)
|
|
453
|
+
.join(',')
|
|
454
|
+
),
|
|
455
|
+
].join('\n') + '\n';
|
|
456
|
+
|
|
457
|
+
locals = { ...locals, managedScreens: finalScreens };
|
|
358
458
|
|
|
359
459
|
// PMB-style ACL registry — matrices load this CSV at runtime.
|
|
360
|
-
writeOrPreview(
|
|
460
|
+
writeOrPreview(csvPath, mergedCsv, { bypassMerge: true });
|
|
361
461
|
|
|
362
462
|
/** Flat template name → foldered output under src/app/core/user-management */
|
|
363
463
|
const iamLayout = [
|
|
@@ -531,7 +631,11 @@ async function generateIamModule({ outputDir, templatesDir, locals, writeOrPrevi
|
|
|
531
631
|
|
|
532
632
|
for (const entry of iamLayout) {
|
|
533
633
|
const rendered = await renderTemplate(path.join(templatesDir, 'iam', `${entry.template}.ejs`), locals);
|
|
534
|
-
writeOrPreview(
|
|
634
|
+
writeOrPreview(
|
|
635
|
+
path.join(iamRoot, entry.out),
|
|
636
|
+
rendered,
|
|
637
|
+
entry.template === 'screen-catalog.ts' ? { bypassMerge: true } : {}
|
|
638
|
+
);
|
|
535
639
|
generatedFiles += 1;
|
|
536
640
|
if (entry.html) {
|
|
537
641
|
const htmlRendered = await renderTemplate(path.join(templatesDir, 'iam', `${entry.html}.ejs`), locals);
|
|
@@ -668,7 +772,7 @@ function registerIamRoutes(outputDir, config, folderName, writeOrPreview) {
|
|
|
668
772
|
legacyImport,
|
|
669
773
|
"loadChildren: () => import('@/app/core/user-management/user-management.routes')"
|
|
670
774
|
);
|
|
671
|
-
writeOrPreview(routesFilePath, content);
|
|
775
|
+
writeOrPreview(routesFilePath, content, { bypassMerge: true });
|
|
672
776
|
return;
|
|
673
777
|
}
|
|
674
778
|
|
|
@@ -680,7 +784,7 @@ function registerIamRoutes(outputDir, config, folderName, writeOrPreview) {
|
|
|
680
784
|
} else {
|
|
681
785
|
content = content.replace('children: [', `children: [\n${iamRoute}`);
|
|
682
786
|
}
|
|
683
|
-
writeOrPreview(routesFilePath, content);
|
|
787
|
+
writeOrPreview(routesFilePath, content, { bypassMerge: true });
|
|
684
788
|
}
|
|
685
789
|
|
|
686
790
|
function registerIamNavigation(outputDir, config, folderName, writeOrPreview) {
|
|
@@ -705,7 +809,7 @@ function registerIamNavigation(outputDir, config, folderName, writeOrPreview) {
|
|
|
705
809
|
/id: 'identity-group',\n\s*label: '[^']*',(?:\n\s*description: '[^']*',)?\n\s*icon: '[^']*'/,
|
|
706
810
|
`id: 'identity-group',\n label: 'User Management',\n icon: 'users'`
|
|
707
811
|
);
|
|
708
|
-
writeOrPreview(navFilePath, content);
|
|
812
|
+
writeOrPreview(navFilePath, content, { bypassMerge: true });
|
|
709
813
|
return;
|
|
710
814
|
}
|
|
711
815
|
|
|
@@ -717,7 +821,7 @@ function registerIamNavigation(outputDir, config, folderName, writeOrPreview) {
|
|
|
717
821
|
`export const NAVIGATION: NavigationItem[] = [\n${block}`
|
|
718
822
|
);
|
|
719
823
|
}
|
|
720
|
-
writeOrPreview(navFilePath, content);
|
|
824
|
+
writeOrPreview(navFilePath, content, { bypassMerge: true });
|
|
721
825
|
}
|
|
722
826
|
|
|
723
827
|
function patchAppConfig(outputDir, writeOrPreview) {
|
|
@@ -2,14 +2,18 @@ const path = require('path');
|
|
|
2
2
|
const { kebabCase } = require('lodash');
|
|
3
3
|
|
|
4
4
|
// Source folders name service modules; the common container is always micro-functions.
|
|
5
|
-
function serviceFolder(entity, config) {
|
|
6
|
-
const multiSource = Array.isArray(config
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
function serviceFolder(entity, config = {}) {
|
|
6
|
+
const multiSource = Array.isArray(config?.input?.sources) && config.input.sources.length > 0;
|
|
7
|
+
const folder = entity.folderName && entity.folderName !== 'micro-functions' ? entity.folderName : null;
|
|
8
|
+
let module = multiSource
|
|
9
|
+
? (folder || kebabCase(entity.sourceName || entity.microservice || 'microservice'))
|
|
10
|
+
: kebabCase(folder || entity.microservice || config?.api?.microservice || 'microservice');
|
|
10
11
|
if (!module || /[\\/]/.test(module) || ['.', '..'].includes(module)) {
|
|
11
12
|
throw new Error('Microservice module must be a single folder name.');
|
|
12
13
|
}
|
|
14
|
+
if (module === 'micro-functions') {
|
|
15
|
+
return 'micro-functions';
|
|
16
|
+
}
|
|
13
17
|
return `micro-functions/${module}`;
|
|
14
18
|
}
|
|
15
19
|
|
package/src/generator/index.js
CHANGED
|
@@ -77,7 +77,7 @@ async function generate(parsedData, config) {
|
|
|
77
77
|
}
|
|
78
78
|
return true;
|
|
79
79
|
};
|
|
80
|
-
fs.copySync(starterPackPath, outputDir, { filter: filterFn });
|
|
80
|
+
fs.copySync(starterPackPath, outputDir, { filter: filterFn, overwrite: false });
|
|
81
81
|
cleanupStaleKeycloakArtifacts(outputDir, config);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -95,7 +95,7 @@ async function generate(parsedData, config) {
|
|
|
95
95
|
const conflicts = [];
|
|
96
96
|
const { stampGeneratedBanner } = require('./generated-banner');
|
|
97
97
|
|
|
98
|
-
const writeOrPreview = (filePath, rawContent) => {
|
|
98
|
+
const writeOrPreview = (filePath, rawContent, writeOpts = {}) => {
|
|
99
99
|
const content = stampGeneratedBanner(filePath, rawContent);
|
|
100
100
|
const relativePath = path.relative(outputDir, filePath);
|
|
101
101
|
const exists = fs.existsSync(filePath);
|
|
@@ -138,6 +138,12 @@ async function generate(parsedData, config) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
if (policy === 'merge') {
|
|
141
|
+
if (writeOpts.bypassMerge) {
|
|
142
|
+
fs.ensureDirSync(path.dirname(filePath));
|
|
143
|
+
fs.writeFileSync(filePath, content);
|
|
144
|
+
fileCount++;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
141
147
|
const { mergeContent } = require('../utils/merger');
|
|
142
148
|
const merged = mergeContent(existingContent, content);
|
|
143
149
|
if (merged !== existingContent) {
|
|
@@ -451,23 +457,50 @@ async function generate(parsedData, config) {
|
|
|
451
457
|
content = content.replace(/\s*\/\/\s*-+\n\s*\/\/\s*(Dashboards|General|Documentation)\n\s*\/\/\s*-+\n(?:[\s\S]*?(?=\s*\/\/\s*-+|\s*\/\/\s*404|\]))/g, '\n');
|
|
452
458
|
}
|
|
453
459
|
|
|
454
|
-
|
|
460
|
+
// Merge with existing routes between // cforge-routes-start and // cforge-routes-end
|
|
461
|
+
const existingRoutesMap = new Map();
|
|
462
|
+
const cforgeRoutesMatch = content.match(/\/\/ cforge-routes-start\s*([\s\S]*?)\/\/ cforge-routes-end/);
|
|
463
|
+
if (cforgeRoutesMatch && cforgeRoutesMatch[1]) {
|
|
464
|
+
const routeRegex = /\{\s*path:\s*['"]([^'"]+)['"][\s\S]*?\n\s*\},?/g;
|
|
465
|
+
let m;
|
|
466
|
+
while ((m = routeRegex.exec(cforgeRoutesMatch[1])) !== null) {
|
|
467
|
+
const p = m[1];
|
|
468
|
+
if (p && p !== 'user-management') {
|
|
469
|
+
existingRoutesMap.set(p, m[0].trim().replace(/,$/, ''));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const entity of entities) {
|
|
455
475
|
const kebabName = helpers.toKebabCase(entity.name);
|
|
456
|
-
|
|
457
|
-
|
|
476
|
+
const routeStr = `{\n path: '${kebabName}',\n loadChildren: () => import('./modules/apps/${outputEntityFolder(entity, config)}/${kebabName}/${kebabName}.routes'),\n }`;
|
|
477
|
+
existingRoutesMap.set(kebabName, routeStr);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const mergedRoutes = Array.from(existingRoutesMap.values())
|
|
481
|
+
.map(r => ` ${r},`)
|
|
482
|
+
.join('\n');
|
|
458
483
|
|
|
459
484
|
if (config.fuse.cleanLayout) {
|
|
460
485
|
let content = fs.readFileSync(routesFilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
461
486
|
const childrenMatch = /children:\s*\[/.exec(content);
|
|
462
487
|
if (childrenMatch) {
|
|
463
|
-
|
|
488
|
+
// Preserve IAM user-management route added by auth-pack; cleanLayout rewrites children.
|
|
489
|
+
const iamRouteMatch = content.match(
|
|
490
|
+
/\{\s*path:\s*['"]user-management['"][\s\S]*?loadChildren:\s*\(\)\s*=>\s*import\(['"][^'"]+user-management\.routes['"]\)\s*,?\s*\},?/,
|
|
491
|
+
);
|
|
492
|
+
const iamRouteBlock = iamRouteMatch
|
|
493
|
+
? ` ${iamRouteMatch[0].replace(/,\s*$/, '')},\n`
|
|
494
|
+
: '';
|
|
495
|
+
|
|
496
|
+
const cleanChildrenBlock = `children: [
|
|
464
497
|
{ path: '', pathMatch: 'full', redirectTo: '/auth/sign-in' },
|
|
465
498
|
{
|
|
466
499
|
path: 'welcome',
|
|
467
500
|
loadComponent: () => import('./modules/extras/welcome/welcome.component')
|
|
468
501
|
},
|
|
469
|
-
// cforge-routes-start
|
|
470
|
-
${
|
|
502
|
+
${iamRouteBlock} // cforge-routes-start
|
|
503
|
+
${mergedRoutes}
|
|
471
504
|
// cforge-routes-end
|
|
472
505
|
{
|
|
473
506
|
path: '404',
|
|
@@ -478,16 +511,29 @@ ${routesBlock}
|
|
|
478
511
|
{
|
|
479
512
|
path: '**',
|
|
480
513
|
loadComponent: () => import('./modules/extras/error/features/error-page'),
|
|
481
|
-
data: { errorCode: 404, title: 'Page not found' }
|
|
514
|
+
data: { errorCode: 404, title: 'Page not found' }
|
|
482
515
|
},
|
|
483
516
|
]`;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
517
|
+
let depth = 1;
|
|
518
|
+
let childrenEndIdx = -1;
|
|
519
|
+
const searchStart = childrenMatch.index + childrenMatch[0].length;
|
|
520
|
+
for (let i = searchStart; i < content.length; i++) {
|
|
521
|
+
if (content[i] === '[') depth++;
|
|
522
|
+
else if (content[i] === ']') {
|
|
523
|
+
depth--;
|
|
524
|
+
if (depth === 0) {
|
|
525
|
+
childrenEndIdx = i;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (childrenEndIdx !== -1) {
|
|
531
|
+
const newContent = content.slice(0, childrenMatch.index) + cleanChildrenBlock + content.slice(childrenEndIdx + 1);
|
|
532
|
+
writeOrPreview(routesFilePath, newContent, { bypassMerge: true });
|
|
488
533
|
}
|
|
489
534
|
}
|
|
490
535
|
} else {
|
|
536
|
+
let content = fs.readFileSync(routesFilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
491
537
|
// Ensure markers exist
|
|
492
538
|
if (!content.includes('// cforge-routes-start') || !content.includes('// cforge-routes-end')) {
|
|
493
539
|
const childrenMatch = /children:\s*\[/.exec(content);
|
|
@@ -502,23 +548,55 @@ ${routesBlock}
|
|
|
502
548
|
|
|
503
549
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
504
550
|
const startMarker = '// cforge-routes-start';
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
writeOrPreview(routesFilePath, newContent);
|
|
551
|
+
const newContent = content.slice(0, startIdx + startMarker.length) + '\n' + mergedRoutes + '\n ' + content.slice(endIdx);
|
|
552
|
+
writeOrPreview(routesFilePath, newContent, { bypassMerge: true });
|
|
508
553
|
}
|
|
509
554
|
}
|
|
510
555
|
}
|
|
511
556
|
|
|
512
557
|
// 2. Update Navigation File
|
|
513
558
|
if (config.generate.registrations?.navigation !== false && fs.existsSync(navFilePath)) {
|
|
514
|
-
|
|
515
|
-
|
|
516
559
|
let content = fs.readFileSync(navFilePath, 'utf8');
|
|
517
560
|
|
|
518
561
|
if (config.fuse?.includePrebuilt === false && !config.fuse?.cleanLayout) {
|
|
519
562
|
content = content.replace(/\s*{\s*id:\s*'(dashboards|general|extras|documentation)'[\s\S]*?(?=\s*{\s*id:\s*'(dashboards|general|extras|documentation)'|\s*\];)/g, '');
|
|
520
563
|
}
|
|
521
564
|
|
|
565
|
+
// Extract existing navigation groups from NAVIGATION array
|
|
566
|
+
const existingNavGroupsMap = new Map();
|
|
567
|
+
const navArrayStart = content.indexOf('export const NAVIGATION: NavigationItem[] = [');
|
|
568
|
+
if (navArrayStart !== -1) {
|
|
569
|
+
const afterStart = content.slice(navArrayStart + 'export const NAVIGATION: NavigationItem[] = ['.length);
|
|
570
|
+
const navArrayEnd = afterStart.lastIndexOf('];');
|
|
571
|
+
const arrayBody = navArrayEnd !== -1 ? afterStart.slice(0, navArrayEnd) : afterStart;
|
|
572
|
+
|
|
573
|
+
let depth = 0;
|
|
574
|
+
let currentGroupStart = -1;
|
|
575
|
+
for (let i = 0; i < arrayBody.length; i++) {
|
|
576
|
+
const ch = arrayBody[i];
|
|
577
|
+
if (ch === '{') {
|
|
578
|
+
if (depth === 0) currentGroupStart = i;
|
|
579
|
+
depth++;
|
|
580
|
+
} else if (ch === '}') {
|
|
581
|
+
depth--;
|
|
582
|
+
if (depth === 0 && currentGroupStart !== -1) {
|
|
583
|
+
const rawGroup = arrayBody.slice(currentGroupStart, i + 1);
|
|
584
|
+
const idMatch = rawGroup.match(/id:\s*['"]([^'"]+)['"]/);
|
|
585
|
+
if (idMatch) {
|
|
586
|
+
const groupId = idMatch[1];
|
|
587
|
+
if (
|
|
588
|
+
!config.fuse.cleanLayout ||
|
|
589
|
+
!['dashboards', 'general', 'extras', 'documentation'].includes(groupId)
|
|
590
|
+
) {
|
|
591
|
+
existingNavGroupsMap.set(groupId, rawGroup.trim().replace(/,$/, ''));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
currentGroupStart = -1;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
522
600
|
const groups = new Map();
|
|
523
601
|
for (const entity of entities) {
|
|
524
602
|
const entityFolder = entity.folderName || folderName;
|
|
@@ -547,28 +625,31 @@ ${routesBlock}
|
|
|
547
625
|
});
|
|
548
626
|
}
|
|
549
627
|
|
|
550
|
-
const
|
|
551
|
-
.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
628
|
+
for (const group of groups.values()) {
|
|
629
|
+
group.items.sort((a, b) => {
|
|
630
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
631
|
+
return a.label.localeCompare(b.label);
|
|
632
|
+
});
|
|
633
|
+
const formattedItems = group.items
|
|
634
|
+
.map((item) => {
|
|
635
|
+
let str = ` {\n`;
|
|
636
|
+
str += ` id: '${item.id}',\n`;
|
|
637
|
+
str += ` label: '${item.label}',\n`;
|
|
638
|
+
str += ` icon: '${item.icon}',\n`;
|
|
639
|
+
str += ` route: '${item.route}'`;
|
|
640
|
+
if (item.permissions) {
|
|
641
|
+
str += `,\n meta: { permissions: ${JSON.stringify(item.permissions)} }`;
|
|
642
|
+
}
|
|
643
|
+
str += `\n }`;
|
|
644
|
+
return str;
|
|
645
|
+
})
|
|
646
|
+
.join(',\n');
|
|
647
|
+
const groupBlock = ` {\n id: '${group.folder}-group',\n label: '${group.label}',\n description: 'Generated CRUD Modules',\n icon: 'database',\n children: [\n${formattedItems}\n ]\n }`;
|
|
648
|
+
existingNavGroupsMap.set(`${group.folder}-group`, groupBlock.trim());
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const mergedNavigationBlock = Array.from(existingNavGroupsMap.values())
|
|
652
|
+
.map((g) => (g.startsWith(' ') ? `${g},` : ` ${g},`))
|
|
572
653
|
.join('\n');
|
|
573
654
|
|
|
574
655
|
if (config.fuse.cleanLayout) {
|
|
@@ -577,8 +658,8 @@ ${routesBlock}
|
|
|
577
658
|
if (startArrayIdx !== -1) {
|
|
578
659
|
const arrayEndIdx = normalized.lastIndexOf('];');
|
|
579
660
|
if (arrayEndIdx !== -1) {
|
|
580
|
-
const newContent = content.slice(0, startArrayIdx) + 'export const NAVIGATION: NavigationItem[] = [\n' +
|
|
581
|
-
writeOrPreview(navFilePath, newContent);
|
|
661
|
+
const newContent = content.slice(0, startArrayIdx) + 'export const NAVIGATION: NavigationItem[] = [\n' + mergedNavigationBlock + '\n];' + content.slice(arrayEndIdx + 2);
|
|
662
|
+
writeOrPreview(navFilePath, newContent, { bypassMerge: true });
|
|
582
663
|
}
|
|
583
664
|
}
|
|
584
665
|
} else {
|
|
@@ -596,9 +677,8 @@ ${routesBlock}
|
|
|
596
677
|
|
|
597
678
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
598
679
|
const startMarker = '// cforge-navigation-start';
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
writeOrPreview(navFilePath, newContent);
|
|
680
|
+
const newContent = content.slice(0, startIdx + startMarker.length) + '\n' + mergedNavigationBlock + '\n ' + content.slice(endIdx);
|
|
681
|
+
writeOrPreview(navFilePath, newContent, { bypassMerge: true });
|
|
602
682
|
}
|
|
603
683
|
}
|
|
604
684
|
}
|
|
@@ -104,8 +104,9 @@ async function generateProject(parsed, config, { configPath, inputPath, options
|
|
|
104
104
|
action = answer.action;
|
|
105
105
|
}
|
|
106
106
|
if (action === 'skip') { change.skip = true; continue; }
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if (action === 'merge' && binary) {
|
|
108
|
+
throw new Error(`Cannot merge binary file "${change.target}". Use skip, ask, or overwrite. No project files were changed.`);
|
|
109
|
+
}
|
|
109
110
|
if (action === 'merge') change.content = Buffer.from(mergeContent(change.old.toString('utf8'), change.content.toString('utf8')));
|
|
110
111
|
}
|
|
111
112
|
if (!config.dryRun) {
|
|
@@ -16,7 +16,7 @@ import { AccessCtrlPipe } from '@/app/core/auth';
|
|
|
16
16
|
selector: 'app-<%= toKebabCase(entity.name) %>-actions',
|
|
17
17
|
templateUrl: './<%= toKebabCase(entity.name) %>-actions.component.html',
|
|
18
18
|
host: {
|
|
19
|
-
class: 'block w-full
|
|
19
|
+
class: 'block w-full sm:w-auto md:shrink-0',
|
|
20
20
|
},
|
|
21
21
|
standalone: true,
|
|
22
22
|
imports: [
|
|
@@ -50,7 +50,8 @@ export class <%= entity.name %>ActionsComponent {
|
|
|
50
50
|
@Output() newClick = new EventEmitter<void>();
|
|
51
51
|
|
|
52
52
|
onSearchByChange(value: string): void {
|
|
53
|
-
this.
|
|
53
|
+
this.searchTerm = '';
|
|
54
|
+
this.searchChange.emit({ searchBy: value, searchTerm: '' });
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
onSearchTermChange(value: string): void {
|
package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs
CHANGED
|
@@ -31,9 +31,9 @@ const estimateHeaderWidth = (label) => {
|
|
|
31
31
|
<div #listViewport class="absolute inset-0 flex min-w-0 flex-col overflow-hidden">
|
|
32
32
|
|
|
33
33
|
<!-- Header (fixed) -->
|
|
34
|
-
<div class="flex flex-0 flex-col border-b border-slate-200 bg-slate-100 p-6 pb-
|
|
35
|
-
<div class="min-w-0 flex-1">
|
|
36
|
-
<h2 class="
|
|
34
|
+
<div class="flex flex-0 flex-col border-b border-slate-200 bg-slate-100 p-6 pb-20 sm:flex-row sm:items-center sm:justify-between sm:flex-wrap md:px-10 md:pb-28 md:pt-12 dark:border-slate-800 dark:bg-slate-900/60">
|
|
35
|
+
<div class="min-w-0 flex-1 shrink-0">
|
|
36
|
+
<h2 class="text-3xl font-extrabold leading-7 tracking-tight sm:leading-10 md:text-4xl">
|
|
37
37
|
<%= entity.name.replace(/([A-Z])/g, ' $1').trim() %> Management
|
|
38
38
|
</h2>
|
|
39
39
|
</div>
|
package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs
CHANGED
|
@@ -220,12 +220,6 @@ export class <%= entity.name %>ListComponent extends BaseListComponent<<%= prefe
|
|
|
220
220
|
this.restoreViewMode();
|
|
221
221
|
this.restoreColumnPreferences();
|
|
222
222
|
this.loadSearchState();
|
|
223
|
-
if (typeof localStorage !== 'undefined') {
|
|
224
|
-
const savedPageSize = localStorage.getItem(this.storageKeyPageSize);
|
|
225
|
-
if (savedPageSize) {
|
|
226
|
-
this.itemsPerPage = Number(savedPageSize);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
223
|
if (this.dialogData?.parentFilters) {
|
|
230
224
|
this.baseParentFilters = { ...this.dialogData.parentFilters };
|
|
231
225
|
}
|
|
@@ -287,13 +281,13 @@ export class <%= entity.name %>ListComponent extends BaseListComponent<<%= prefe
|
|
|
287
281
|
return;
|
|
288
282
|
}
|
|
289
283
|
|
|
290
|
-
if (
|
|
284
|
+
if (this.paginator && this.paginator.pageSize !== this.itemsPerPage) {
|
|
291
285
|
this.itemsPerPage = this.paginator.pageSize;
|
|
292
|
-
localStorage.setItem(this.storageKeyPageSize, String(this.itemsPerPage));
|
|
293
286
|
}
|
|
294
287
|
|
|
295
288
|
this.listRequest?.unsubscribe();
|
|
296
289
|
this.isLoading = true;
|
|
290
|
+
this.cdr.markForCheck();
|
|
297
291
|
const req = {
|
|
298
292
|
page: this.paginator.pageIndex,
|
|
299
293
|
size: this.paginator.pageSize,
|
|
@@ -307,12 +301,15 @@ export class <%= entity.name %>ListComponent extends BaseListComponent<<%= prefe
|
|
|
307
301
|
tap(res => {
|
|
308
302
|
this.isLoading = false;
|
|
309
303
|
this.totalItems = Number(res.headers.get('X-Total-Count') ?? 0);
|
|
310
|
-
this.
|
|
304
|
+
this.rawRows = res.body ?? [];
|
|
305
|
+
this.dataSource.data = this.sortRowsForDisplay(this.rawRows);
|
|
306
|
+
this.cdr.markForCheck();
|
|
311
307
|
this.cdr.detectChanges();
|
|
312
308
|
setTimeout(() => { if (!this.destroyRef.destroyed) this.onTableScroll(); }, 0);
|
|
313
309
|
}),
|
|
314
310
|
catchError(() => {
|
|
315
311
|
this.isLoading = false;
|
|
312
|
+
this.cdr.markForCheck();
|
|
316
313
|
this.cdr.detectChanges();
|
|
317
314
|
return of(null);
|
|
318
315
|
})
|
|
@@ -528,8 +525,13 @@ export class <%= entity.name %>ListComponent extends BaseListComponent<<%= prefe
|
|
|
528
525
|
<% }); %>
|
|
529
526
|
|
|
530
527
|
handleSearchChanged(event: { searchBy: string; searchTerm: string }): void {
|
|
531
|
-
|
|
532
|
-
|
|
528
|
+
if (event.searchBy !== this.searchBy) {
|
|
529
|
+
this.searchBy = event.searchBy;
|
|
530
|
+
this.searchTerm = '';
|
|
531
|
+
} else {
|
|
532
|
+
this.searchBy = event.searchBy;
|
|
533
|
+
this.searchTerm = event.searchTerm;
|
|
534
|
+
}
|
|
533
535
|
|
|
534
536
|
if (typeof localStorage !== 'undefined') {
|
|
535
537
|
localStorage.setItem(this.storageKeySearchTerm, this.searchTerm);
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
<div class="absolute inset-0 flex min-w-0 flex-col overflow-hidden">
|
|
4
4
|
|
|
5
5
|
<!-- Header (fixed, does not scroll) — PMB User Management layout -->
|
|
6
|
-
<div class="flex flex-0 flex-col border-b border-slate-200 bg-slate-100 p-6 pb-
|
|
7
|
-
<div class="min-w-0 flex-1">
|
|
6
|
+
<div class="flex flex-0 flex-col border-b border-slate-200 bg-slate-100 p-6 pb-20 sm:flex-row sm:items-center sm:justify-between sm:flex-wrap md:px-10 md:pb-28 md:pt-12 dark:border-slate-800 dark:bg-slate-900/60">
|
|
7
|
+
<div class="min-w-0 flex-1 shrink-0">
|
|
8
8
|
<div class="flex flex-wrap items-center gap-3">
|
|
9
|
-
<h2 class="
|
|
9
|
+
<h2 class="text-3xl font-extrabold leading-7 tracking-tight sm:leading-10 md:text-4xl">
|
|
10
10
|
User Management <span class="text-2xl font-bold text-primary-800 tabular-nums dark:text-primary-400 md:text-3xl">({{ totalItems }})</span>
|
|
11
11
|
</h2>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
<div class="mt-6 grid w-full grid-cols-2 gap-3
|
|
15
|
+
<div class="mt-6 grid w-full grid-cols-2 gap-3 sm:mt-0 sm:w-auto sm:flex sm:shrink-0 sm:items-center md:ml-4">
|
|
16
16
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="col-span-2 w-full md:w-64">
|
|
17
17
|
<mat-icon matPrefix svgIcon="search" class="mr-2 text-neutral-500"></mat-icon>
|
|
18
18
|
<input matInput placeholder="Search users..." [formControl]="searchControl" autocomplete="off" />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
|
2
2
|
import { CommonModule } from '@angular/common';
|
|
3
|
-
import { Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
|
|
3
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
|
|
4
4
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
5
5
|
import { MatButtonModule } from '@angular/material/button';
|
|
6
6
|
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|
@@ -50,14 +50,16 @@ type ColumnPref = { key: string; label: string; alwaysVisible?: boolean };
|
|
|
50
50
|
UserDetailComponent,
|
|
51
51
|
UserFormComponent,
|
|
52
52
|
],
|
|
53
|
-
host: { class: 'relative flex h-full min-h-0 w-full min-w-0 flex-auto flex-col' },
|
|
53
|
+
host: { class: 'cforge-iam-list relative flex h-full min-h-0 w-full min-w-0 flex-auto flex-col' },
|
|
54
54
|
templateUrl: './user-list.component.html',
|
|
55
55
|
styleUrl: './user-list.component.scss',
|
|
56
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
56
57
|
})
|
|
57
58
|
export class UserListComponent implements OnInit, OnDestroy {
|
|
58
59
|
readonly SCREEN = 'user-management';
|
|
59
60
|
@ViewChild('drawer') drawer?: MatSidenav;
|
|
60
61
|
|
|
62
|
+
private readonly cdr = inject(ChangeDetectorRef);
|
|
61
63
|
private readonly keycloak = inject(KeycloakAdminService);
|
|
62
64
|
private readonly dialog = inject(MatDialog);
|
|
63
65
|
private readonly router = inject(Router);
|
|
@@ -135,6 +137,7 @@ export class UserListComponent implements OnInit, OnDestroy {
|
|
|
135
137
|
|
|
136
138
|
onSortChange(sort: Sort): void {
|
|
137
139
|
this.sortState = sort;
|
|
140
|
+
this.cdr.markForCheck();
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
setViewMode(mode: 'table' | 'card'): void {
|
|
@@ -144,21 +147,25 @@ export class UserListComponent implements OnInit, OnDestroy {
|
|
|
144
147
|
} catch {
|
|
145
148
|
/* ignore */
|
|
146
149
|
}
|
|
150
|
+
this.cdr.markForCheck();
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
load(): void {
|
|
150
154
|
this.isSearching = true;
|
|
155
|
+
this.cdr.markForCheck();
|
|
151
156
|
const term = (this.searchControl.value || '').trim();
|
|
152
157
|
this.keycloak.searchUsers(term, this.pageIndex, this.pageSize).subscribe({
|
|
153
158
|
next: (page) => {
|
|
154
159
|
this.users = page.users;
|
|
155
160
|
this.totalItems = page.totalCount;
|
|
156
161
|
this.isSearching = false;
|
|
162
|
+
this.cdr.markForCheck();
|
|
157
163
|
},
|
|
158
164
|
error: () => {
|
|
159
165
|
this.users = [];
|
|
160
166
|
this.totalItems = 0;
|
|
161
167
|
this.isSearching = false;
|
|
168
|
+
this.cdr.markForCheck();
|
|
162
169
|
},
|
|
163
170
|
});
|
|
164
171
|
}
|
|
@@ -312,6 +319,7 @@ export class UserListComponent implements OnInit, OnDestroy {
|
|
|
312
319
|
} catch {
|
|
313
320
|
/* ignore */
|
|
314
321
|
}
|
|
322
|
+
this.cdr.markForCheck();
|
|
315
323
|
}
|
|
316
324
|
|
|
317
325
|
isPinned(id?: string): boolean {
|
|
@@ -337,6 +345,7 @@ export class UserListComponent implements OnInit, OnDestroy {
|
|
|
337
345
|
/* ignore */
|
|
338
346
|
}
|
|
339
347
|
this.activeFlagUserId = null;
|
|
348
|
+
this.cdr.markForCheck();
|
|
340
349
|
}
|
|
341
350
|
|
|
342
351
|
getFlagClass(id?: string): string {
|