@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, ViewChild, ElementRef, ChangeDetectorRef, DestroyRef, inject } from '@angular/core';
|
|
1
|
+
import { Component, ViewChild, ElementRef, ChangeDetectorRef, DestroyRef, NgZone, inject } from '@angular/core';
|
|
2
2
|
import { MatPaginator } from '@angular/material/paginator';
|
|
3
3
|
import { MatSort } from '@angular/material/sort';
|
|
4
4
|
import { MatDrawer } from '@angular/material/sidenav';
|
|
@@ -48,6 +48,7 @@ export interface RowFlagOption {
|
|
|
48
48
|
})
|
|
49
49
|
export abstract class BaseListComponent<T extends { id?: number }> {
|
|
50
50
|
protected readonly cdr = inject(ChangeDetectorRef);
|
|
51
|
+
protected readonly ngZone = inject(NgZone, { optional: true });
|
|
51
52
|
protected readonly dialog = inject(MatDialog);
|
|
52
53
|
protected readonly snackBar = inject(MatSnackBar);
|
|
53
54
|
|
|
@@ -85,9 +86,16 @@ export abstract class BaseListComponent<T extends { id?: number }> {
|
|
|
85
86
|
if (!viewport) return;
|
|
86
87
|
const update = (width: number) => {
|
|
87
88
|
if (width <= 0) return;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const apply = () => {
|
|
90
|
+
this.compactView = width < <%= preferences.ui?.cardBreakpointPx || 640 %>;
|
|
91
|
+
this.viewMode = this.compactView ? 'card' : this.preferredViewMode;
|
|
92
|
+
this.cdr.markForCheck();
|
|
93
|
+
};
|
|
94
|
+
if (this.ngZone) {
|
|
95
|
+
this.ngZone.run(apply);
|
|
96
|
+
} else {
|
|
97
|
+
apply();
|
|
98
|
+
}
|
|
91
99
|
};
|
|
92
100
|
update(viewport.getBoundingClientRect().width);
|
|
93
101
|
if (typeof ResizeObserver !== 'undefined') {
|
|
@@ -116,6 +124,8 @@ export abstract class BaseListComponent<T extends { id?: number }> {
|
|
|
116
124
|
exportAfterFilter = false;
|
|
117
125
|
totalItems = 0;
|
|
118
126
|
itemsPerPage = 10;
|
|
127
|
+
dataSource?: any;
|
|
128
|
+
protected rawRows: T[] = [];
|
|
119
129
|
|
|
120
130
|
protected baseParentFilters: { [key: string]: string | number } = {};
|
|
121
131
|
protected activeFilters: { [key: string]: string } = {};
|
|
@@ -301,14 +311,16 @@ export abstract class BaseListComponent<T extends { id?: number }> {
|
|
|
301
311
|
}
|
|
302
312
|
|
|
303
313
|
protected sortRowsForDisplay(rows: T[]): T[] {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
314
|
+
const pinned: T[] = [];
|
|
315
|
+
const unpinned: T[] = [];
|
|
316
|
+
for (const row of rows) {
|
|
317
|
+
if (this.isPinned(row.id)) {
|
|
318
|
+
pinned.push(row);
|
|
319
|
+
} else {
|
|
320
|
+
unpinned.push(row);
|
|
309
321
|
}
|
|
310
|
-
|
|
311
|
-
|
|
322
|
+
}
|
|
323
|
+
return [...pinned, ...unpinned];
|
|
312
324
|
}
|
|
313
325
|
|
|
314
326
|
togglePin(element: T): void {
|
|
@@ -321,6 +333,10 @@ export abstract class BaseListComponent<T extends { id?: number }> {
|
|
|
321
333
|
if (typeof localStorage !== 'undefined') {
|
|
322
334
|
localStorage.setItem(this.storageKeyPinned, JSON.stringify(Array.from(this.pinnedIds)));
|
|
323
335
|
}
|
|
336
|
+
if (this.dataSource) {
|
|
337
|
+
const baseline = this.rawRows.length > 0 ? this.rawRows : (this.rawRows = [...(this.dataSource.data ?? [])]);
|
|
338
|
+
this.dataSource.data = this.sortRowsForDisplay(baseline);
|
|
339
|
+
}
|
|
324
340
|
this.cdr.markForCheck();
|
|
325
341
|
}
|
|
326
342
|
|
package/src/utils/merger.js
CHANGED
|
@@ -60,7 +60,8 @@ function extractZones(content) {
|
|
|
60
60
|
function mergeContent(oldContent, newContent) {
|
|
61
61
|
const oldZones = extractZones(oldContent);
|
|
62
62
|
if (Object.keys(oldZones).length === 0) {
|
|
63
|
-
|
|
63
|
+
// Preserve existing file content when no merge zones are present to protect developer customizations
|
|
64
|
+
return oldContent;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
const lines = newContent.split(/\r?\n/);
|
package/test/list-state.js
CHANGED
|
@@ -133,6 +133,77 @@ async function main() {
|
|
|
133
133
|
host.closeFormDrawer();
|
|
134
134
|
assert.equal(host.selectedBook, null);
|
|
135
135
|
assert.equal(host.formVisible, false);
|
|
136
|
-
|
|
136
|
+
|
|
137
|
+
// Issue 1: Record pinning position & restoration on unpin
|
|
138
|
+
const initialRows = [{ id: 10, title: 'Ten' }, { id: 20, title: 'Twenty' }, { id: 30, title: 'Thirty' }, { id: 40, title: 'Forty' }];
|
|
139
|
+
list.pinnedIds = new Set();
|
|
140
|
+
list.dataSource = { data: [...initialRows] };
|
|
141
|
+
list.rawRows = [...initialRows];
|
|
142
|
+
|
|
143
|
+
// Pin row 30: immediately shifts to top
|
|
144
|
+
list.togglePin({ id: 30 });
|
|
145
|
+
assert.deepEqual(Array.from(list.dataSource.data).map(r => r.id), [30, 10, 20, 40]);
|
|
146
|
+
|
|
147
|
+
// Pin row 40: grouped at top, unpinned preserve order
|
|
148
|
+
list.togglePin({ id: 40 });
|
|
149
|
+
assert.deepEqual(Array.from(list.dataSource.data).map(r => r.id), [30, 40, 10, 20]);
|
|
150
|
+
|
|
151
|
+
// Unpin row 30: returns to its original position in unpinned set
|
|
152
|
+
list.togglePin({ id: 30 });
|
|
153
|
+
assert.deepEqual(Array.from(list.dataSource.data).map(r => r.id), [40, 10, 20, 30]);
|
|
154
|
+
|
|
155
|
+
// Unpin row 40: returns to exact initial dataset
|
|
156
|
+
list.togglePin({ id: 40 });
|
|
157
|
+
assert.deepEqual(Array.from(list.dataSource.data).map(r => r.id), [10, 20, 30, 40]);
|
|
158
|
+
|
|
159
|
+
// Issue 2: Page size resets to default 10 and does not persist in localStorage across navigations
|
|
160
|
+
storage.set('book-page-size', '50');
|
|
161
|
+
const entityList = Object.create(List.prototype);
|
|
162
|
+
Object.assign(entityList, {
|
|
163
|
+
entityNameKebab: 'book',
|
|
164
|
+
itemsPerPage: 10,
|
|
165
|
+
paginator: { pageIndex: 0, pageSize: 25 },
|
|
166
|
+
activeFilters: {},
|
|
167
|
+
baseParentFilters: {},
|
|
168
|
+
getSortParameters: () => ['id,asc'],
|
|
169
|
+
restoreViewMode() {},
|
|
170
|
+
restoreColumnPreferences() {},
|
|
171
|
+
loadSearchState() {},
|
|
172
|
+
dialogData: null,
|
|
173
|
+
cdr: { markForCheck() {}, detectChanges() {} },
|
|
174
|
+
destroyRef: { destroyed: false },
|
|
175
|
+
bookService: { query: () => rx.of({ headers: new Map(), body: [] }) },
|
|
176
|
+
});
|
|
177
|
+
entityList.ngOnInit();
|
|
178
|
+
assert.equal(entityList.itemsPerPage, 10, 'ngOnInit does not override default page size from localStorage');
|
|
179
|
+
entityList.loadData();
|
|
180
|
+
assert.equal(entityList.itemsPerPage, 25, 'loadData updates itemsPerPage in-memory during interaction');
|
|
181
|
+
assert.equal(storage.get('book-page-size'), '50', 'loadData does not overwrite localStorage page size');
|
|
182
|
+
|
|
183
|
+
// Issue 3: Search bar clearing on search preference change
|
|
184
|
+
const Actions = load('entity/components/interaction/actions/_entity_-actions.component.ts.ejs', 'BookActionsComponent', {}, {}, locals);
|
|
185
|
+
const actions = Object.create(Actions.prototype);
|
|
186
|
+
const emittedSearches = [];
|
|
187
|
+
actions.searchChange = { emit: val => emittedSearches.push(val) };
|
|
188
|
+
actions.searchTerm = 'initial search';
|
|
189
|
+
actions.searchBy = 'title';
|
|
190
|
+
actions.onSearchByChange('author');
|
|
191
|
+
assert.equal(actions.searchTerm, '', 'ActionsComponent clears searchTerm on searchBy change');
|
|
192
|
+
assert.equal(emittedSearches.length, 1);
|
|
193
|
+
assert.equal(emittedSearches[0].searchBy, 'author');
|
|
194
|
+
assert.equal(emittedSearches[0].searchTerm, '');
|
|
195
|
+
|
|
196
|
+
// ListComponent.handleSearchChanged resets searchTerm when searchBy changes
|
|
197
|
+
entityList.searchBy = 'title';
|
|
198
|
+
entityList.searchTerm = 'existing';
|
|
199
|
+
entityList.storageKeySearchTerm = 'book-search-term';
|
|
200
|
+
entityList.storageKeySearchBy = 'book-search-by';
|
|
201
|
+
entityList.applySearchFilters = () => {};
|
|
202
|
+
entityList.refreshTrigger = { next() {} };
|
|
203
|
+
entityList.handleSearchChanged({ searchBy: 'code', searchTerm: 'existing' });
|
|
204
|
+
assert.equal(entityList.searchBy, 'code');
|
|
205
|
+
assert.equal(entityList.searchTerm, '', 'ListComponent resets searchTerm when search preference changes');
|
|
206
|
+
|
|
207
|
+
console.log('List state tests passed: audit names/cache/fallback, column defaults/preferences, responsive saved view, observer cleanup, reused form defaults, stale edit responses, row pinning, page-size reset, and search clearing.');
|
|
137
208
|
}
|
|
138
209
|
main().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const assert = require('assert/strict');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { generate, parseJdl } = require('../src');
|
|
6
|
+
const defaults = require('../src/config/default-values');
|
|
7
|
+
const merger = require('../src/utils/merger');
|
|
8
|
+
const { serviceFolder } = require('../src/generator/entity-layout');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
console.log('๐งช Testing Regeneration Isolation & Preservation...');
|
|
12
|
+
|
|
13
|
+
// 1. Merger preservation when no zones exist
|
|
14
|
+
{
|
|
15
|
+
const oldCode = 'export const customized = true;\n// Developer custom changes';
|
|
16
|
+
const newCode = 'export const customized = false;\n// Fresh template output';
|
|
17
|
+
const merged = merger.mergeContent(oldCode, newCode);
|
|
18
|
+
assert.strictEqual(merged, oldCode, 'Merger must preserve un-bracketed developer code when no zones exist');
|
|
19
|
+
console.log('โ
Merger preserves files without zone markers under merge policy.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Double nesting check in serviceFolder
|
|
23
|
+
{
|
|
24
|
+
const folder1 = serviceFolder({ folderName: 'micro-functions', microservice: 'procurement-service' });
|
|
25
|
+
assert.strictEqual(folder1, 'micro-functions/procurement-service', 'serviceFolder should use microservice name without duplicating micro-functions');
|
|
26
|
+
const folder2 = serviceFolder({ folderName: 'procurement-module' });
|
|
27
|
+
assert.strictEqual(folder2, 'micro-functions/procurement-module', 'serviceFolder should correctly prefix module name');
|
|
28
|
+
console.log('โ
serviceFolder avoids micro-functions/micro-functions nesting.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. Incremental generation with Route & Navigation preservation
|
|
32
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'crudforge-regen-'));
|
|
33
|
+
try {
|
|
34
|
+
const config = structuredClone(defaults);
|
|
35
|
+
config.output.directory = root;
|
|
36
|
+
config.output.overwritePolicy = 'merge';
|
|
37
|
+
config.fuse = {
|
|
38
|
+
...config.fuse,
|
|
39
|
+
enabled: true,
|
|
40
|
+
cleanLayout: true,
|
|
41
|
+
includePrebuilt: false,
|
|
42
|
+
starterPackPath: path.resolve(__dirname, '../fuse-starter-pack/fuse-v21.1')
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// First pass: Generate Service A (Identity Module)
|
|
46
|
+
const schemaA = parseJdl('entity Staff { name String }');
|
|
47
|
+
for (const entity of schemaA.entities) {
|
|
48
|
+
entity.folderName = 'identityModule';
|
|
49
|
+
entity.sourceName = 'Identity';
|
|
50
|
+
entity.microservice = 'identity-service';
|
|
51
|
+
}
|
|
52
|
+
await generate(schemaA, config);
|
|
53
|
+
|
|
54
|
+
const routesFile = path.join(root, 'src/app/domains/admin/routes.ts');
|
|
55
|
+
const navFile = path.join(root, 'src/app/domains/admin/layout/data/navigation.ts');
|
|
56
|
+
const routesPass1 = fs.readFileSync(routesFile, 'utf8');
|
|
57
|
+
const navPass1 = fs.readFileSync(navFile, 'utf8');
|
|
58
|
+
|
|
59
|
+
assert(routesPass1.includes('identity-module') || routesPass1.includes('staff.routes'), 'Pass 1 must include staff route');
|
|
60
|
+
assert(navPass1.includes('identity-module') || navPass1.includes('Staff'), 'Pass 1 must include Staff in navigation');
|
|
61
|
+
|
|
62
|
+
// Second pass: Generate Service B (Procurement Module)
|
|
63
|
+
const schemaB = parseJdl('entity PurchaseOrder { orderNumber String }');
|
|
64
|
+
for (const entity of schemaB.entities) {
|
|
65
|
+
entity.folderName = 'procurement-module';
|
|
66
|
+
entity.sourceName = 'Procurement';
|
|
67
|
+
entity.microservice = 'procurement-service';
|
|
68
|
+
}
|
|
69
|
+
await generate(schemaB, config);
|
|
70
|
+
|
|
71
|
+
const routesPass2 = fs.readFileSync(routesFile, 'utf8');
|
|
72
|
+
const navPass2 = fs.readFileSync(navFile, 'utf8');
|
|
73
|
+
|
|
74
|
+
// Verify both Service A and Service B routes and navigation exist in Pass 2
|
|
75
|
+
assert(routesPass2.includes('staff.routes'), 'Pass 2 must preserve staff route');
|
|
76
|
+
assert(routesPass2.includes('purchase-order.routes'), 'Pass 2 must include purchase-order route');
|
|
77
|
+
assert(navPass2.includes('PurchaseOrder') || navPass2.includes('procurement-module'), 'Pass 2 must include PurchaseOrder navigation');
|
|
78
|
+
assert(navPass2.includes('Staff') || navPass2.includes('identity-module'), 'Pass 2 must preserve Staff navigation');
|
|
79
|
+
console.log('โ
Incremental generation preserves routes and navigation for preexisting services.');
|
|
80
|
+
|
|
81
|
+
// 4. Screen Catalog CSV preservation
|
|
82
|
+
const csvFile = path.join(root, 'public/assets/screen-components.csv');
|
|
83
|
+
if (fs.existsSync(csvFile)) {
|
|
84
|
+
const csvContent = fs.readFileSync(csvFile, 'utf8');
|
|
85
|
+
assert(csvContent.includes('staff'), 'CSV catalog must retain staff screen');
|
|
86
|
+
assert(csvContent.includes('purchase-order'), 'CSV catalog must include purchase-order screen');
|
|
87
|
+
console.log('โ
Screen Catalog CSV accumulates and preserves screens across services.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('๐ Regeneration isolation tests PASSED successfully!');
|
|
91
|
+
} finally {
|
|
92
|
+
await fs.remove(root);
|
|
93
|
+
}
|
|
94
|
+
})();
|