@memberjunction/ng-entity-viewer 0.0.1 → 2.122.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.
Files changed (42) hide show
  1. package/README.md +224 -43
  2. package/dist/lib/entity-cards/entity-cards.component.d.ts +163 -0
  3. package/dist/lib/entity-cards/entity-cards.component.d.ts.map +1 -0
  4. package/dist/lib/entity-cards/entity-cards.component.js +797 -0
  5. package/dist/lib/entity-cards/entity-cards.component.js.map +1 -0
  6. package/dist/lib/entity-grid/entity-grid.component.d.ts +216 -0
  7. package/dist/lib/entity-grid/entity-grid.component.d.ts.map +1 -0
  8. package/dist/lib/entity-grid/entity-grid.component.js +676 -0
  9. package/dist/lib/entity-grid/entity-grid.component.js.map +1 -0
  10. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts +182 -0
  11. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts.map +1 -0
  12. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +787 -0
  13. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -0
  14. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +252 -0
  15. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -0
  16. package/dist/lib/entity-viewer/entity-viewer.component.js +883 -0
  17. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -0
  18. package/dist/lib/pagination/pagination.component.d.ts +60 -0
  19. package/dist/lib/pagination/pagination.component.d.ts.map +1 -0
  20. package/dist/lib/pagination/pagination.component.js +199 -0
  21. package/dist/lib/pagination/pagination.component.js.map +1 -0
  22. package/dist/lib/pill/pill.component.d.ts +58 -0
  23. package/dist/lib/pill/pill.component.d.ts.map +1 -0
  24. package/dist/lib/pill/pill.component.js +125 -0
  25. package/dist/lib/pill/pill.component.js.map +1 -0
  26. package/dist/lib/types.d.ts +316 -0
  27. package/dist/lib/types.d.ts.map +1 -0
  28. package/dist/lib/types.js +30 -0
  29. package/dist/lib/types.js.map +1 -0
  30. package/dist/lib/utils/highlight.util.d.ts +69 -0
  31. package/dist/lib/utils/highlight.util.d.ts.map +1 -0
  32. package/dist/lib/utils/highlight.util.js +214 -0
  33. package/dist/lib/utils/highlight.util.js.map +1 -0
  34. package/dist/module.d.ts +38 -0
  35. package/dist/module.d.ts.map +1 -0
  36. package/dist/module.js +83 -0
  37. package/dist/module.js.map +1 -0
  38. package/dist/public-api.d.ts +16 -0
  39. package/dist/public-api.d.ts.map +1 -0
  40. package/dist/public-api.js +20 -0
  41. package/dist/public-api.js.map +1 -0
  42. package/package.json +45 -6
