@memberjunction/ng-entity-viewer 2.133.0 → 3.1.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 +1 -1
- package/dist/lib/entity-cards/entity-cards.component.js.map +1 -1
- package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +10 -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 +87 -35
- package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
- package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +1 -1
- 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 +89 -13
- package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
- package/dist/lib/entity-viewer/entity-viewer.component.js +317 -136
- package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
- package/dist/lib/pagination/pagination.component.js +1 -1
- package/dist/lib/pagination/pagination.component.js.map +1 -1
- package/dist/lib/pill/pill.component.js +1 -1
- package/dist/lib/pill/pill.component.js.map +1 -1
- package/dist/lib/view-config-panel/view-config-panel.component.d.ts +87 -2
- package/dist/lib/view-config-panel/view-config-panel.component.d.ts.map +1 -1
- package/dist/lib/view-config-panel/view-config-panel.component.js +720 -426
- 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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
1
|
+
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
|
|
2
2
|
import { Subject } from 'rxjs';
|
|
3
3
|
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';
|
|
4
|
-
import { EntityFieldTSType, RunView } from '@memberjunction/core';
|
|
4
|
+
import { EntityFieldTSType, RunView, Metadata } from '@memberjunction/core';
|
|
5
5
|
import { TimelineGroup } from '@memberjunction/ng-timeline';
|
|
6
6
|
import { DEFAULT_VIEWER_CONFIG } from '../types';
|
|
7
|
+
import { EntityDataGridComponent } from '../entity-data-grid/entity-data-grid.component';
|
|
7
8
|
import * as i0 from "@angular/core";
|
|
8
9
|
import * as i1 from "@angular/forms";
|
|
9
10
|
import * as i2 from "@memberjunction/ng-shared-generic";
|
|
@@ -164,7 +165,7 @@ function EntityViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
|
164
165
|
i0.ɵɵadvance();
|
|
165
166
|
i0.ɵɵconditional(ctx_r1.effectiveConfig.showFilter ? 1 : -1);
|
|
166
167
|
i0.ɵɵadvance();
|
|
167
|
-
i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.
|
|
168
|
+
i0.ɵɵconditional(ctx_r1.effectiveConfig.showRecordCount && ctx_r1.effectiveEntity ? 2 : -1);
|
|
168
169
|
i0.ɵɵadvance();
|
|
169
170
|
i0.ɵɵconditional(ctx_r1.effectiveConfig.showViewModeToggle ? 3 : -1);
|
|
170
171
|
i0.ɵɵadvance();
|
|
@@ -215,20 +216,70 @@ function EntityViewerComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) {
|
|
|
215
216
|
export class EntityViewerComponent {
|
|
216
217
|
cdr;
|
|
217
218
|
// ========================================
|
|
218
|
-
// INPUTS
|
|
219
|
+
// INPUTS (using getter/setter pattern)
|
|
219
220
|
// ========================================
|
|
221
|
+
_entity = null;
|
|
222
|
+
_records = null;
|
|
223
|
+
_config = {};
|
|
224
|
+
_viewMode = null;
|
|
225
|
+
_filterText = null;
|
|
226
|
+
_sortState = null;
|
|
227
|
+
_viewEntity = null;
|
|
228
|
+
_timelineConfig = null;
|
|
229
|
+
_initialized = false;
|
|
220
230
|
/**
|
|
221
231
|
* The entity to display records for
|
|
222
232
|
*/
|
|
223
|
-
entity
|
|
233
|
+
get entity() {
|
|
234
|
+
return this._entity;
|
|
235
|
+
}
|
|
236
|
+
set entity(value) {
|
|
237
|
+
this._entity = value;
|
|
238
|
+
// Detect date fields for timeline support
|
|
239
|
+
this.detectDateFields();
|
|
240
|
+
if (this._initialized) {
|
|
241
|
+
if (value && !this._records) {
|
|
242
|
+
// Reset state for new entity - synchronously clear all data and force change detection
|
|
243
|
+
// before starting the async load to prevent stale data display
|
|
244
|
+
this.resetPaginationState();
|
|
245
|
+
this.cdr.detectChanges();
|
|
246
|
+
this.loadData();
|
|
247
|
+
}
|
|
248
|
+
else if (!value) {
|
|
249
|
+
this.internalRecords = [];
|
|
250
|
+
this.totalRecordCount = 0;
|
|
251
|
+
this.filteredRecordCount = 0;
|
|
252
|
+
this.resetPaginationState();
|
|
253
|
+
this.cdr.detectChanges();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
224
257
|
/**
|
|
225
258
|
* Pre-loaded records (optional - if not provided, component loads data)
|
|
226
259
|
*/
|
|
227
|
-
records
|
|
260
|
+
get records() {
|
|
261
|
+
return this._records;
|
|
262
|
+
}
|
|
263
|
+
set records(value) {
|
|
264
|
+
this._records = value;
|
|
265
|
+
if (value) {
|
|
266
|
+
this.internalRecords = value;
|
|
267
|
+
this.totalRecordCount = value.length;
|
|
268
|
+
this.filteredRecordCount = value.length;
|
|
269
|
+
// Update timeline with new records
|
|
270
|
+
this.updateTimelineGroups();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
228
273
|
/**
|
|
229
274
|
* Configuration options for the viewer
|
|
230
275
|
*/
|
|
231
|
-
config
|
|
276
|
+
get config() {
|
|
277
|
+
return this._config;
|
|
278
|
+
}
|
|
279
|
+
set config(value) {
|
|
280
|
+
this._config = value;
|
|
281
|
+
this.applyConfig();
|
|
282
|
+
}
|
|
232
283
|
/**
|
|
233
284
|
* Currently selected record ID (primary key string)
|
|
234
285
|
*/
|
|
@@ -237,16 +288,67 @@ export class EntityViewerComponent {
|
|
|
237
288
|
* External view mode - allows parent to control view mode
|
|
238
289
|
* Supports two-way binding: [(viewMode)]="state.viewMode"
|
|
239
290
|
*/
|
|
240
|
-
viewMode
|
|
291
|
+
get viewMode() {
|
|
292
|
+
return this._viewMode;
|
|
293
|
+
}
|
|
294
|
+
set viewMode(value) {
|
|
295
|
+
this._viewMode = value;
|
|
296
|
+
if (value !== null) {
|
|
297
|
+
this.internalViewMode = value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
241
300
|
/**
|
|
242
301
|
* External filter text - allows parent to control filter
|
|
243
302
|
* Supports two-way binding: [(filterText)]="state.filterText"
|
|
244
303
|
*/
|
|
245
|
-
filterText
|
|
304
|
+
get filterText() {
|
|
305
|
+
return this._filterText;
|
|
306
|
+
}
|
|
307
|
+
set filterText(value) {
|
|
308
|
+
const oldFilter = this.debouncedFilterText;
|
|
309
|
+
this._filterText = value;
|
|
310
|
+
const newFilter = value ?? '';
|
|
311
|
+
this.internalFilterText = newFilter;
|
|
312
|
+
this.debouncedFilterText = newFilter;
|
|
313
|
+
if (this._initialized) {
|
|
314
|
+
// If server-side filtering and filter changed, reload from page 1
|
|
315
|
+
// Keep existing records visible during refresh for better UX
|
|
316
|
+
if (this.effectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this._records) {
|
|
317
|
+
this.resetPaginationState(false);
|
|
318
|
+
this.loadData();
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
this.updateFilteredCount();
|
|
322
|
+
}
|
|
323
|
+
this.cdr.detectChanges();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
246
326
|
/**
|
|
247
327
|
* External sort state - allows parent to control sorting
|
|
248
328
|
*/
|
|
249
|
-
sortState
|
|
329
|
+
get sortState() {
|
|
330
|
+
return this._sortState;
|
|
331
|
+
}
|
|
332
|
+
set sortState(value) {
|
|
333
|
+
const oldSort = this.internalSortState;
|
|
334
|
+
this._sortState = value;
|
|
335
|
+
if (value !== null) {
|
|
336
|
+
this.internalSortState = value;
|
|
337
|
+
if (this._initialized) {
|
|
338
|
+
// If sort changed and using server-side sorting, reload
|
|
339
|
+
// Keep existing records visible during refresh for better UX
|
|
340
|
+
if (this.effectiveConfig.serverSideSorting && !this._records) {
|
|
341
|
+
const sortChanged = !oldSort || !value ||
|
|
342
|
+
oldSort.field !== value.field ||
|
|
343
|
+
oldSort.direction !== value.direction;
|
|
344
|
+
if (sortChanged) {
|
|
345
|
+
this.resetPaginationState(false);
|
|
346
|
+
this.loadData();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
250
352
|
/**
|
|
251
353
|
* Custom grid column definitions
|
|
252
354
|
*/
|
|
@@ -260,7 +362,26 @@ export class EntityViewerComponent {
|
|
|
260
362
|
* When provided, the component will use the view's WhereClause, GridState, SortState, etc.
|
|
261
363
|
* The view's filter is additive - UserSearchString is applied ON TOP of the view's WhereClause
|
|
262
364
|
*/
|
|
263
|
-
viewEntity
|
|
365
|
+
get viewEntity() {
|
|
366
|
+
return this._viewEntity;
|
|
367
|
+
}
|
|
368
|
+
set viewEntity(value) {
|
|
369
|
+
this._viewEntity = value;
|
|
370
|
+
if (this._initialized && this._entity && !this._records) {
|
|
371
|
+
// Apply view's sort state if available
|
|
372
|
+
if (value) {
|
|
373
|
+
const viewSortInfo = value.ViewSortInfo;
|
|
374
|
+
if (viewSortInfo && viewSortInfo.length > 0) {
|
|
375
|
+
this.internalSortState = {
|
|
376
|
+
field: viewSortInfo[0].field,
|
|
377
|
+
direction: viewSortInfo[0].direction === 'Desc' ? 'desc' : 'asc'
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
this.resetPaginationState();
|
|
382
|
+
this.loadData();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
264
385
|
/**
|
|
265
386
|
* Grid state configuration from a User View
|
|
266
387
|
* Controls column visibility, widths, order, and sort settings
|
|
@@ -284,13 +405,12 @@ export class EntityViewerComponent {
|
|
|
284
405
|
prev.segmentGrouping === value.segmentGrouping);
|
|
285
406
|
if (!isEqual) {
|
|
286
407
|
this._timelineConfig = value;
|
|
287
|
-
if (value && this.
|
|
408
|
+
if (value && this._entity) {
|
|
288
409
|
this.configureTimeline();
|
|
289
410
|
this.cdr.markForCheck();
|
|
290
411
|
}
|
|
291
412
|
}
|
|
292
413
|
}
|
|
293
|
-
_timelineConfig = null;
|
|
294
414
|
/**
|
|
295
415
|
* Whether to show the grid toolbar.
|
|
296
416
|
* When false, the grid is displayed without its own toolbar - useful when
|
|
@@ -308,6 +428,12 @@ export class EntityViewerComponent {
|
|
|
308
428
|
* @default 'single'
|
|
309
429
|
*/
|
|
310
430
|
gridSelectionMode = 'single';
|
|
431
|
+
/**
|
|
432
|
+
* Show the "Add to List" button in the grid toolbar.
|
|
433
|
+
* Requires gridSelectionMode to be 'multiple' for best UX.
|
|
434
|
+
* @default false
|
|
435
|
+
*/
|
|
436
|
+
showAddToListButton = false;
|
|
311
437
|
// ========================================
|
|
312
438
|
// OUTPUTS
|
|
313
439
|
// ========================================
|
|
@@ -364,6 +490,16 @@ export class EntityViewerComponent {
|
|
|
364
490
|
* Emitted when the Export button is clicked in the grid toolbar
|
|
365
491
|
*/
|
|
366
492
|
exportRequested = new EventEmitter();
|
|
493
|
+
/**
|
|
494
|
+
* Emitted when the Add to List button is clicked in the grid toolbar.
|
|
495
|
+
* Parent components should handle this to show the list management dialog.
|
|
496
|
+
*/
|
|
497
|
+
addToListRequested = new EventEmitter();
|
|
498
|
+
/**
|
|
499
|
+
* Emitted when grid selection changes.
|
|
500
|
+
* Parent components can use this to track selected records for their own toolbar buttons.
|
|
501
|
+
*/
|
|
502
|
+
selectionChanged = new EventEmitter();
|
|
367
503
|
// ========================================
|
|
368
504
|
// INTERNAL STATE
|
|
369
505
|
// ========================================
|
|
@@ -428,12 +564,69 @@ export class EntityViewerComponent {
|
|
|
428
564
|
filterInput$ = new Subject();
|
|
429
565
|
/** Track if this is the first load (vs. load more) */
|
|
430
566
|
isInitialLoad = true;
|
|
567
|
+
/** Reference to the data grid component for flushing pending changes */
|
|
568
|
+
dataGridRef;
|
|
431
569
|
constructor(cdr) {
|
|
432
570
|
this.cdr = cdr;
|
|
433
571
|
}
|
|
434
572
|
// ========================================
|
|
573
|
+
// PUBLIC METHODS
|
|
574
|
+
// ========================================
|
|
575
|
+
/**
|
|
576
|
+
* Ensures any pending grid state changes are saved immediately without waiting for debounce.
|
|
577
|
+
* Call this before switching views or entities to ensure changes are saved.
|
|
578
|
+
*/
|
|
579
|
+
EnsurePendingChangesSaved() {
|
|
580
|
+
this.dataGridRef?.EnsurePendingChangesSaved();
|
|
581
|
+
}
|
|
582
|
+
// ========================================
|
|
435
583
|
// COMPUTED PROPERTIES
|
|
436
584
|
// ========================================
|
|
585
|
+
/**
|
|
586
|
+
* Get the effective entity - uses entity input if provided, otherwise derives from viewEntity
|
|
587
|
+
* This allows callers to provide just a viewEntity without explicitly setting the entity input.
|
|
588
|
+
* Uses fallback resolution when ViewEntityInfo is not available.
|
|
589
|
+
*/
|
|
590
|
+
get effectiveEntity() {
|
|
591
|
+
if (this.entity) {
|
|
592
|
+
return this.entity;
|
|
593
|
+
}
|
|
594
|
+
// Auto-derive from viewEntity if available
|
|
595
|
+
if (this.viewEntity) {
|
|
596
|
+
return this.getEntityInfoFromViewEntity(this.viewEntity);
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Gets EntityInfo from a ViewEntity with multiple fallback strategies.
|
|
602
|
+
* Priority: 1) ViewEntityInfo property (set by Load)
|
|
603
|
+
* 2) Entity name lookup (virtual field)
|
|
604
|
+
* 3) EntityID lookup
|
|
605
|
+
* Returns null if entity cannot be determined.
|
|
606
|
+
*/
|
|
607
|
+
getEntityInfoFromViewEntity(viewEntity) {
|
|
608
|
+
// First try: ViewEntityInfo is the preferred source (set by UserViewEntityExtended.Load)
|
|
609
|
+
if (viewEntity.ViewEntityInfo) {
|
|
610
|
+
return viewEntity.ViewEntityInfo;
|
|
611
|
+
}
|
|
612
|
+
const md = new Metadata();
|
|
613
|
+
// Second try: Look up by Entity name (virtual field that returns entity name)
|
|
614
|
+
if (viewEntity.Entity) {
|
|
615
|
+
const entityByName = md.Entities.find(e => e.Name === viewEntity.Entity);
|
|
616
|
+
if (entityByName) {
|
|
617
|
+
return entityByName;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Third try: Look up by EntityID
|
|
621
|
+
if (viewEntity.EntityID) {
|
|
622
|
+
const entityById = md.Entities.find(e => e.ID === viewEntity.EntityID);
|
|
623
|
+
if (entityById) {
|
|
624
|
+
return entityById;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
console.warn(`[EntityViewer] Could not determine entity for view "${viewEntity.Name}" (ID: ${viewEntity.ID})`);
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
437
630
|
/**
|
|
438
631
|
* Get the effective view mode (external or internal)
|
|
439
632
|
*/
|
|
@@ -495,17 +688,18 @@ export class EntityViewerComponent {
|
|
|
495
688
|
* which would cause the grid to reinitialize
|
|
496
689
|
*/
|
|
497
690
|
get gridParams() {
|
|
498
|
-
|
|
691
|
+
const entity = this.effectiveEntity;
|
|
692
|
+
if (!entity) {
|
|
499
693
|
return null;
|
|
500
694
|
}
|
|
501
695
|
// Check if we need to recreate the params object
|
|
502
|
-
const entityChanged = this._lastGridParamsEntity !==
|
|
696
|
+
const entityChanged = this._lastGridParamsEntity !== entity.Name;
|
|
503
697
|
const viewEntityChanged = this._lastGridParamsViewEntity !== this.viewEntity;
|
|
504
698
|
if (entityChanged || viewEntityChanged || !this._cachedGridParams) {
|
|
505
|
-
this._lastGridParamsEntity =
|
|
699
|
+
this._lastGridParamsEntity = entity.Name;
|
|
506
700
|
this._lastGridParamsViewEntity = this.viewEntity ?? null;
|
|
507
701
|
this._cachedGridParams = {
|
|
508
|
-
EntityName:
|
|
702
|
+
EntityName: entity.Name,
|
|
509
703
|
ViewEntity: this.viewEntity || undefined
|
|
510
704
|
};
|
|
511
705
|
}
|
|
@@ -672,91 +866,11 @@ export class EntityViewerComponent {
|
|
|
672
866
|
direction: this.effectiveConfig.defaultSortDirection ?? 'asc'
|
|
673
867
|
};
|
|
674
868
|
}
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (changes['config']) {
|
|
681
|
-
this.applyConfig();
|
|
682
|
-
}
|
|
683
|
-
if (changes['entity']) {
|
|
684
|
-
// Detect date fields for timeline support
|
|
685
|
-
this.detectDateFields();
|
|
686
|
-
if (this.entity && !this.records) {
|
|
687
|
-
// Reset state for new entity - synchronously clear all data and force change detection
|
|
688
|
-
// before starting the async load to prevent stale data display
|
|
689
|
-
this.resetPaginationState();
|
|
690
|
-
this.cdr.detectChanges();
|
|
691
|
-
this.loadData();
|
|
692
|
-
}
|
|
693
|
-
else if (!this.entity) {
|
|
694
|
-
this.internalRecords = [];
|
|
695
|
-
this.totalRecordCount = 0;
|
|
696
|
-
this.filteredRecordCount = 0;
|
|
697
|
-
this.resetPaginationState();
|
|
698
|
-
this.cdr.detectChanges();
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
if (changes['records'] && this.records) {
|
|
702
|
-
this.internalRecords = this.records;
|
|
703
|
-
this.totalRecordCount = this.records.length;
|
|
704
|
-
this.filteredRecordCount = this.records.length;
|
|
705
|
-
// Update timeline with new records
|
|
706
|
-
this.updateTimelineGroups();
|
|
707
|
-
}
|
|
708
|
-
// Timeline config is now handled by setter - no ngOnChanges handling needed
|
|
709
|
-
// Handle external filter text changes (from parent component)
|
|
710
|
-
if (changes['filterText']) {
|
|
711
|
-
const newFilter = this.filterText ?? '';
|
|
712
|
-
const oldFilter = this.debouncedFilterText;
|
|
713
|
-
this.internalFilterText = newFilter;
|
|
714
|
-
this.debouncedFilterText = newFilter;
|
|
715
|
-
// If server-side filtering and filter changed, reload from page 1
|
|
716
|
-
if (this.effectiveConfig.serverSideFiltering && newFilter !== oldFilter && !this.records) {
|
|
717
|
-
this.resetPaginationState();
|
|
718
|
-
this.loadData();
|
|
719
|
-
}
|
|
720
|
-
else {
|
|
721
|
-
this.updateFilteredCount();
|
|
722
|
-
}
|
|
723
|
-
this.cdr.detectChanges();
|
|
724
|
-
}
|
|
725
|
-
// Handle external view mode changes
|
|
726
|
-
if (changes['viewMode'] && this.viewMode !== null) {
|
|
727
|
-
this.internalViewMode = this.viewMode;
|
|
728
|
-
}
|
|
729
|
-
// Handle external sort state changes
|
|
730
|
-
if (changes['sortState'] && this.sortState !== null) {
|
|
731
|
-
const oldSort = this.internalSortState;
|
|
732
|
-
this.internalSortState = this.sortState;
|
|
733
|
-
// If sort changed and using server-side sorting, reload
|
|
734
|
-
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
735
|
-
const sortChanged = !oldSort || !this.sortState ||
|
|
736
|
-
oldSort.field !== this.sortState.field ||
|
|
737
|
-
oldSort.direction !== this.sortState.direction;
|
|
738
|
-
if (sortChanged) {
|
|
739
|
-
this.resetPaginationState();
|
|
740
|
-
this.loadData();
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
// Handle viewEntity changes - reload data when view changes
|
|
745
|
-
if (changes['viewEntity']) {
|
|
746
|
-
if (this.entity && !this.records) {
|
|
747
|
-
// Apply view's sort state if available
|
|
748
|
-
if (this.viewEntity) {
|
|
749
|
-
const viewSortInfo = this.viewEntity.ViewSortInfo;
|
|
750
|
-
if (viewSortInfo && viewSortInfo.length > 0) {
|
|
751
|
-
this.internalSortState = {
|
|
752
|
-
field: viewSortInfo[0].field,
|
|
753
|
-
direction: viewSortInfo[0].direction === 'Desc' ? 'desc' : 'asc'
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
this.resetPaginationState();
|
|
758
|
-
this.loadData();
|
|
759
|
-
}
|
|
869
|
+
// Mark as initialized - setters will now trigger data loading
|
|
870
|
+
this._initialized = true;
|
|
871
|
+
// If entity was set before initialization, load data now
|
|
872
|
+
if (this._entity && !this._records) {
|
|
873
|
+
this.loadData();
|
|
760
874
|
}
|
|
761
875
|
}
|
|
762
876
|
ngOnDestroy() {
|
|
@@ -781,8 +895,9 @@ export class EntityViewerComponent {
|
|
|
781
895
|
this.debouncedFilterText = filterText;
|
|
782
896
|
this.filterTextChange.emit(filterText);
|
|
783
897
|
// If server-side filtering and filter changed, reload from page 1
|
|
898
|
+
// Keep existing records visible during refresh for better UX
|
|
784
899
|
if (this.effectiveConfig.serverSideFiltering && filterText !== oldFilter && !this.records) {
|
|
785
|
-
this.resetPaginationState();
|
|
900
|
+
this.resetPaginationState(false);
|
|
786
901
|
this.loadData();
|
|
787
902
|
}
|
|
788
903
|
else {
|
|
@@ -806,42 +921,54 @@ export class EntityViewerComponent {
|
|
|
806
921
|
}
|
|
807
922
|
/**
|
|
808
923
|
* Reset pagination state for a fresh load.
|
|
809
|
-
*
|
|
924
|
+
* When clearRecords is true (default), clears all record data - use for entity switches.
|
|
925
|
+
* When clearRecords is false, keeps existing records visible during refresh - use for sort/filter changes.
|
|
810
926
|
*/
|
|
811
|
-
resetPaginationState() {
|
|
927
|
+
resetPaginationState(clearRecords = true) {
|
|
812
928
|
this.pagination = {
|
|
813
929
|
currentPage: 0,
|
|
814
930
|
pageSize: this.effectiveConfig.pageSize,
|
|
815
|
-
totalRecords: 0,
|
|
931
|
+
totalRecords: clearRecords ? 0 : this.pagination.totalRecords,
|
|
816
932
|
hasMore: false,
|
|
817
933
|
isLoading: false
|
|
818
934
|
};
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
935
|
+
if (clearRecords) {
|
|
936
|
+
this.internalRecords = [];
|
|
937
|
+
this.totalRecordCount = 0;
|
|
938
|
+
this.filteredRecordCount = 0;
|
|
939
|
+
}
|
|
822
940
|
this.isInitialLoad = true;
|
|
823
941
|
}
|
|
824
942
|
// ========================================
|
|
825
943
|
// DATA LOADING
|
|
826
944
|
// ========================================
|
|
945
|
+
// Sequence counter for tracking load requests and detecting stale responses
|
|
946
|
+
_loadSequence = 0;
|
|
947
|
+
// Flag to indicate a reload is pending (requested while another load was in progress)
|
|
948
|
+
_pendingReload = false;
|
|
827
949
|
/**
|
|
828
950
|
* Load data for the current entity with server-side filtering/sorting/pagination
|
|
829
951
|
*/
|
|
830
952
|
async loadData() {
|
|
831
|
-
|
|
953
|
+
const entity = this.effectiveEntity;
|
|
954
|
+
if (!entity) {
|
|
832
955
|
this.internalRecords = [];
|
|
833
956
|
this.totalRecordCount = 0;
|
|
834
957
|
this.filteredRecordCount = 0;
|
|
835
958
|
return;
|
|
836
959
|
}
|
|
837
|
-
//
|
|
960
|
+
// Increment sequence to track this load request
|
|
961
|
+
const loadId = ++this._loadSequence;
|
|
962
|
+
// If a load is already in progress, mark that we need to reload when it completes
|
|
963
|
+
// This handles the case where view/filter changes occur during an active load
|
|
838
964
|
if (this.isLoading) {
|
|
965
|
+
this._pendingReload = true;
|
|
839
966
|
return;
|
|
840
967
|
}
|
|
841
968
|
this.isLoading = true;
|
|
842
969
|
this.pagination.isLoading = true;
|
|
843
970
|
this.loadingMessage = this.isInitialLoad
|
|
844
|
-
? `Loading ${
|
|
971
|
+
? `Loading ${entity.Name}...`
|
|
845
972
|
: 'Loading more records...';
|
|
846
973
|
this.cdr.detectChanges();
|
|
847
974
|
const startTime = Date.now();
|
|
@@ -864,14 +991,22 @@ export class EntityViewerComponent {
|
|
|
864
991
|
// The view's WhereClause is the "business filter" - UserSearchString is additive
|
|
865
992
|
const extraFilter = this.viewEntity?.WhereClause || undefined;
|
|
866
993
|
const result = await rv.RunView({
|
|
867
|
-
EntityName:
|
|
994
|
+
EntityName: entity.Name,
|
|
868
995
|
ResultType: 'entity_object',
|
|
869
996
|
MaxRows: config.pageSize,
|
|
870
997
|
StartRow: startRow,
|
|
871
998
|
OrderBy: orderBy,
|
|
872
999
|
ExtraFilter: extraFilter,
|
|
873
|
-
UserSearchString
|
|
1000
|
+
// Only use UserSearchString for regular text search, NOT for smart filters
|
|
1001
|
+
// Smart filters generate WhereClause via AI on the server, so the prompt text should not be passed as UserSearchString
|
|
1002
|
+
UserSearchString: config.serverSideFiltering && !this.viewEntity?.SmartFilterEnabled
|
|
1003
|
+
? this.debouncedFilterText || undefined
|
|
1004
|
+
: undefined
|
|
874
1005
|
});
|
|
1006
|
+
// Check if this load is still the current one (detect stale responses)
|
|
1007
|
+
if (loadId !== this._loadSequence) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
875
1010
|
if (result.Success) {
|
|
876
1011
|
// Append or replace records based on whether this is initial load
|
|
877
1012
|
if (this.isInitialLoad) {
|
|
@@ -899,7 +1034,6 @@ export class EntityViewerComponent {
|
|
|
899
1034
|
this.updateTimelineGroups();
|
|
900
1035
|
}
|
|
901
1036
|
else {
|
|
902
|
-
console.error('Failed to load records:', result.ErrorMessage);
|
|
903
1037
|
if (this.isInitialLoad) {
|
|
904
1038
|
this.internalRecords = [];
|
|
905
1039
|
}
|
|
@@ -908,7 +1042,6 @@ export class EntityViewerComponent {
|
|
|
908
1042
|
}
|
|
909
1043
|
}
|
|
910
1044
|
catch (error) {
|
|
911
|
-
console.error('Error loading records:', error);
|
|
912
1045
|
if (this.isInitialLoad) {
|
|
913
1046
|
this.internalRecords = [];
|
|
914
1047
|
}
|
|
@@ -920,6 +1053,13 @@ export class EntityViewerComponent {
|
|
|
920
1053
|
this.pagination.isLoading = false;
|
|
921
1054
|
this.isInitialLoad = false;
|
|
922
1055
|
this.cdr.detectChanges();
|
|
1056
|
+
// If a reload was requested while we were loading, trigger it now
|
|
1057
|
+
if (this._pendingReload) {
|
|
1058
|
+
this._pendingReload = false;
|
|
1059
|
+
this.resetPaginationState();
|
|
1060
|
+
// Use setTimeout to break the call stack and allow Angular to process
|
|
1061
|
+
setTimeout(() => this.loadData(), 0);
|
|
1062
|
+
}
|
|
923
1063
|
}
|
|
924
1064
|
}
|
|
925
1065
|
/**
|
|
@@ -934,10 +1074,11 @@ export class EntityViewerComponent {
|
|
|
934
1074
|
}
|
|
935
1075
|
/**
|
|
936
1076
|
* Refresh data (re-load from server, starting at page 1)
|
|
1077
|
+
* Keeps existing records visible during refresh for better UX
|
|
937
1078
|
*/
|
|
938
1079
|
refresh() {
|
|
939
1080
|
if (!this.records) {
|
|
940
|
-
this.resetPaginationState();
|
|
1081
|
+
this.resetPaginationState(false);
|
|
941
1082
|
this.loadData();
|
|
942
1083
|
}
|
|
943
1084
|
}
|
|
@@ -983,12 +1124,13 @@ export class EntityViewerComponent {
|
|
|
983
1124
|
this.internalSortState = event.sort;
|
|
984
1125
|
this.sortChanged.emit(event);
|
|
985
1126
|
// If server-side sorting, reload from page 1
|
|
1127
|
+
// Keep existing records visible during refresh for better UX
|
|
986
1128
|
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
987
1129
|
const sortChanged = !oldSort || !event.sort ||
|
|
988
1130
|
oldSort.field !== event.sort?.field ||
|
|
989
1131
|
oldSort.direction !== event.sort?.direction;
|
|
990
1132
|
if (sortChanged) {
|
|
991
|
-
this.resetPaginationState();
|
|
1133
|
+
this.resetPaginationState(false);
|
|
992
1134
|
this.loadData();
|
|
993
1135
|
}
|
|
994
1136
|
}
|
|
@@ -1028,11 +1170,12 @@ export class EntityViewerComponent {
|
|
|
1028
1170
|
* Maps to recordSelected event for parent components
|
|
1029
1171
|
*/
|
|
1030
1172
|
onDataGridRowClick(event) {
|
|
1031
|
-
|
|
1173
|
+
const entity = this.effectiveEntity;
|
|
1174
|
+
if (!entity || !event.row)
|
|
1032
1175
|
return;
|
|
1033
1176
|
this.recordSelected.emit({
|
|
1034
1177
|
record: event.row,
|
|
1035
|
-
entity:
|
|
1178
|
+
entity: entity,
|
|
1036
1179
|
compositeKey: event.row.PrimaryKey
|
|
1037
1180
|
});
|
|
1038
1181
|
}
|
|
@@ -1041,11 +1184,12 @@ export class EntityViewerComponent {
|
|
|
1041
1184
|
* Maps to recordOpened event for parent components
|
|
1042
1185
|
*/
|
|
1043
1186
|
onDataGridRowDoubleClick(event) {
|
|
1044
|
-
|
|
1187
|
+
const entity = this.effectiveEntity;
|
|
1188
|
+
if (!entity || !event.row)
|
|
1045
1189
|
return;
|
|
1046
1190
|
this.recordOpened.emit({
|
|
1047
1191
|
record: event.row,
|
|
1048
|
-
entity:
|
|
1192
|
+
entity: entity,
|
|
1049
1193
|
compositeKey: event.row.PrimaryKey
|
|
1050
1194
|
});
|
|
1051
1195
|
}
|
|
@@ -1064,8 +1208,9 @@ export class EntityViewerComponent {
|
|
|
1064
1208
|
this.internalSortState = newSort;
|
|
1065
1209
|
this.sortChanged.emit({ sort: newSort });
|
|
1066
1210
|
// If server-side sorting, reload from page 1
|
|
1211
|
+
// Keep existing records visible during refresh for better UX
|
|
1067
1212
|
if (this.effectiveConfig.serverSideSorting && !this.records) {
|
|
1068
|
-
this.resetPaginationState();
|
|
1213
|
+
this.resetPaginationState(false);
|
|
1069
1214
|
this.loadData();
|
|
1070
1215
|
}
|
|
1071
1216
|
}
|
|
@@ -1095,6 +1240,27 @@ export class EntityViewerComponent {
|
|
|
1095
1240
|
onGridExportRequested() {
|
|
1096
1241
|
this.exportRequested.emit({ format: 'excel' });
|
|
1097
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Handle Add to List button click from data grid toolbar.
|
|
1245
|
+
* Forwards the event to parent components for list management.
|
|
1246
|
+
*/
|
|
1247
|
+
onGridAddToListRequested(event) {
|
|
1248
|
+
this.addToListRequested.emit(event);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Handle selection change from data grid.
|
|
1252
|
+
* Converts selected keys to records and forwards to parent components.
|
|
1253
|
+
*/
|
|
1254
|
+
onGridSelectionChange(selectedKeys) {
|
|
1255
|
+
// Find the actual records from our filtered records
|
|
1256
|
+
const records = this.filteredRecords.filter(record => {
|
|
1257
|
+
const key = record.PrimaryKey?.ToConcatenatedString() || String(record.Get('ID'));
|
|
1258
|
+
return selectedKeys.includes(key);
|
|
1259
|
+
});
|
|
1260
|
+
// Get the raw primary key values for list management
|
|
1261
|
+
const recordIds = records.map(record => String(record.PrimaryKey.KeyValuePairs[0].Value));
|
|
1262
|
+
this.selectionChanged.emit({ records, recordIds });
|
|
1263
|
+
}
|
|
1098
1264
|
// ========================================
|
|
1099
1265
|
// TIMELINE METHODS
|
|
1100
1266
|
// ========================================
|
|
@@ -1103,10 +1269,11 @@ export class EntityViewerComponent {
|
|
|
1103
1269
|
*/
|
|
1104
1270
|
onTimelineEventClick(event) {
|
|
1105
1271
|
const record = event.event.entity;
|
|
1106
|
-
|
|
1272
|
+
const entity = this.effectiveEntity;
|
|
1273
|
+
if (record && entity) {
|
|
1107
1274
|
this.recordSelected.emit({
|
|
1108
1275
|
record,
|
|
1109
|
-
entity:
|
|
1276
|
+
entity: entity,
|
|
1110
1277
|
compositeKey: record.PrimaryKey
|
|
1111
1278
|
});
|
|
1112
1279
|
}
|
|
@@ -1339,8 +1506,13 @@ export class EntityViewerComponent {
|
|
|
1339
1506
|
const firstOther = fields.find(f => f.Name !== excludeField);
|
|
1340
1507
|
return firstOther?.Name || null;
|
|
1341
1508
|
}
|
|
1342
|
-
static ɵfac = function EntityViewerComponent_Factory(
|
|
1343
|
-
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: EntityViewerComponent, selectors: [["mj-entity-viewer"]],
|
|
1509
|
+
static ɵfac = function EntityViewerComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || EntityViewerComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
|
|
1510
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: EntityViewerComponent, selectors: [["mj-entity-viewer"]], viewQuery: function EntityViewerComponent_Query(rf, ctx) { if (rf & 1) {
|
|
1511
|
+
i0.ɵɵviewQuery(EntityDataGridComponent, 5);
|
|
1512
|
+
} if (rf & 2) {
|
|
1513
|
+
let _t;
|
|
1514
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.dataGridRef = _t.first);
|
|
1515
|
+
} }, hostAttrs: [2, "display", "block", "height", "100%"], inputs: { entity: "entity", records: "records", config: "config", selectedRecordId: "selectedRecordId", viewMode: "viewMode", filterText: "filterText", sortState: "sortState", gridColumns: "gridColumns", cardTemplate: "cardTemplate", viewEntity: "viewEntity", gridState: "gridState", timelineConfig: "timelineConfig", showGridToolbar: "showGridToolbar", gridToolbarConfig: "gridToolbarConfig", gridSelectionMode: "gridSelectionMode", showAddToListButton: "showAddToListButton" }, outputs: { recordSelected: "recordSelected", recordOpened: "recordOpened", dataLoaded: "dataLoaded", viewModeChange: "viewModeChange", filterTextChange: "filterTextChange", filteredCountChanged: "filteredCountChanged", sortChanged: "sortChanged", gridStateChanged: "gridStateChanged", timelineConfigChange: "timelineConfigChange", addRequested: "addRequested", deleteRequested: "deleteRequested", refreshRequested: "refreshRequested", exportRequested: "exportRequested", addToListRequested: "addToListRequested", selectionChanged: "selectionChanged" }, decls: 19, vars: 38, consts: [[1, "entity-viewer-container"], [1, "viewer-header"], [1, "viewer-content"], [1, "loading-container", 3, "hidden"], ["size", "medium", 3, "text"], [1, "loading-overlay", 3, "hidden"], ["size", "small", 3, "text"], [1, "empty-state", 3, "hidden"], [1, "fa-solid", "fa-database"], [1, "fa-solid", "fa-inbox"], [3, "AfterRowClick", "AfterRowDoubleClick", "AfterSort", "GridStateChanged", "SelectionChange", "NewButtonClick", "RefreshButtonClick", "DeleteButtonClick", "ExportButtonClick", "AddToListRequested", "hidden", "Data", "Params", "FilterText", "GridState", "Height", "ShowToolbar", "ToolbarConfig", "SelectionMode", "ShowAddToListButton", "AllowLoad"], [3, "recordSelected", "recordOpened", "hidden", "entity", "records", "selectedRecordId", "cardTemplate", "hiddenFieldMatches", "filterText"], [3, "afterEventClick", "hidden", "groups", "orientation", "layout", "sortOrder", "segmentGrouping", "segmentsCollapsible", "segmentsDefaultExpanded", "selectedEventId"], [3, "pagination", "loadedRecordCount"], [1, "filter-container"], [1, "record-count"], [1, "view-mode-toggle"], [1, "fa-solid", "fa-search", "filter-icon"], ["type", "text", 1, "filter-input", 3, "input", "placeholder", "value"], ["title", "Clear filter", 1, "clear-filter-btn"], ["title", "Clear filter", 1, "clear-filter-btn", 3, "click"], [1, "fa-solid", "fa-times"], ["title", "Grid View", 1, "toggle-btn", 3, "click"], [1, "fa-solid", "fa-list"], ["title", "Cards View", 1, "toggle-btn", 3, "click"], [1, "fa-solid", "fa-grip"], ["title", "Timeline View", 1, "toggle-btn", 3, "active"], ["title", "Timeline View", 1, "toggle-btn", 3, "click"], [1, "fa-solid", "fa-timeline"], [1, "timeline-date-selector"], [1, "fa-solid", "fa-calendar-days"], [1, "date-field-label"], [1, "date-field-select", 3, "value"], [1, "timeline-orientation-toggle"], [1, "toggle-btn", 3, "click", "title"], [1, "timeline-sort-toggle"], [1, "date-field-select", 3, "change", "value"], [3, "value"], [3, "loadMore", "pagination", "loadedRecordCount"]], template: function EntityViewerComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1344
1516
|
i0.ɵɵelementStart(0, "div", 0);
|
|
1345
1517
|
i0.ɵɵtemplate(1, EntityViewerComponent_Conditional_1_Template, 5, 4, "div", 1);
|
|
1346
1518
|
i0.ɵɵelementStart(2, "div", 2)(3, "div", 3);
|
|
@@ -1360,7 +1532,7 @@ export class EntityViewerComponent {
|
|
|
1360
1532
|
i0.ɵɵtext(14);
|
|
1361
1533
|
i0.ɵɵelementEnd()();
|
|
1362
1534
|
i0.ɵɵelementStart(15, "mj-entity-data-grid", 10);
|
|
1363
|
-
i0.ɵɵlistener("AfterRowClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowClick_15_listener($event) { return ctx.onDataGridRowClick($event); })("AfterRowDoubleClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowDoubleClick_15_listener($event) { return ctx.onDataGridRowDoubleClick($event); })("AfterSort", function EntityViewerComponent_Template_mj_entity_data_grid_AfterSort_15_listener($event) { return ctx.onDataGridSortChanged($event); })("GridStateChanged", function EntityViewerComponent_Template_mj_entity_data_grid_GridStateChanged_15_listener($event) { return ctx.onGridStateChanged($event); })("NewButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_NewButtonClick_15_listener() { return ctx.onGridAddRequested(); })("RefreshButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_RefreshButtonClick_15_listener() { return ctx.onGridRefreshRequested(); })("DeleteButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_DeleteButtonClick_15_listener($event) { return ctx.onGridDeleteRequested($event); })("ExportButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_ExportButtonClick_15_listener() { return ctx.onGridExportRequested(); });
|
|
1535
|
+
i0.ɵɵlistener("AfterRowClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowClick_15_listener($event) { return ctx.onDataGridRowClick($event); })("AfterRowDoubleClick", function EntityViewerComponent_Template_mj_entity_data_grid_AfterRowDoubleClick_15_listener($event) { return ctx.onDataGridRowDoubleClick($event); })("AfterSort", function EntityViewerComponent_Template_mj_entity_data_grid_AfterSort_15_listener($event) { return ctx.onDataGridSortChanged($event); })("GridStateChanged", function EntityViewerComponent_Template_mj_entity_data_grid_GridStateChanged_15_listener($event) { return ctx.onGridStateChanged($event); })("SelectionChange", function EntityViewerComponent_Template_mj_entity_data_grid_SelectionChange_15_listener($event) { return ctx.onGridSelectionChange($event); })("NewButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_NewButtonClick_15_listener() { return ctx.onGridAddRequested(); })("RefreshButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_RefreshButtonClick_15_listener() { return ctx.onGridRefreshRequested(); })("DeleteButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_DeleteButtonClick_15_listener($event) { return ctx.onGridDeleteRequested($event); })("ExportButtonClick", function EntityViewerComponent_Template_mj_entity_data_grid_ExportButtonClick_15_listener() { return ctx.onGridExportRequested(); })("AddToListRequested", function EntityViewerComponent_Template_mj_entity_data_grid_AddToListRequested_15_listener($event) { return ctx.onGridAddToListRequested($event); });
|
|
1364
1536
|
i0.ɵɵelementEnd();
|
|
1365
1537
|
i0.ɵɵelementStart(16, "mj-entity-cards", 11);
|
|
1366
1538
|
i0.ɵɵlistener("recordSelected", function EntityViewerComponent_Template_mj_entity_cards_recordSelected_16_listener($event) { return ctx.onRecordSelected($event); })("recordOpened", function EntityViewerComponent_Template_mj_entity_cards_recordOpened_16_listener($event) { return ctx.onRecordOpened($event); });
|
|
@@ -1383,26 +1555,26 @@ export class EntityViewerComponent {
|
|
|
1383
1555
|
i0.ɵɵadvance();
|
|
1384
1556
|
i0.ɵɵproperty("text", ctx.loadingMessage);
|
|
1385
1557
|
i0.ɵɵadvance();
|
|
1386
|
-
i0.ɵɵproperty("hidden", !!ctx.
|
|
1558
|
+
i0.ɵɵproperty("hidden", !!ctx.effectiveEntity);
|
|
1387
1559
|
i0.ɵɵadvance(4);
|
|
1388
|
-
i0.ɵɵproperty("hidden", !ctx.
|
|
1560
|
+
i0.ɵɵproperty("hidden", !ctx.effectiveEntity || ctx.filteredRecords.length > 0 || ctx.isLoading);
|
|
1389
1561
|
i0.ɵɵadvance(3);
|
|
1390
1562
|
i0.ɵɵtextInterpolate(ctx.debouncedFilterText ? "No matching records" : "No records found");
|
|
1391
1563
|
i0.ɵɵadvance();
|
|
1392
|
-
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "grid" || !ctx.
|
|
1564
|
+
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "grid" || !ctx.effectiveEntity)("Data", ctx.filteredRecords)("Params", ctx.gridParams)("FilterText", ctx.debouncedFilterText)("GridState", ctx.gridState)("Height", "auto")("ShowToolbar", ctx.showGridToolbar)("ToolbarConfig", ctx.effectiveGridToolbarConfig)("SelectionMode", ctx.gridSelectionMode)("ShowAddToListButton", ctx.showAddToListButton)("AllowLoad", false);
|
|
1393
1565
|
i0.ɵɵadvance();
|
|
1394
|
-
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "cards" || !ctx.
|
|
1566
|
+
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "cards" || !ctx.effectiveEntity)("entity", ctx.effectiveEntity)("records", ctx.filteredRecords)("selectedRecordId", ctx.selectedRecordId)("cardTemplate", ctx.cardTemplate)("hiddenFieldMatches", ctx.hiddenFieldMatches)("filterText", ctx.debouncedFilterText);
|
|
1395
1567
|
i0.ɵɵadvance();
|
|
1396
1568
|
i0.ɵɵproperty("hidden", ctx.effectiveViewMode !== "timeline" || !ctx.hasDateFields)("groups", ctx.timelineGroups)("orientation", ctx.timelineOrientation)("layout", ctx.timelineOrientation === "vertical" ? "alternating" : "single")("sortOrder", ctx.timelineSortOrder)("segmentGrouping", ctx.timelineSegmentGrouping)("segmentsCollapsible", true)("segmentsDefaultExpanded", true)("selectedEventId", ctx.timelineSelectedEventId);
|
|
1397
1569
|
i0.ɵɵadvance();
|
|
1398
|
-
i0.ɵɵconditional(ctx.effectiveConfig.showPagination && ctx.
|
|
1399
|
-
} }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i2.LoadingComponent, i3.TimelineComponent, i4.EntityCardsComponent, i5.PaginationComponent, i6.EntityDataGridComponent, i7.DecimalPipe], styles: [".entity-viewer-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: #fafafa;\n}\n\n\n\nmj-pagination[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n\n\n\n.viewer-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: white;\n border-bottom: 1px solid #e0e0e0;\n flex-shrink: 0;\n}\n\n\n\n.filter-container[_ngcontent-%COMP%] {\n flex: 1;\n max-width: 400px;\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.filter-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 12px;\n color: #9e9e9e;\n font-size: 14px;\n pointer-events: none;\n}\n\n.filter-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 8px 36px 8px 36px;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.filter-input[_ngcontent-%COMP%]:focus {\n border-color: #1976d2;\n}\n\n.filter-input[_ngcontent-%COMP%]::placeholder {\n color: #9e9e9e;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%] {\n position: absolute;\n right: 8px;\n width: 20px;\n height: 20px;\n border: none;\n background: transparent;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #9e9e9e;\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%]:hover {\n background: #f5f5f5;\n color: #616161;\n}\n\n\n\n.record-count[_ngcontent-%COMP%] {\n font-size: 13px;\n color: #757575;\n white-space: nowrap;\n}\n\n\n\n.view-mode-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n.toggle-btn[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #757575;\n transition: all 0.15s ease;\n}\n\n.toggle-btn[_ngcontent-%COMP%]:hover {\n color: #424242;\n}\n\n.toggle-btn.active[_ngcontent-%COMP%] {\n background: white;\n color: #1976d2;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n\n\n.timeline-date-selector[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #9e9e9e;\n}\n\n.date-field-label[_ngcontent-%COMP%] {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select[_ngcontent-%COMP%] {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select[_ngcontent-%COMP%]:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select[_ngcontent-%COMP%]:focus {\n border-color: #1976d2;\n}\n\n\n\n.timeline-orientation-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n\n\n.viewer-content[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\n}\n\n\n\n.loading-container[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n\n\n.loading-overlay[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n\n\n.empty-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #9e9e9e;\n text-align: center;\n}\n\n.empty-state[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 14px;\n}\n\n\n\nmj-entity-cards[hidden][_ngcontent-%COMP%], \nmj-timeline[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}\n\n\n\nmj-entity-cards[_ngcontent-%COMP%]:not([hidden]), \nmj-timeline[_ngcontent-%COMP%]:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}"] });
|
|
1570
|
+
i0.ɵɵconditional(ctx.effectiveConfig.showPagination && ctx.effectiveEntity && (ctx.pagination.hasMore || ctx.pagination.totalRecords > ctx.effectiveConfig.pageSize) ? 18 : -1);
|
|
1571
|
+
} }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i2.LoadingComponent, i3.TimelineComponent, i4.EntityCardsComponent, i5.PaginationComponent, i6.EntityDataGridComponent, i7.DecimalPipe], styles: [".entity-viewer-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: #fafafa;\n}\n\n\n\nmj-pagination[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n\n\n\n.viewer-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: white;\n border-bottom: 1px solid #e0e0e0;\n flex-shrink: 0;\n}\n\n\n\n.filter-container[_ngcontent-%COMP%] {\n flex: 1;\n max-width: 400px;\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.filter-icon[_ngcontent-%COMP%] {\n position: absolute;\n left: 12px;\n color: #9e9e9e;\n font-size: 14px;\n pointer-events: none;\n}\n\n.filter-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 8px 36px 8px 36px;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.filter-input[_ngcontent-%COMP%]:focus {\n border-color: #1976d2;\n}\n\n.filter-input[_ngcontent-%COMP%]::placeholder {\n color: #9e9e9e;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%] {\n position: absolute;\n right: 8px;\n width: 20px;\n height: 20px;\n border: none;\n background: transparent;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #9e9e9e;\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn[_ngcontent-%COMP%]:hover {\n background: #f5f5f5;\n color: #616161;\n}\n\n\n\n.record-count[_ngcontent-%COMP%] {\n font-size: 13px;\n color: #757575;\n white-space: nowrap;\n}\n\n\n\n.view-mode-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n.toggle-btn[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #757575;\n transition: all 0.15s ease;\n}\n\n.toggle-btn[_ngcontent-%COMP%]:hover {\n color: #424242;\n}\n\n.toggle-btn.active[_ngcontent-%COMP%] {\n background: white;\n color: #1976d2;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n\n\n.timeline-date-selector[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #9e9e9e;\n}\n\n.date-field-label[_ngcontent-%COMP%] {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select[_ngcontent-%COMP%] {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select[_ngcontent-%COMP%]:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select[_ngcontent-%COMP%]:focus {\n border-color: #1976d2;\n}\n\n\n\n.timeline-orientation-toggle[_ngcontent-%COMP%] {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n\n\n.viewer-content[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\n}\n\n\n\n.loading-container[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n\n\n.loading-overlay[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n\n\n.loading-container[hidden][_ngcontent-%COMP%], \n.loading-overlay[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}\n\n\n\n.empty-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #9e9e9e;\n text-align: center;\n}\n\n.empty-state[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 14px;\n}\n\n\n\nmj-entity-cards[hidden][_ngcontent-%COMP%], \nmj-timeline[hidden][_ngcontent-%COMP%] {\n display: none !important;\n}\n\n\n\nmj-entity-cards[_ngcontent-%COMP%]:not([hidden]), \nmj-timeline[_ngcontent-%COMP%]:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}"] });
|
|
1400
1572
|
}
|
|
1401
1573
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(EntityViewerComponent, [{
|
|
1402
1574
|
type: Component,
|
|
1403
1575
|
args: [{ selector: 'mj-entity-viewer', host: {
|
|
1404
1576
|
'style': 'display: block; height: 100%;'
|
|
1405
|
-
}, template: "<div class=\"entity-viewer-container\" [style.height]=\"effectiveConfig.height\">\n <!-- Header -->\n @if (effectiveConfig.showFilter || effectiveConfig.showViewModeToggle || effectiveConfig.showRecordCount) {\n <div class=\"viewer-header\">\n <!-- Filter Input -->\n @if (effectiveConfig.showFilter) {\n <div class=\"filter-container\">\n <i class=\"fa-solid fa-search filter-icon\"></i>\n <input\n type=\"text\"\n class=\"filter-input\"\n [placeholder]=\"effectiveConfig.filterPlaceholder\"\n [value]=\"effectiveFilterText\"\n (input)=\"onFilterChange($any($event.target).value)\"\n />\n @if (effectiveFilterText) {\n <button class=\"clear-filter-btn\" (click)=\"clearFilter()\" title=\"Clear filter\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n }\n </div>\n }\n\n <!-- Record Count -->\n @if (effectiveConfig.showRecordCount && entity) {\n <div class=\"record-count\">\n @if (filteredRecordCount !== totalRecordCount) {\n <span>{{ filteredRecordCount | number }} of {{ totalRecordCount | number }} records</span>\n } @else {\n <span>{{ totalRecordCount | number }} records</span>\n }\n </div>\n }\n\n <!-- View Mode Toggle -->\n @if (effectiveConfig.showViewModeToggle) {\n <div class=\"view-mode-toggle\">\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'grid'\"\n (click)=\"setViewMode('grid')\"\n title=\"Grid View\">\n <i class=\"fa-solid fa-list\"></i>\n </button>\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'cards'\"\n (click)=\"setViewMode('cards')\"\n title=\"Cards View\">\n <i class=\"fa-solid fa-grip\"></i>\n </button>\n @if (hasDateFields) {\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'timeline'\"\n (click)=\"setViewMode('timeline')\"\n title=\"Timeline View\">\n <i class=\"fa-solid fa-timeline\"></i>\n </button>\n }\n </div>\n }\n\n <!-- Timeline Controls (only shown when timeline is active) -->\n @if (effectiveViewMode === 'timeline' && hasDateFields) {\n <!-- Date Field Selector -->\n <div class=\"timeline-date-selector\">\n <i class=\"fa-solid fa-calendar-days\"></i>\n @if (availableDateFields.length === 1) {\n <span class=\"date-field-label\">{{ selectedDateFieldDisplayName }}</span>\n } @else {\n <select\n class=\"date-field-select\"\n [value]=\"selectedTimelineDateField\"\n (change)=\"setTimelineDateField($any($event.target).value)\">\n @for (field of availableDateFields; track field.Name) {\n <option [value]=\"field.Name\">{{ field.DisplayNameOrName }}</option>\n }\n </select>\n }\n </div>\n\n <!-- Orientation Toggle -->\n <div class=\"timeline-orientation-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineOrientation()\"\n [title]=\"timelineOrientation === 'vertical' ? 'Switch to Horizontal' : 'Switch to Vertical'\">\n <i [class]=\"timelineOrientation === 'vertical' ? 'fa-solid fa-ellipsis-vertical' : 'fa-solid fa-ellipsis'\"></i>\n </button>\n </div>\n\n <!-- Sort Order Toggle -->\n <div class=\"timeline-sort-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineSortOrder()\"\n [title]=\"timelineSortOrder === 'desc' ? 'Showing Newest First (click for Oldest First)' : 'Showing Oldest First (click for Newest First)'\">\n <i [class]=\"timelineSortOrder === 'desc' ? 'fa-solid fa-arrow-down-wide-short' : 'fa-solid fa-arrow-up-wide-short'\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <!-- Content -->\n <div class=\"viewer-content\">\n <!-- Loading container - full page when no data exists -->\n <div class=\"loading-container\" [hidden]=\"!(isLoading && filteredRecords.length === 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"medium\"></mj-loading>\n </div>\n\n <!-- Loading overlay - shown on top of content when loading with existing data -->\n <div class=\"loading-overlay\" [hidden]=\"!(isLoading && filteredRecords.length > 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"small\"></mj-loading>\n </div>\n\n <!-- Empty state: no entity selected -->\n <div class=\"empty-state\" [hidden]=\"!!entity\">\n <i class=\"fa-solid fa-database\"></i>\n <p>Select an entity to view records</p>\n </div>\n\n <!-- Empty state: no records found -->\n <div class=\"empty-state\" [hidden]=\"!entity || filteredRecords.length > 0 || isLoading\">\n <i class=\"fa-solid fa-inbox\"></i>\n <p>{{ debouncedFilterText ? 'No matching records' : 'No records found' }}</p>\n </div>\n\n <!-- Grid View - always rendered, visibility controlled by hidden -->\n <mj-entity-data-grid\n [hidden]=\"effectiveViewMode !== 'grid' || !entity\"\n [Data]=\"filteredRecords\"\n [Params]=\"gridParams\"\n [FilterText]=\"debouncedFilterText\"\n [GridState]=\"gridState\"\n [Height]=\"'auto'\"\n [ShowToolbar]=\"showGridToolbar\"\n [ToolbarConfig]=\"effectiveGridToolbarConfig\"\n [SelectionMode]=\"gridSelectionMode\"\n [AllowLoad]=\"false\"\n (AfterRowClick)=\"onDataGridRowClick($event)\"\n (AfterRowDoubleClick)=\"onDataGridRowDoubleClick($event)\"\n (AfterSort)=\"onDataGridSortChanged($event)\"\n (GridStateChanged)=\"onGridStateChanged($event)\"\n (NewButtonClick)=\"onGridAddRequested()\"\n (RefreshButtonClick)=\"onGridRefreshRequested()\"\n (DeleteButtonClick)=\"onGridDeleteRequested($event)\"\n (ExportButtonClick)=\"onGridExportRequested()\">\n </mj-entity-data-grid>\n\n <!-- Cards View - always rendered, visibility controlled by hidden -->\n <mj-entity-cards\n [hidden]=\"effectiveViewMode !== 'cards' || !entity\"\n [entity]=\"entity\"\n [records]=\"filteredRecords\"\n [selectedRecordId]=\"selectedRecordId\"\n [cardTemplate]=\"cardTemplate\"\n [hiddenFieldMatches]=\"hiddenFieldMatches\"\n [filterText]=\"debouncedFilterText\"\n (recordSelected)=\"onRecordSelected($event)\"\n (recordOpened)=\"onRecordOpened($event)\">\n </mj-entity-cards>\n\n <!-- Timeline View - always rendered when date fields exist, visibility controlled by hidden -->\n <mj-timeline\n [hidden]=\"effectiveViewMode !== 'timeline' || !hasDateFields\"\n [groups]=\"timelineGroups\"\n [orientation]=\"timelineOrientation\"\n [layout]=\"timelineOrientation === 'vertical' ? 'alternating' : 'single'\"\n [sortOrder]=\"timelineSortOrder\"\n [segmentGrouping]=\"timelineSegmentGrouping\"\n [segmentsCollapsible]=\"true\"\n [segmentsDefaultExpanded]=\"true\"\n [selectedEventId]=\"timelineSelectedEventId\"\n (afterEventClick)=\"onTimelineEventClick($event)\">\n </mj-timeline>\n </div>\n\n <!-- Pagination - only show when there's more data to load OR we've loaded more than one page -->\n @if (effectiveConfig.showPagination && entity && (pagination.hasMore || pagination.totalRecords > effectiveConfig.pageSize)) {\n <mj-pagination\n [pagination]=\"pagination\"\n [loadedRecordCount]=\"filteredRecords.length\"\n (loadMore)=\"onLoadMore()\">\n </mj-pagination>\n }\n</div>\n", styles: [".entity-viewer-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: #fafafa;\n}\n\n/* Pagination footer - always visible at bottom */\nmj-pagination {\n flex-shrink: 0;\n}\n\n/* Header */\n.viewer-header {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: white;\n border-bottom: 1px solid #e0e0e0;\n flex-shrink: 0;\n}\n\n/* Filter */\n.filter-container {\n flex: 1;\n max-width: 400px;\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.filter-icon {\n position: absolute;\n left: 12px;\n color: #9e9e9e;\n font-size: 14px;\n pointer-events: none;\n}\n\n.filter-input {\n width: 100%;\n padding: 8px 36px 8px 36px;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.filter-input:focus {\n border-color: #1976d2;\n}\n\n.filter-input::placeholder {\n color: #9e9e9e;\n}\n\n.clear-filter-btn {\n position: absolute;\n right: 8px;\n width: 20px;\n height: 20px;\n border: none;\n background: transparent;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #9e9e9e;\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn:hover {\n background: #f5f5f5;\n color: #616161;\n}\n\n/* Record Count */\n.record-count {\n font-size: 13px;\n color: #757575;\n white-space: nowrap;\n}\n\n/* View Mode Toggle */\n.view-mode-toggle {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n.toggle-btn {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #757575;\n transition: all 0.15s ease;\n}\n\n.toggle-btn:hover {\n color: #424242;\n}\n\n.toggle-btn.active {\n background: white;\n color: #1976d2;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n/* Timeline Date Field Selector */\n.timeline-date-selector {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector i {\n color: #9e9e9e;\n}\n\n.date-field-label {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select:focus {\n border-color: #1976d2;\n}\n\n/* Timeline Orientation Toggle */\n.timeline-orientation-toggle {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n/* Content */\n.viewer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\n}\n\n/* Loading State - positioned as overlay when data exists */\n.loading-container {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n/* Loading overlay when shown on top of content */\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Empty State */\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #9e9e9e;\n text-align: center;\n}\n\n.empty-state i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state p {\n margin: 0;\n font-size: 14px;\n}\n\n/* Hidden components should not display - ensure [hidden] attribute works properly */\nmj-entity-cards[hidden],\nmj-timeline[hidden] {\n display: none !important;\n}\n\n/* Visible view components should fill available space */\nmj-entity-cards:not([hidden]),\nmj-timeline:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}\n"] }]
|
|
1577
|
+
}, template: "<div class=\"entity-viewer-container\" [style.height]=\"effectiveConfig.height\">\n <!-- Header -->\n @if (effectiveConfig.showFilter || effectiveConfig.showViewModeToggle || effectiveConfig.showRecordCount) {\n <div class=\"viewer-header\">\n <!-- Filter Input -->\n @if (effectiveConfig.showFilter) {\n <div class=\"filter-container\">\n <i class=\"fa-solid fa-search filter-icon\"></i>\n <input\n type=\"text\"\n class=\"filter-input\"\n [placeholder]=\"effectiveConfig.filterPlaceholder\"\n [value]=\"effectiveFilterText\"\n (input)=\"onFilterChange($any($event.target).value)\"\n />\n @if (effectiveFilterText) {\n <button class=\"clear-filter-btn\" (click)=\"clearFilter()\" title=\"Clear filter\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n }\n </div>\n }\n\n <!-- Record Count -->\n @if (effectiveConfig.showRecordCount && effectiveEntity) {\n <div class=\"record-count\">\n @if (filteredRecordCount !== totalRecordCount) {\n <span>{{ filteredRecordCount | number }} of {{ totalRecordCount | number }} records</span>\n } @else {\n <span>{{ totalRecordCount | number }} records</span>\n }\n </div>\n }\n\n <!-- View Mode Toggle -->\n @if (effectiveConfig.showViewModeToggle) {\n <div class=\"view-mode-toggle\">\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'grid'\"\n (click)=\"setViewMode('grid')\"\n title=\"Grid View\">\n <i class=\"fa-solid fa-list\"></i>\n </button>\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'cards'\"\n (click)=\"setViewMode('cards')\"\n title=\"Cards View\">\n <i class=\"fa-solid fa-grip\"></i>\n </button>\n @if (hasDateFields) {\n <button\n class=\"toggle-btn\"\n [class.active]=\"effectiveViewMode === 'timeline'\"\n (click)=\"setViewMode('timeline')\"\n title=\"Timeline View\">\n <i class=\"fa-solid fa-timeline\"></i>\n </button>\n }\n </div>\n }\n\n <!-- Timeline Controls (only shown when timeline is active) -->\n @if (effectiveViewMode === 'timeline' && hasDateFields) {\n <!-- Date Field Selector -->\n <div class=\"timeline-date-selector\">\n <i class=\"fa-solid fa-calendar-days\"></i>\n @if (availableDateFields.length === 1) {\n <span class=\"date-field-label\">{{ selectedDateFieldDisplayName }}</span>\n } @else {\n <select\n class=\"date-field-select\"\n [value]=\"selectedTimelineDateField\"\n (change)=\"setTimelineDateField($any($event.target).value)\">\n @for (field of availableDateFields; track field.Name) {\n <option [value]=\"field.Name\">{{ field.DisplayNameOrName }}</option>\n }\n </select>\n }\n </div>\n\n <!-- Orientation Toggle -->\n <div class=\"timeline-orientation-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineOrientation()\"\n [title]=\"timelineOrientation === 'vertical' ? 'Switch to Horizontal' : 'Switch to Vertical'\">\n <i [class]=\"timelineOrientation === 'vertical' ? 'fa-solid fa-ellipsis-vertical' : 'fa-solid fa-ellipsis'\"></i>\n </button>\n </div>\n\n <!-- Sort Order Toggle -->\n <div class=\"timeline-sort-toggle\">\n <button\n class=\"toggle-btn\"\n (click)=\"toggleTimelineSortOrder()\"\n [title]=\"timelineSortOrder === 'desc' ? 'Showing Newest First (click for Oldest First)' : 'Showing Oldest First (click for Newest First)'\">\n <i [class]=\"timelineSortOrder === 'desc' ? 'fa-solid fa-arrow-down-wide-short' : 'fa-solid fa-arrow-up-wide-short'\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <!-- Content -->\n <div class=\"viewer-content\">\n <!-- Loading container - full page when no data exists -->\n <div class=\"loading-container\" [hidden]=\"!(isLoading && filteredRecords.length === 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"medium\"></mj-loading>\n </div>\n\n <!-- Loading overlay - shown on top of content when loading with existing data -->\n <div class=\"loading-overlay\" [hidden]=\"!(isLoading && filteredRecords.length > 0)\">\n <mj-loading [text]=\"loadingMessage\" size=\"small\"></mj-loading>\n </div>\n\n <!-- Empty state: no entity selected -->\n <div class=\"empty-state\" [hidden]=\"!!effectiveEntity\">\n <i class=\"fa-solid fa-database\"></i>\n <p>Select an entity to view records</p>\n </div>\n\n <!-- Empty state: no records found -->\n <div class=\"empty-state\" [hidden]=\"!effectiveEntity || filteredRecords.length > 0 || isLoading\">\n <i class=\"fa-solid fa-inbox\"></i>\n <p>{{ debouncedFilterText ? 'No matching records' : 'No records found' }}</p>\n </div>\n\n <!-- Grid View - always rendered, visibility controlled by hidden -->\n <mj-entity-data-grid\n [hidden]=\"effectiveViewMode !== 'grid' || !effectiveEntity\"\n [Data]=\"filteredRecords\"\n [Params]=\"gridParams\"\n [FilterText]=\"debouncedFilterText\"\n [GridState]=\"gridState\"\n [Height]=\"'auto'\"\n [ShowToolbar]=\"showGridToolbar\"\n [ToolbarConfig]=\"effectiveGridToolbarConfig\"\n [SelectionMode]=\"gridSelectionMode\"\n [ShowAddToListButton]=\"showAddToListButton\"\n [AllowLoad]=\"false\"\n (AfterRowClick)=\"onDataGridRowClick($event)\"\n (AfterRowDoubleClick)=\"onDataGridRowDoubleClick($event)\"\n (AfterSort)=\"onDataGridSortChanged($event)\"\n (GridStateChanged)=\"onGridStateChanged($event)\"\n (SelectionChange)=\"onGridSelectionChange($event)\"\n (NewButtonClick)=\"onGridAddRequested()\"\n (RefreshButtonClick)=\"onGridRefreshRequested()\"\n (DeleteButtonClick)=\"onGridDeleteRequested($event)\"\n (ExportButtonClick)=\"onGridExportRequested()\"\n (AddToListRequested)=\"onGridAddToListRequested($event)\">\n </mj-entity-data-grid>\n\n <!-- Cards View - always rendered, visibility controlled by hidden -->\n <mj-entity-cards\n [hidden]=\"effectiveViewMode !== 'cards' || !effectiveEntity\"\n [entity]=\"effectiveEntity\"\n [records]=\"filteredRecords\"\n [selectedRecordId]=\"selectedRecordId\"\n [cardTemplate]=\"cardTemplate\"\n [hiddenFieldMatches]=\"hiddenFieldMatches\"\n [filterText]=\"debouncedFilterText\"\n (recordSelected)=\"onRecordSelected($event)\"\n (recordOpened)=\"onRecordOpened($event)\">\n </mj-entity-cards>\n\n <!-- Timeline View - always rendered when date fields exist, visibility controlled by hidden -->\n <mj-timeline\n [hidden]=\"effectiveViewMode !== 'timeline' || !hasDateFields\"\n [groups]=\"timelineGroups\"\n [orientation]=\"timelineOrientation\"\n [layout]=\"timelineOrientation === 'vertical' ? 'alternating' : 'single'\"\n [sortOrder]=\"timelineSortOrder\"\n [segmentGrouping]=\"timelineSegmentGrouping\"\n [segmentsCollapsible]=\"true\"\n [segmentsDefaultExpanded]=\"true\"\n [selectedEventId]=\"timelineSelectedEventId\"\n (afterEventClick)=\"onTimelineEventClick($event)\">\n </mj-timeline>\n </div>\n\n <!-- Pagination - only show when there's more data to load OR we've loaded more than one page -->\n @if (effectiveConfig.showPagination && effectiveEntity && (pagination.hasMore || pagination.totalRecords > effectiveConfig.pageSize)) {\n <mj-pagination\n [pagination]=\"pagination\"\n [loadedRecordCount]=\"filteredRecords.length\"\n (loadMore)=\"onLoadMore()\">\n </mj-pagination>\n }\n</div>\n", styles: [".entity-viewer-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n background: #fafafa;\n}\n\n/* Pagination footer - always visible at bottom */\nmj-pagination {\n flex-shrink: 0;\n}\n\n/* Header */\n.viewer-header {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n background: white;\n border-bottom: 1px solid #e0e0e0;\n flex-shrink: 0;\n}\n\n/* Filter */\n.filter-container {\n flex: 1;\n max-width: 400px;\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.filter-icon {\n position: absolute;\n left: 12px;\n color: #9e9e9e;\n font-size: 14px;\n pointer-events: none;\n}\n\n.filter-input {\n width: 100%;\n padding: 8px 36px 8px 36px;\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.filter-input:focus {\n border-color: #1976d2;\n}\n\n.filter-input::placeholder {\n color: #9e9e9e;\n}\n\n.clear-filter-btn {\n position: absolute;\n right: 8px;\n width: 20px;\n height: 20px;\n border: none;\n background: transparent;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #9e9e9e;\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn:hover {\n background: #f5f5f5;\n color: #616161;\n}\n\n/* Record Count */\n.record-count {\n font-size: 13px;\n color: #757575;\n white-space: nowrap;\n}\n\n/* View Mode Toggle */\n.view-mode-toggle {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n.toggle-btn {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #757575;\n transition: all 0.15s ease;\n}\n\n.toggle-btn:hover {\n color: #424242;\n}\n\n.toggle-btn.active {\n background: white;\n color: #1976d2;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n/* Timeline Date Field Selector */\n.timeline-date-selector {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: #616161;\n}\n\n.timeline-date-selector i {\n color: #9e9e9e;\n}\n\n.date-field-label {\n color: #424242;\n font-weight: 500;\n}\n\n.date-field-select {\n padding: 4px 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n background: white;\n color: #424242;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.date-field-select:hover {\n border-color: #bdbdbd;\n}\n\n.date-field-select:focus {\n border-color: #1976d2;\n}\n\n/* Timeline Orientation Toggle */\n.timeline-orientation-toggle {\n display: flex;\n background: #f5f5f5;\n border-radius: 6px;\n padding: 2px;\n}\n\n/* Content */\n.viewer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n position: relative;\n background: white;\n}\n\n/* Loading State - full-page centered loading for initial load when no data exists */\n.loading-container {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n}\n\n/* Loading overlay - semi-transparent overlay on top of existing content during refresh */\n.loading-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.8);\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Ensure [hidden] attribute works properly on loading elements */\n.loading-container[hidden],\n.loading-overlay[hidden] {\n display: none !important;\n}\n\n/* Empty State */\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #9e9e9e;\n text-align: center;\n}\n\n.empty-state i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n.empty-state p {\n margin: 0;\n font-size: 14px;\n}\n\n/* Hidden components should not display - ensure [hidden] attribute works properly */\nmj-entity-cards[hidden],\nmj-timeline[hidden] {\n display: none !important;\n}\n\n/* Visible view components should fill available space */\nmj-entity-cards:not([hidden]),\nmj-timeline:not([hidden]) {\n display: block;\n height: 100%;\n width: 100%;\n}\n"] }]
|
|
1406
1578
|
}], () => [{ type: i0.ChangeDetectorRef }], { entity: [{
|
|
1407
1579
|
type: Input
|
|
1408
1580
|
}], records: [{
|
|
@@ -1433,6 +1605,8 @@ export class EntityViewerComponent {
|
|
|
1433
1605
|
type: Input
|
|
1434
1606
|
}], gridSelectionMode: [{
|
|
1435
1607
|
type: Input
|
|
1608
|
+
}], showAddToListButton: [{
|
|
1609
|
+
type: Input
|
|
1436
1610
|
}], recordSelected: [{
|
|
1437
1611
|
type: Output
|
|
1438
1612
|
}], recordOpened: [{
|
|
@@ -1459,6 +1633,13 @@ export class EntityViewerComponent {
|
|
|
1459
1633
|
type: Output
|
|
1460
1634
|
}], exportRequested: [{
|
|
1461
1635
|
type: Output
|
|
1636
|
+
}], addToListRequested: [{
|
|
1637
|
+
type: Output
|
|
1638
|
+
}], selectionChanged: [{
|
|
1639
|
+
type: Output
|
|
1640
|
+
}], dataGridRef: [{
|
|
1641
|
+
type: ViewChild,
|
|
1642
|
+
args: [EntityDataGridComponent]
|
|
1462
1643
|
}] }); })();
|
|
1463
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber:
|
|
1644
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(EntityViewerComponent, { className: "EntityViewerComponent", filePath: "src/lib/entity-viewer/entity-viewer.component.ts", lineNumber: 76 }); })();
|
|
1464
1645
|
//# sourceMappingURL=entity-viewer.component.js.map
|