@memberjunction/ng-entity-viewer 2.133.0 → 3.0.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.
- package/dist/lib/entity-cards/entity-cards.component.js +2 -2
- package/dist/lib/entity-cards/entity-cards.component.js.map +1 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +2 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.js +14 -11
- package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
- package/dist/lib/entity-grid/entity-grid.component.d.ts +216 -0
- package/dist/lib/entity-grid/entity-grid.component.d.ts.map +1 -0
- package/dist/lib/entity-grid/entity-grid.component.js +676 -0
- package/dist/lib/entity-grid/entity-grid.component.js.map +1 -0
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +2 -2
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts +45 -1
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.js +71 -7
- package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
- package/dist/lib/pagination/pagination.component.js +2 -2
- package/dist/lib/pagination/pagination.component.js.map +1 -1
- package/dist/lib/pill/pill.component.js +2 -2
- package/dist/lib/pill/pill.component.js.map +1 -1
- package/dist/lib/view-config-panel/view-config-panel.component.js +2 -2
- package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/package.json +15 -15
|
@@ -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" }); })();
|
|
676
|
+
//# sourceMappingURL=entity-grid.component.js.map
|