@@ -0,0 +1,676 @@
1
+ import { Component, Input, Output, EventEmitter } from '@angular/core';
2
+ import { RunView } from '@memberjunction/core';
3
+ import { ModuleRegistry, AllCommunityModule, themeAlpine } from 'ag-grid-community';
4
+ import { HighlightUtil } from '../utils/highlight.util';
5
+ import * as i0 from "@angular/core";
6
+ import * as i1 from "ag-grid-angular";
7
+ import * as i2 from "@memberjunction/ng-shared-generic";
8
+ function EntityGridComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
9
+ i0.ɵɵelementStart(0, "div", 1);
10
+ i0.ɵɵelement(1, "mj-loading", 5);
11
+ i0.ɵɵelementEnd();
12
+ } }
13
+ function EntityGridComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
14
+ const _r1 = i0.ɵɵgetCurrentView();
15
+ i0.ɵɵelementStart(0, "ag-grid-angular", 6);
16
+ i0.ɵɵlistener("gridReady", function EntityGridComponent_Conditional_2_Template_ag_grid_angular_gridReady_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onGridReady($event)); })("rowClicked", function EntityGridComponent_Conditional_2_Template_ag_grid_angular_rowClicked_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onRowClicked($event)); })("rowDoubleClicked", function EntityGridComponent_Conditional_2_Template_ag_grid_angular_rowDoubleClicked_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onRowDoubleClicked($event)); })("sortChanged", function EntityGridComponent_Conditional_2_Template_ag_grid_angular_sortChanged_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onGridSortChanged($event)); })("columnResized", function EntityGridComponent_Conditional_2_Template_ag_grid_angular_columnResized_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onColumnResized($event)); })("columnMoved", function EntityGridComponent_Conditional_2_Template_ag_grid_angular_columnMoved_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onColumnMoved($event)); });
17
+ i0.ɵɵelementEnd();
18
+ } if (rf & 2) {
19
+ const ctx_r1 = i0.ɵɵnextContext();
20
+ i0.ɵɵproperty("theme", ctx_r1.theme)("columnDefs", ctx_r1.columnDefs)("rowData", ctx_r1.rowData)("defaultColDef", ctx_r1.defaultColDef)("rowSelection", ctx_r1.enableSelection ? ctx_r1.rowSelection : undefined)("getRowId", ctx_r1.getRowId)("suppressCellFocus", true);
21
+ } }
22
+ function EntityGridComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
23
+ i0.ɵɵelementStart(0, "div", 3);
24
+ i0.ɵɵelement(1, "i", 7);
25
+ i0.ɵɵelementStart(2, "p");
26
+ i0.ɵɵtext(3, "No records to display");
27
+ i0.ɵɵelementEnd()();
28
+ } }
29
+ function EntityGridComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
30
+ i0.ɵɵelementStart(0, "div", 4);
31
+ i0.ɵɵelement(1, "i", 8);
32
+ i0.ɵɵelementStart(2, "p");
33
+ i0.ɵɵtext(3, "Select an entity to view records");
34
+ i0.ɵɵelementEnd()();
35
+ } }
36
+ // Register AG Grid modules (required for v34+)
37
+ ModuleRegistry.registerModules([AllCommunityModule]);
38
+ /**
39
+ * EntityGridComponent - AG Grid based table view for entity records
40
+ *
41
+ * This component provides a lightweight, customizable grid view for displaying
42
+ * entity records. It uses AG Grid Community edition for high-performance rendering.
43
+ *
44
+ * Supports two modes:
45
+ * 1. Parent-managed data: Records are passed in via [records] input
46
+ * 2. Standalone: Component loads its own data with pagination
47
+ *
48
+ * @example
49
+ * ```html
50
+ * <mj-entity-grid
51
+ * [entity]="selectedEntity"
52
+ * [records]="filteredRecords"
53
+ * [selectedRecordId]="selectedId"
54
+ * [sortState]="currentSort"
55
+ * [serverSideSorting]="true"
56
+ * (recordSelected)="onRecordSelected($event)"
57
+ * (recordOpened)="onRecordOpened($event)"
58
+ * (sortChanged)="onSortChanged($event)">
59
+ * </mj-entity-grid>
60
+ * ```
61
+ */
62
+ export class EntityGridComponent {
63
+ /**
64
+ * The entity metadata for the records being displayed
65
+ */
66
+ entity = null;
67
+ /**
68
+ * The records to display in the grid (optional - component can load its own)
69
+ */
70
+ records = null;
71
+ /**
72
+ * The currently selected record's primary key string
73
+ */
74
+ selectedRecordId = null;
75
+ /**
76
+ * Custom column definitions (optional - auto-generated if not provided)
77
+ */
78
+ columns = [];
79
+ /**
80
+ * Height of the grid (CSS value)
81
+ * @default '100%'
82
+ */
83
+ height = '100%';
84
+ /**
85
+ * Whether to enable row selection
86
+ * @default true
87
+ */
88
+ enableSelection = true;
89
+ /**
90
+ * Filter text for highlighting matches in cells
91
+ * Supports SQL-style % wildcards
92
+ */
93
+ filterText = '';
94
+ /**
95
+ * Current sort state (for external control)
96
+ */
97
+ sortState = null;
98
+ /**
99
+ * Whether sorting is handled server-side
100
+ * When true, sort changes emit events but don't sort locally
101
+ * @default true
102
+ */
103
+ serverSideSorting = true;
104
+ /**
105
+ * Page size for standalone data loading
106
+ * @default 100
107
+ */
108
+ pageSize = 100;
109
+ /**
110
+ * Grid state from a User View - controls columns, widths, order, sort
111
+ * When provided, this takes precedence over auto-generated columns
112
+ */
113
+ gridState = null;
114
+ /**
115
+ * Emitted when a record is selected (single click)
116
+ */
117
+ recordSelected = new EventEmitter();
118
+ /**
119
+ * Emitted when a record should be opened (double click)
120
+ */
121
+ recordOpened = new EventEmitter();
122
+ /**
123
+ * Emitted when sort state changes
124
+ */
125
+ sortChanged = new EventEmitter();
126
+ /**
127
+ * Emitted when grid state changes (column resize, reorder, etc.)
128
+ */
129
+ gridStateChanged = new EventEmitter();
130
+ /** AG Grid column definitions */
131
+ columnDefs = [];
132
+ /** AG Grid row data */
133
+ rowData = [];
134
+ /** AG Grid API reference */
135
+ gridApi = null;
136
+ /** Internal records when loading standalone */
137
+ internalRecords = [];
138
+ /** Track if we're in standalone mode (no external records provided) */
139
+ standaloneMode = false;
140
+ /** Loading state for standalone mode */
141
+ isLoading = false;
142
+ /** Suppress sort changed events during programmatic sort updates */
143
+ suppressSortEvents = false;
144
+ /** Default column settings */
145
+ defaultColDef = {
146
+ sortable: true,
147
+ filter: false, // Filtering is handled at the parent level
148
+ resizable: true,
149
+ minWidth: 80
150
+ };
151
+ /** AG Grid theme (v34+) */
152
+ theme = themeAlpine;
153
+ /** Row selection configuration (v34+ object-based API) */
154
+ rowSelection = {
155
+ mode: 'singleRow'
156
+ };
157
+ /** Get row ID function for AG Grid */
158
+ getRowId = (params) => params.data['__pk'];
159
+ ngOnInit() {
160
+ this.standaloneMode = this.records === null;
161
+ this.buildColumnDefs();
162
+ if (this.standaloneMode && this.entity) {
163
+ this.loadData();
164
+ }
165
+ else {
166
+ this.buildRowData();
167
+ }
168
+ }
169
+ ngOnChanges(changes) {
170
+ if (changes['entity'] || changes['columns'] || changes['gridState']) {
171
+ this.buildColumnDefs();
172
+ }
173
+ if (changes['entity'] && this.standaloneMode && this.entity) {
174
+ this.loadData();
175
+ }
176
+ if (changes['records']) {
177
+ this.standaloneMode = this.records === null;
178
+ if (!this.standaloneMode) {
179
+ this.buildRowData();
180
+ }
181
+ }
182
+ if (changes['selectedRecordId'] && this.gridApi) {
183
+ this.updateSelection();
184
+ }
185
+ // When filter text changes, refresh the grid to update highlighting
186
+ if (changes['filterText'] && this.gridApi) {
187
+ this.gridApi.refreshCells({ force: true });
188
+ }
189
+ // Handle external sort state changes
190
+ if (changes['sortState'] && this.gridApi && this.sortState) {
191
+ this.applySortStateToGrid();
192
+ }
193
+ // Handle gridState changes - apply sort if present
194
+ if (changes['gridState'] && this.gridApi && this.gridState?.sortSettings?.length) {
195
+ const sortSetting = this.gridState.sortSettings[0];
196
+ this.sortState = {
197
+ field: sortSetting.field,
198
+ direction: sortSetting.dir
199
+ };
200
+ this.applySortStateToGrid();
201
+ }
202
+ }
203
+ /**
204
+ * Get effective records (external or internal)
205
+ */
206
+ get effectiveRecords() {
207
+ return this.records ?? this.internalRecords;
208
+ }
209
+ /**
210
+ * Load data in standalone mode
211
+ */
212
+ async loadData() {
213
+ if (!this.entity)
214
+ return;
215
+ this.isLoading = true;
216
+ try {
217
+ const rv = new RunView();
218
+ // Build OrderBy from sort state
219
+ let orderBy;
220
+ if (this.sortState?.field && this.sortState.direction) {
221
+ orderBy = `${this.sortState.field} ${this.sortState.direction.toUpperCase()}`;
222
+ }
223
+ const result = await rv.RunView({
224
+ EntityName: this.entity.Name,
225
+ ResultType: 'entity_object',
226
+ MaxRows: this.pageSize,
227
+ OrderBy: orderBy
228
+ });
229
+ if (result.Success) {
230
+ this.internalRecords = result.Results;
231
+ this.buildRowData();
232
+ }
233
+ }
234
+ catch (error) {
235
+ console.error('Error loading grid data:', error);
236
+ }
237
+ finally {
238
+ this.isLoading = false;
239
+ }
240
+ }
241
+ /**
242
+ * Handle AG Grid ready event
243
+ */
244
+ onGridReady(event) {
245
+ this.gridApi = event.api;
246
+ this.updateSelection();
247
+ // Apply initial sort state if provided
248
+ if (this.sortState) {
249
+ this.applySortStateToGrid();
250
+ }
251
+ // Auto-size columns to fit content
252
+ event.api.sizeColumnsToFit();
253
+ }
254
+ /**
255
+ * Handle AG Grid sort changed event
256
+ */
257
+ onGridSortChanged(event) {
258
+ if (this.suppressSortEvents)
259
+ return;
260
+ const sortModel = event.api.getColumnState()
261
+ .filter(col => col.sort)
262
+ .map(col => ({ field: col.colId, direction: col.sort }));
263
+ if (sortModel.length > 0) {
264
+ const newSort = {
265
+ field: sortModel[0].field,
266
+ direction: sortModel[0].direction
267
+ };
268
+ this.sortChanged.emit({ sort: newSort });
269
+ // Also emit as grid state change
270
+ this.emitGridStateChanged('sort');
271
+ // If in standalone mode and server-side sorting, reload data
272
+ if (this.standaloneMode && this.serverSideSorting) {
273
+ // The parent should handle this, but if standalone, reload
274
+ this.loadData();
275
+ }
276
+ }
277
+ else {
278
+ this.sortChanged.emit({ sort: null });
279
+ }
280
+ }
281
+ /**
282
+ * Handle column resized event
283
+ */
284
+ onColumnResized(event) {
285
+ // Only emit on finished (not during drag)
286
+ if (event.finished && event.source !== 'api') {
287
+ this.emitGridStateChanged('columns');
288
+ }
289
+ }
290
+ /**
291
+ * Handle column moved event
292
+ */
293
+ onColumnMoved(event) {
294
+ // Only emit when the drag is finished
295
+ if (event.finished && event.source !== 'api') {
296
+ this.emitGridStateChanged('columns');
297
+ }
298
+ }
299
+ /**
300
+ * Emit grid state changed event with current column/sort state
301
+ */
302
+ emitGridStateChanged(changeType) {
303
+ if (!this.gridApi || !this.entity)
304
+ return;
305
+ const currentState = this.buildCurrentGridState();
306
+ this.gridStateChanged.emit({
307
+ gridState: currentState,
308
+ changeType
309
+ });
310
+ }
311
+ /**
312
+ * Build current grid state from AG Grid's column state
313
+ */
314
+ buildCurrentGridState() {
315
+ if (!this.gridApi || !this.entity) {
316
+ return { columnSettings: [], sortSettings: [] };
317
+ }
318
+ const columnState = this.gridApi.getColumnState();
319
+ const columnSettings = [];
320
+ const sortSettings = [];
321
+ for (let i = 0; i < columnState.length; i++) {
322
+ const col = columnState[i];
323
+ const field = this.entity.Fields.find(f => f.Name === col.colId);
324
+ if (field) {
325
+ columnSettings.push({
326
+ ID: field.ID,
327
+ Name: field.Name,
328
+ DisplayName: field.DisplayNameOrName,
329
+ hidden: col.hide ?? false,
330
+ width: col.width ?? undefined,
331
+ orderIndex: i
332
+ });
333
+ }
334
+ // Capture sort settings
335
+ if (col.sort) {
336
+ sortSettings.push({
337
+ field: col.colId,
338
+ dir: col.sort
339
+ });
340
+ }
341
+ }
342
+ return { columnSettings, sortSettings };
343
+ }
344
+ /**
345
+ * Apply external sort state to the grid
346
+ */
347
+ applySortStateToGrid() {
348
+ if (!this.gridApi || !this.sortState)
349
+ return;
350
+ this.suppressSortEvents = true;
351
+ try {
352
+ const columnState = this.gridApi.getColumnState().map(col => ({
353
+ ...col,
354
+ sort: col.colId === this.sortState.field ? this.sortState.direction : null,
355
+ sortIndex: col.colId === this.sortState.field ? 0 : null
356
+ }));
357
+ this.gridApi.applyColumnState({ state: columnState });
358
+ }
359
+ finally {
360
+ this.suppressSortEvents = false;
361
+ }
362
+ }
363
+ /**
364
+ * Handle row click event
365
+ */
366
+ onRowClicked(event) {
367
+ if (!this.entity || !event.data)
368
+ return;
369
+ const record = this.findRecordByPk(event.data['__pk']);
370
+ if (!record)
371
+ return;
372
+ this.recordSelected.emit({
373
+ record,
374
+ entity: this.entity,
375
+ compositeKey: record.PrimaryKey
376
+ });
377
+ }
378
+ /**
379
+ * Handle row double-click event
380
+ */
381
+ onRowDoubleClicked(event) {
382
+ if (!this.entity || !event.data)
383
+ return;
384
+ const record = this.findRecordByPk(event.data['__pk']);
385
+ if (!record)
386
+ return;
387
+ this.recordOpened.emit({
388
+ record,
389
+ entity: this.entity,
390
+ compositeKey: record.PrimaryKey
391
+ });
392
+ }
393
+ /**
394
+ * Build AG Grid column definitions from gridState, custom columns, or entity metadata
395
+ * Priority: gridState > custom columns > auto-generated
396
+ */
397
+ buildColumnDefs() {
398
+ if (this.gridState?.columnSettings && this.gridState.columnSettings.length > 0 && this.entity) {
399
+ // Use gridState column configuration (from User View)
400
+ this.columnDefs = this.buildColumnDefsFromGridState(this.gridState.columnSettings);
401
+ }
402
+ else if (this.columns.length > 0) {
403
+ // Use custom column definitions
404
+ this.columnDefs = this.columns.map(col => this.mapCustomColumnDef(col));
405
+ }
406
+ else if (this.entity) {
407
+ // Auto-generate from entity metadata
408
+ this.columnDefs = this.generateColumnDefs(this.entity);
409
+ }
410
+ else {
411
+ this.columnDefs = [];
412
+ }
413
+ }
414
+ /**
415
+ * Build column definitions from gridState column settings
416
+ */
417
+ buildColumnDefsFromGridState(columnSettings) {
418
+ if (!this.entity)
419
+ return [];
420
+ // Sort by orderIndex
421
+ const sortedColumns = [...columnSettings].sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0));
422
+ const cols = [];
423
+ for (const colConfig of sortedColumns) {
424
+ // Skip hidden columns
425
+ if (colConfig.hidden)
426
+ continue;
427
+ // Find the corresponding entity field
428
+ const field = this.entity.Fields.find(f => f.Name.toLowerCase() === colConfig.Name.toLowerCase());
429
+ if (!field)
430
+ continue;
431
+ const colDef = {
432
+ field: field.Name,
433
+ headerName: colConfig.DisplayName || field.DisplayNameOrName,
434
+ width: colConfig.width || this.estimateColumnWidth(field),
435
+ sortable: true,
436
+ resizable: true
437
+ };
438
+ // For string fields, use a cell renderer that supports highlighting
439
+ if (field.TSType === 'string') {
440
+ colDef.cellRenderer = (params) => this.highlightCellRenderer(params);
441
+ }
442
+ else {
443
+ // For non-string fields, use value formatter
444
+ colDef.valueFormatter = this.getValueFormatter(field);
445
+ }
446
+ cols.push(colDef);
447
+ }
448
+ return cols;
449
+ }
450
+ /**
451
+ * Map custom column definition to AG Grid ColDef
452
+ */
453
+ mapCustomColumnDef(col) {
454
+ return {
455
+ field: col.field,
456
+ headerName: col.headerName,
457
+ width: col.width,
458
+ minWidth: col.minWidth,
459
+ maxWidth: col.maxWidth,
460
+ sortable: col.sortable ?? true,
461
+ filter: false, // Filtering handled at parent level
462
+ resizable: col.resizable ?? true,
463
+ hide: col.hide ?? false
464
+ };
465
+ }
466
+ /**
467
+ * Auto-generate column definitions from entity metadata
468
+ */
469
+ generateColumnDefs(entity) {
470
+ const cols = [];
471
+ // Filter fields to show
472
+ const visibleFields = entity.Fields.filter(f => this.shouldShowField(f));
473
+ for (const field of visibleFields) {
474
+ const colDef = {
475
+ field: field.Name,
476
+ headerName: field.DisplayNameOrName,
477
+ width: this.estimateColumnWidth(field),
478
+ sortable: true,
479
+ resizable: true
480
+ };
481
+ // For string fields, use a cell renderer that supports highlighting
482
+ if (field.TSType === 'string') {
483
+ colDef.cellRenderer = (params) => this.highlightCellRenderer(params);
484
+ }
485
+ else {
486
+ // For non-string fields, use value formatter
487
+ colDef.valueFormatter = this.getValueFormatter(field);
488
+ }
489
+ cols.push(colDef);
490
+ }
491
+ return cols;
492
+ }
493
+ /**
494
+ * Cell renderer that highlights matching text
495
+ * Uses HighlightUtil which only highlights if the text actually matches the pattern
496
+ */
497
+ highlightCellRenderer(params) {
498
+ const value = params.value;
499
+ if (value === null || value === undefined)
500
+ return '';
501
+ const text = String(value);
502
+ return HighlightUtil.highlight(text, this.filterText, true);
503
+ }
504
+ /**
505
+ * Determine if a field should be shown in the grid
506
+ */
507
+ shouldShowField(field) {
508
+ // Skip internal MJ fields
509
+ if (field.Name.startsWith('__mj_'))
510
+ return false;
511
+ // Skip primary key if it's a GUID (usually not useful to display)
512
+ if (field.IsPrimaryKey && field.SQLFullType?.toLowerCase() === 'uniqueidentifier') {
513
+ return false;
514
+ }
515
+ // Prefer DefaultInView fields
516
+ if (field.DefaultInView === true)
517
+ return true;
518
+ // Skip large text fields by default
519
+ if (field.Length > 500)
520
+ return false;
521
+ return true;
522
+ }
523
+ /**
524
+ * Estimate appropriate column width based on field type
525
+ */
526
+ estimateColumnWidth(field) {
527
+ if (field.TSType === 'boolean')
528
+ return 100;
529
+ if (field.TSType === 'number')
530
+ return 120;
531
+ if (field.TSType === 'Date')
532
+ return 150;
533
+ if (field.Name.toLowerCase().includes('id'))
534
+ return 100;
535
+ if (field.Name.toLowerCase().includes('email'))
536
+ return 200;
537
+ if (field.Name.toLowerCase().includes('name'))
538
+ return 180;
539
+ // Default based on length
540
+ const charWidth = 8;
541
+ const padding = 20;
542
+ const estimatedWidth = Math.min(Math.max(field.Length * charWidth / 2, 100), 300) + padding;
543
+ return estimatedWidth;
544
+ }
545
+ /**
546
+ * Get value formatter for a field based on its type
547
+ */
548
+ getValueFormatter(field) {
549
+ if (field.TSType === 'Date') {
550
+ return (params) => {
551
+ if (!params.value)
552
+ return '';
553
+ const date = params.value instanceof Date ? params.value : new Date(params.value);
554
+ if (isNaN(date.getTime()))
555
+ return String(params.value);
556
+ return date.toLocaleDateString(undefined, {
557
+ month: 'short',
558
+ day: 'numeric',
559
+ year: 'numeric'
560
+ });
561
+ };
562
+ }
563
+ if (field.TSType === 'boolean') {
564
+ return (params) => {
565
+ if (params.value === null || params.value === undefined)
566
+ return '';
567
+ return params.value ? 'Yes' : 'No';
568
+ };
569
+ }
570
+ if (field.TSType === 'number') {
571
+ const fieldNameLower = field.Name.toLowerCase();
572
+ const isCurrency = fieldNameLower.includes('amount') ||
573
+ fieldNameLower.includes('price') ||
574
+ fieldNameLower.includes('cost') ||
575
+ fieldNameLower.includes('total');
576
+ if (isCurrency) {
577
+ return (params) => {
578
+ if (params.value === null || params.value === undefined)
579
+ return '';
580
+ const num = Number(params.value);
581
+ if (isNaN(num))
582
+ return String(params.value);
583
+ return `$${num.toLocaleString()}`;
584
+ };
585
+ }
586
+ }
587
+ return undefined;
588
+ }
589
+ /**
590
+ * Build row data from entity records
591
+ */
592
+ buildRowData() {
593
+ if (!this.entity) {
594
+ this.rowData = [];
595
+ return;
596
+ }
597
+ const records = this.effectiveRecords;
598
+ this.rowData = records.map(record => {
599
+ const row = {
600
+ __pk: record.PrimaryKey.ToConcatenatedString()
601
+ };
602
+ // Copy all field values
603
+ for (const field of this.entity.Fields) {
604
+ row[field.Name] = record.Get(field.Name);
605
+ }
606
+ return row;
607
+ });
608
+ }
609
+ /**
610
+ * Update grid selection to match selectedRecordId and scroll to the selected row
611
+ */
612
+ updateSelection() {
613
+ if (!this.gridApi || !this.selectedRecordId) {
614
+ this.gridApi?.deselectAll();
615
+ return;
616
+ }
617
+ const node = this.gridApi.getRowNode(this.selectedRecordId);
618
+ if (node) {
619
+ node.setSelected(true);
620
+ // Scroll the selected row into view (middle of viewport if possible)
621
+ this.gridApi.ensureNodeVisible(node, 'middle');
622
+ }
623
+ }
624
+ /**
625
+ * Find a record by its primary key string
626
+ */
627
+ findRecordByPk(pkString) {
628
+ return this.effectiveRecords.find(r => r.PrimaryKey.ToConcatenatedString() === pkString);
629
+ }
630
+ static ɵfac = function EntityGridComponent_Factory(t) { return new (t || EntityGridComponent)(); };
631
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: EntityGridComponent, selectors: [["mj-entity-grid"]], inputs: { entity: "entity", records: "records", selectedRecordId: "selectedRecordId", columns: "columns", height: "height", enableSelection: "enableSelection", filterText: "filterText", sortState: "sortState", serverSideSorting: "serverSideSorting", pageSize: "pageSize", gridState: "gridState" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", sortChanged: "sortChanged", gridStateChanged: "gridStateChanged" }, features: [i0.ɵɵNgOnChangesFeature], decls: 5, vars: 3, consts: [[1, "entity-grid-container"], [1, "loading-state"], [1, "entity-grid", 3, "theme", "columnDefs", "rowData", "defaultColDef", "rowSelection", "getRowId", "suppressCellFocus"], [1, "no-data"], [1, "no-entity"], ["text", "Loading...", "size", "medium"], [1, "entity-grid", 3, "gridReady", "rowClicked", "rowDoubleClicked", "sortChanged", "columnResized", "columnMoved", "theme", "columnDefs", "rowData", "defaultColDef", "rowSelection", "getRowId", "suppressCellFocus"], [1, "fa-solid", "fa-inbox"], [1, "fa-solid", "fa-database"]], template: function EntityGridComponent_Template(rf, ctx) { if (rf & 1) {
632
+ i0.ɵɵelementStart(0, "div", 0);
633
+ i0.ɵɵtemplate(1, EntityGridComponent_Conditional_1_Template, 2, 0, "div", 1)(2, EntityGridComponent_Conditional_2_Template, 1, 7, "ag-grid-angular", 2)(3, EntityGridComponent_Conditional_3_Template, 4, 0, "div", 3)(4, EntityGridComponent_Conditional_4_Template, 4, 0, "div", 4);
634
+ i0.ɵɵelementEnd();
635
+ } if (rf & 2) {
636
+ i0.ɵɵstyleProp("height", ctx.height);
637
+ i0.ɵɵadvance();
638
+ i0.ɵɵconditional(ctx.isLoading && ctx.rowData.length === 0 ? 1 : ctx.entity && ctx.rowData.length > 0 ? 2 : ctx.entity && ctx.rowData.length === 0 ? 3 : 4);
639
+ } }, dependencies: [i1.AgGridAngular, i2.LoadingComponent], styles: [".entity-grid-container[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-grid[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n flex: 1;\n min-height: 0;\n}\n\n\n\n.no-data[_ngcontent-%COMP%], \n.no-entity[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n min-height: 200px;\n color: #9e9e9e;\n text-align: center;\n}\n\n.no-data[_ngcontent-%COMP%] i[_ngcontent-%COMP%], \n.no-entity[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%], \n.no-entity[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 14px;\n}\n\n\n\n .highlight-match {\n background-color: #fff176;\n border-radius: 2px;\n}"] });
640
+ }
641
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(EntityGridComponent, [{
642
+ type: Component,
643
+ args: [{ selector: 'mj-entity-grid', template: "<div class=\"entity-grid-container\" [style.height]=\"height\">\n @if (isLoading && rowData.length === 0) {\n <div class=\"loading-state\">\n <mj-loading text=\"Loading...\" size=\"medium\"></mj-loading>\n </div>\n } @else if (entity && rowData.length > 0) {\n <ag-grid-angular\n class=\"entity-grid\"\n [theme]=\"theme\"\n [columnDefs]=\"columnDefs\"\n [rowData]=\"rowData\"\n [defaultColDef]=\"defaultColDef\"\n [rowSelection]=\"enableSelection ? rowSelection : undefined\"\n [getRowId]=\"getRowId\"\n [suppressCellFocus]=\"true\"\n (gridReady)=\"onGridReady($event)\"\n (rowClicked)=\"onRowClicked($event)\"\n (rowDoubleClicked)=\"onRowDoubleClicked($event)\"\n (sortChanged)=\"onGridSortChanged($event)\"\n (columnResized)=\"onColumnResized($event)\"\n (columnMoved)=\"onColumnMoved($event)\">\n </ag-grid-angular>\n } @else if (entity && rowData.length === 0) {\n <div class=\"no-data\">\n <i class=\"fa-solid fa-inbox\"></i>\n <p>No records to display</p>\n </div>\n } @else {\n <div class=\"no-entity\">\n <i class=\"fa-solid fa-database\"></i>\n <p>Select an entity to view records</p>\n </div>\n }\n</div>\n", styles: [".entity-grid-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-grid {\n width: 100%;\n height: 100%;\n flex: 1;\n min-height: 0;\n}\n\n/* Empty states */\n.no-data,\n.no-entity {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n min-height: 200px;\n color: #9e9e9e;\n text-align: center;\n}\n\n.no-data i,\n.no-entity i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.no-data p,\n.no-entity p {\n margin: 0;\n font-size: 14px;\n}\n\n/* Highlight matches in grid cells */\n::ng-deep .highlight-match {\n background-color: #fff176;\n border-radius: 2px;\n}\n"] }]
644
+ }], null, { entity: [{
645
+ type: Input
646
+ }], records: [{
647
+ type: Input
648
+ }], selectedRecordId: [{
649
+ type: Input
650
+ }], columns: [{
651
+ type: Input
652
+ }], height: [{
653
+ type: Input
654
+ }], enableSelection: [{
655
+ type: Input
656
+ }], filterText: [{
657
+ type: Input
658
+ }], sortState: [{
659
+ type: Input
660
+ }], serverSideSorting: [{
661
+ type: Input
662
+ }], pageSize: [{
663
+ type: Input
664
+ }], gridState: [{
665
+ type: Input
666
+ }], recordSelected: [{
667
+ type: Output
668
+ }], recordOpened: [{
669
+ type: Output
670
+ }], sortChanged: [{
671
+ type: Output
672
+ }], gridStateChanged: [{
673
+ type: Output
674
+ }] }); })();
675
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityGridComponent, { className: "EntityGridComponent", filePath: "src/lib/entity-grid/entity-grid.component.ts", lineNumber: 56 }); })();
676
+ //# sourceMappingURL=entity-grid.component.js.map