@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.
@@ -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
- this.compactView = width < <%= preferences.ui?.cardBreakpointPx || 640 %>;
89
- this.viewMode = this.compactView ? 'card' : this.preferredViewMode;
90
- this.cdr.markForCheck();
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
- return [...rows].sort((left, right) => {
305
- const leftPinned = this.isPinned(left.id);
306
- const rightPinned = this.isPinned(right.id);
307
- if (leftPinned === rightPinned) {
308
- return 0;
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
- return leftPinned ? -1 : 1;
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
 
@@ -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
- return newContent; // No custom code to merge
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/);
@@ -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
- console.log('List state tests passed: audit names/cache/fallback, column defaults/preferences, responsive saved view, observer cleanup, reused form defaults, and stale edit responses.');
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
+ })();