osl-base-extended 1.1.57 → 1.1.58
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.
|
@@ -2603,6 +2603,8 @@ class OslSetup {
|
|
|
2603
2603
|
formFooterTpl;
|
|
2604
2604
|
customFooterWrapperTpl;
|
|
2605
2605
|
searchbar;
|
|
2606
|
+
gridRef;
|
|
2607
|
+
cardContainerRef;
|
|
2606
2608
|
// ── Inputs ────────────────────────────────────────────────────
|
|
2607
2609
|
title = '';
|
|
2608
2610
|
columns = [];
|
|
@@ -2613,8 +2615,7 @@ class OslSetup {
|
|
|
2613
2615
|
tableHeight = 'calc(100vh - 220px)';
|
|
2614
2616
|
totalRecords = 0;
|
|
2615
2617
|
loading = false;
|
|
2616
|
-
dialogWidth =
|
|
2617
|
-
/** Dynamic form elements — when provided, enables Add/Edit dialog and action column. */
|
|
2618
|
+
dialogWidth = '50vw';
|
|
2618
2619
|
formElements = [];
|
|
2619
2620
|
beforeDisplay;
|
|
2620
2621
|
onAddEditFn;
|
|
@@ -2628,10 +2629,14 @@ class OslSetup {
|
|
|
2628
2629
|
partialCustomHeaderTemp;
|
|
2629
2630
|
stateKey = '';
|
|
2630
2631
|
primaryKey = 'id';
|
|
2632
|
+
onSave;
|
|
2633
|
+
/** Fixed page size used for card view infinite scroll. Defaults to pageSize. */
|
|
2634
|
+
cardPageSize;
|
|
2635
|
+
/** Optional custom card template. Context: { $implicit: row, index: number } */
|
|
2636
|
+
cardTemplate;
|
|
2631
2637
|
// ── Outputs ───────────────────────────────────────────────────
|
|
2632
2638
|
onSearch = new EventEmitter();
|
|
2633
2639
|
onAdd = new EventEmitter();
|
|
2634
|
-
// @Output() onSave = new EventEmitter<OslSetupSaveEvent>();
|
|
2635
2640
|
onEdit = new EventEmitter();
|
|
2636
2641
|
onDelete = new EventEmitter();
|
|
2637
2642
|
pageChange = new EventEmitter();
|
|
@@ -2639,15 +2644,187 @@ class OslSetup {
|
|
|
2639
2644
|
sortChange = new EventEmitter();
|
|
2640
2645
|
onRowClick = new EventEmitter();
|
|
2641
2646
|
onStateRestored = new EventEmitter();
|
|
2642
|
-
gridRef;
|
|
2643
|
-
onSave;
|
|
2644
2647
|
// ── Dialog state ──────────────────────────────────────────────
|
|
2645
2648
|
dialogModel = {};
|
|
2646
2649
|
dialogMode = 'add';
|
|
2647
2650
|
_dialogRef = null;
|
|
2651
|
+
// ── View mode ─────────────────────────────────────────────────
|
|
2652
|
+
viewMode = 'table';
|
|
2653
|
+
// ── Card view state ───────────────────────────────────────────
|
|
2654
|
+
cardDatasource = [];
|
|
2655
|
+
cardPage = 0;
|
|
2656
|
+
allCardsLoaded = false;
|
|
2657
|
+
cardOpenMenuIndex = null;
|
|
2658
|
+
cardMenuPosition = { top: 0, left: 0 };
|
|
2659
|
+
_cardExpectedPage = 0;
|
|
2660
|
+
_cardRestoreTargetPage = 0;
|
|
2661
|
+
_needsInitialCardLoad = false;
|
|
2662
|
+
onDocumentClick() {
|
|
2663
|
+
this.cardOpenMenuIndex = null;
|
|
2664
|
+
}
|
|
2648
2665
|
get hasForm() {
|
|
2649
2666
|
return this.formElements?.length > 0 || !!this.onAddEditFn;
|
|
2650
2667
|
}
|
|
2668
|
+
get _effectiveCardPageSize() {
|
|
2669
|
+
return this.cardPageSize ?? this.pageSize;
|
|
2670
|
+
}
|
|
2671
|
+
get _displayColumns() {
|
|
2672
|
+
return this.columns.filter(c => !c.isActions);
|
|
2673
|
+
}
|
|
2674
|
+
get cardTitleColumn() {
|
|
2675
|
+
return this._displayColumns[0];
|
|
2676
|
+
}
|
|
2677
|
+
get cardBodyColumns() {
|
|
2678
|
+
return this._displayColumns.slice(1);
|
|
2679
|
+
}
|
|
2680
|
+
get skeletonCardRows() {
|
|
2681
|
+
return Array.from({ length: 6 });
|
|
2682
|
+
}
|
|
2683
|
+
// ── Lifecycle ─────────────────────────────────────────────────
|
|
2684
|
+
ngOnInit() {
|
|
2685
|
+
this._loadViewMode();
|
|
2686
|
+
if (this.viewMode === 'card') {
|
|
2687
|
+
this._needsInitialCardLoad = true;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
ngAfterViewInit() {
|
|
2691
|
+
this.statemainTain();
|
|
2692
|
+
if (this._needsInitialCardLoad) {
|
|
2693
|
+
this._needsInitialCardLoad = false;
|
|
2694
|
+
setTimeout(() => { this._startCardLoad(); });
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
ngOnChanges(changes) {
|
|
2698
|
+
if (changes['datasource']) {
|
|
2699
|
+
if (this.viewMode === 'card' && this._cardExpectedPage !== 0) {
|
|
2700
|
+
const newData = changes['datasource'].currentValue ?? [];
|
|
2701
|
+
const isFirstPage = this._cardExpectedPage === 1;
|
|
2702
|
+
this._cardExpectedPage = 0;
|
|
2703
|
+
if (isFirstPage) {
|
|
2704
|
+
this.cardDatasource = [...newData];
|
|
2705
|
+
this.cardPage = 1;
|
|
2706
|
+
this.allCardsLoaded = false;
|
|
2707
|
+
}
|
|
2708
|
+
else if (newData.length > 0) {
|
|
2709
|
+
this.cardDatasource = [...this.cardDatasource, ...newData];
|
|
2710
|
+
this.cardPage++;
|
|
2711
|
+
}
|
|
2712
|
+
if (newData.length === 0 || (this.totalRecords > 0 && this.cardDatasource.length >= this.totalRecords)) {
|
|
2713
|
+
this.allCardsLoaded = true;
|
|
2714
|
+
}
|
|
2715
|
+
// Multi-page state restore: continue loading until target page is reached
|
|
2716
|
+
if (this._cardRestoreTargetPage > 0 && this.cardPage < this._cardRestoreTargetPage && !this.allCardsLoaded) {
|
|
2717
|
+
const nextPage = this.cardPage + 1;
|
|
2718
|
+
this._cardExpectedPage = nextPage;
|
|
2719
|
+
this.pageChange.emit({ page: nextPage, pageSize: this._effectiveCardPageSize, searchValue: this.searchbar?.searchControl?.value ?? '' });
|
|
2720
|
+
}
|
|
2721
|
+
else if (this._cardRestoreTargetPage > 0 && (this.cardPage >= this._cardRestoreTargetPage || this.allCardsLoaded)) {
|
|
2722
|
+
this._cardRestoreTargetPage = 0;
|
|
2723
|
+
if (this._pendingScrollTop !== null) {
|
|
2724
|
+
const top = this._pendingScrollTop;
|
|
2725
|
+
this._pendingScrollTop = null;
|
|
2726
|
+
setTimeout(() => { this.cardContainerRef?.nativeElement?.scrollTo({ top }); }, 50);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
else if (this.viewMode !== 'card' && this._pendingScrollTop !== null) {
|
|
2731
|
+
const ds = changes['datasource'].currentValue;
|
|
2732
|
+
if (ds?.length > 0) {
|
|
2733
|
+
const top = this._pendingScrollTop;
|
|
2734
|
+
this._pendingScrollTop = null;
|
|
2735
|
+
setTimeout(() => { this.gridRef?.scrollTo(top); }, 50);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
// ── View persistence ──────────────────────────────────────────
|
|
2741
|
+
_getViewKey() {
|
|
2742
|
+
return this.stateKey ? `osl-view-${this.stateKey}` : '';
|
|
2743
|
+
}
|
|
2744
|
+
_loadViewMode() {
|
|
2745
|
+
const key = this._getViewKey();
|
|
2746
|
+
if (!key)
|
|
2747
|
+
return;
|
|
2748
|
+
const saved = localStorage.getItem(key);
|
|
2749
|
+
if (saved === 'card' || saved === 'table') {
|
|
2750
|
+
this.viewMode = saved;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
toggleView(mode) {
|
|
2754
|
+
if (this.viewMode === mode)
|
|
2755
|
+
return;
|
|
2756
|
+
this.viewMode = mode;
|
|
2757
|
+
const key = this._getViewKey();
|
|
2758
|
+
if (key)
|
|
2759
|
+
localStorage.setItem(key, mode);
|
|
2760
|
+
if (mode === 'card') {
|
|
2761
|
+
this._startCardLoad();
|
|
2762
|
+
}
|
|
2763
|
+
else {
|
|
2764
|
+
this._cardExpectedPage = 0;
|
|
2765
|
+
if (this.gridRef)
|
|
2766
|
+
this.gridRef.currentPage = 1;
|
|
2767
|
+
this.pageChange.emit({ page: 1, pageSize: this.pageSize, searchValue: this.searchbar?.searchControl?.value ?? '' });
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
// ── Card view helpers ─────────────────────────────────────────
|
|
2771
|
+
_startCardLoad() {
|
|
2772
|
+
this.cardDatasource = [];
|
|
2773
|
+
this.cardPage = 0;
|
|
2774
|
+
this.allCardsLoaded = false;
|
|
2775
|
+
this._cardRestoreTargetPage = 0;
|
|
2776
|
+
this._cardExpectedPage = 1;
|
|
2777
|
+
this.pageChange.emit({ page: 1, pageSize: this._effectiveCardPageSize, searchValue: this.searchbar?.searchControl?.value ?? '' });
|
|
2778
|
+
}
|
|
2779
|
+
loadMoreCards() {
|
|
2780
|
+
if (this.loading || this.allCardsLoaded || this._cardExpectedPage !== 0)
|
|
2781
|
+
return;
|
|
2782
|
+
if (this.totalRecords > 0 && this.cardDatasource.length >= this.totalRecords) {
|
|
2783
|
+
this.allCardsLoaded = true;
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
const nextPage = this.cardPage + 1;
|
|
2787
|
+
this._cardExpectedPage = nextPage;
|
|
2788
|
+
this.pageChange.emit({ page: nextPage, pageSize: this._effectiveCardPageSize, searchValue: this.searchbar?.searchControl?.value ?? '' });
|
|
2789
|
+
}
|
|
2790
|
+
onCardScroll(event) {
|
|
2791
|
+
const el = event.target;
|
|
2792
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 150) {
|
|
2793
|
+
this.loadMoreCards();
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
isHighlightedCard(row) {
|
|
2797
|
+
if (!this.restoredRow || !this.primaryKey)
|
|
2798
|
+
return false;
|
|
2799
|
+
const val = row[this.primaryKey];
|
|
2800
|
+
return val !== undefined && val === this.restoredRow[this.primaryKey];
|
|
2801
|
+
}
|
|
2802
|
+
toggleCardMenu(index, event) {
|
|
2803
|
+
event.stopPropagation();
|
|
2804
|
+
if (this.cardOpenMenuIndex === index) {
|
|
2805
|
+
this.cardOpenMenuIndex = null;
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2809
|
+
const menuWidth = 190;
|
|
2810
|
+
const left = Math.min(Math.max(rect.right - menuWidth, 8), window.innerWidth - menuWidth - 8);
|
|
2811
|
+
this.cardMenuPosition = { top: rect.bottom + 6, left };
|
|
2812
|
+
this.cardOpenMenuIndex = index;
|
|
2813
|
+
}
|
|
2814
|
+
getCellValue(row, col) {
|
|
2815
|
+
const raw = row[col.key];
|
|
2816
|
+
if (col.displayFn)
|
|
2817
|
+
return col.displayFn(raw, row);
|
|
2818
|
+
if (col.enums?.length) {
|
|
2819
|
+
const match = col.enums.find(e => e.value === raw);
|
|
2820
|
+
return match ? match.label : (raw ?? '--');
|
|
2821
|
+
}
|
|
2822
|
+
return raw !== null && raw !== undefined && raw !== '' ? raw : '--';
|
|
2823
|
+
}
|
|
2824
|
+
hasVisibleActions(row) {
|
|
2825
|
+
return this.moreMenuActions.some(a => !a.hideIf || !a.hideIf(row));
|
|
2826
|
+
}
|
|
2827
|
+
// ── State maintenance ─────────────────────────────────────────
|
|
2651
2828
|
statemainTain() {
|
|
2652
2829
|
if (!this.stateKey)
|
|
2653
2830
|
return;
|
|
@@ -2656,51 +2833,64 @@ class OslSetup {
|
|
|
2656
2833
|
return;
|
|
2657
2834
|
this._pendingScrollTop = state.scrollTop;
|
|
2658
2835
|
this.restoredRow = state.highlightedRow;
|
|
2659
|
-
if (this.
|
|
2660
|
-
this.
|
|
2661
|
-
this.
|
|
2662
|
-
this.
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
this.
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2836
|
+
if (this.viewMode === 'card') {
|
|
2837
|
+
this._needsInitialCardLoad = false;
|
|
2838
|
+
this.cardDatasource = [];
|
|
2839
|
+
this.cardPage = 0;
|
|
2840
|
+
this.allCardsLoaded = false;
|
|
2841
|
+
this._cardRestoreTargetPage = state.page;
|
|
2842
|
+
if (this.searchbar && state.searchValue) {
|
|
2843
|
+
this._isRestoring = true;
|
|
2844
|
+
this.searchbar.searchControl.setValue(state.searchValue, { emitEvent: false });
|
|
2845
|
+
}
|
|
2846
|
+
this._cardExpectedPage = 1;
|
|
2847
|
+
if (state.searchValue) {
|
|
2848
|
+
this.onSearchSetup(state.searchValue);
|
|
2849
|
+
}
|
|
2850
|
+
else {
|
|
2851
|
+
this.pageChange.emit({ page: 1, pageSize: this._effectiveCardPageSize, searchValue: '' });
|
|
2852
|
+
}
|
|
2853
|
+
this.onStateRestored.emit(state);
|
|
2672
2854
|
}
|
|
2673
2855
|
else {
|
|
2674
|
-
|
|
2675
|
-
|
|
2856
|
+
if (this.gridRef) {
|
|
2857
|
+
this.gridRef.currentPage = state.page;
|
|
2858
|
+
this.gridRef.pageSize = state.pageSize;
|
|
2859
|
+
this.gridRef.setRestorePage(state.page);
|
|
2860
|
+
}
|
|
2861
|
+
if (this.searchbar && state.searchValue) {
|
|
2862
|
+
this._isRestoring = true;
|
|
2863
|
+
this.searchbar.searchControl.setValue(state.searchValue, { emitEvent: false });
|
|
2864
|
+
}
|
|
2865
|
+
if (state.searchValue) {
|
|
2866
|
+
this.onSearchSetup(state.searchValue);
|
|
2867
|
+
}
|
|
2868
|
+
else {
|
|
2869
|
+
this.pageChange.emit({ page: state.page, pageSize: state.pageSize, searchValue: '' });
|
|
2870
|
+
}
|
|
2871
|
+
this.onStateRestored.emit(state);
|
|
2676
2872
|
}
|
|
2677
|
-
// Also emit the dedicated restore event for apps that want fine-grained control.
|
|
2678
|
-
this.onStateRestored.emit(state);
|
|
2679
2873
|
setTimeout(() => {
|
|
2680
2874
|
this.restoredRow = null;
|
|
2681
2875
|
this.gridRef?.clearRestorePage();
|
|
2682
2876
|
}, 3000);
|
|
2683
2877
|
}
|
|
2684
|
-
|
|
2685
|
-
this.statemainTain();
|
|
2686
|
-
}
|
|
2687
|
-
ngOnChanges(changes) {
|
|
2688
|
-
if (changes['datasource'] && this._pendingScrollTop !== null) {
|
|
2689
|
-
const ds = changes['datasource'].currentValue;
|
|
2690
|
-
// Only restore scroll once real data has arrived (not on intermediate empty/loading states)
|
|
2691
|
-
if (ds?.length > 0) {
|
|
2692
|
-
const scrollTop = this._pendingScrollTop;
|
|
2693
|
-
this._pendingScrollTop = null;
|
|
2694
|
-
setTimeout(() => { this.gridRef?.scrollTo(scrollTop); }, 50);
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2878
|
+
// ── Search ────────────────────────────────────────────────────
|
|
2698
2879
|
onSearchSetup(event) {
|
|
2880
|
+
if (this.viewMode === 'card') {
|
|
2881
|
+
this.cardDatasource = [];
|
|
2882
|
+
this.cardPage = 0;
|
|
2883
|
+
this.allCardsLoaded = false;
|
|
2884
|
+
this._cardExpectedPage = 1;
|
|
2885
|
+
}
|
|
2699
2886
|
if (this._isRestoring) {
|
|
2700
|
-
|
|
2701
|
-
const restoredPage = this.gridRef?.currentPage ?? 1;
|
|
2887
|
+
const restoredPage = this.viewMode === 'card' ? 1 : (this.gridRef?.currentPage ?? 1);
|
|
2702
2888
|
this._isRestoring = false;
|
|
2703
|
-
this.pageChange.emit({
|
|
2889
|
+
this.pageChange.emit({
|
|
2890
|
+
page: restoredPage,
|
|
2891
|
+
pageSize: this.viewMode === 'card' ? this._effectiveCardPageSize : (this.gridRef?.pageSize ?? this.pageSize),
|
|
2892
|
+
searchValue: event,
|
|
2893
|
+
});
|
|
2704
2894
|
this.onSearch.emit(event);
|
|
2705
2895
|
return;
|
|
2706
2896
|
}
|
|
@@ -2710,12 +2900,12 @@ class OslSetup {
|
|
|
2710
2900
|
}
|
|
2711
2901
|
this.pageChange.emit({
|
|
2712
2902
|
page: 1,
|
|
2713
|
-
pageSize: this.gridRef?.pageSize || 10,
|
|
2714
|
-
searchValue: event
|
|
2903
|
+
pageSize: this.viewMode === 'card' ? this._effectiveCardPageSize : (this.gridRef?.pageSize || 10),
|
|
2904
|
+
searchValue: event,
|
|
2715
2905
|
});
|
|
2716
2906
|
this.onSearch.emit(event);
|
|
2717
2907
|
}
|
|
2718
|
-
|
|
2908
|
+
// ── Table helpers ─────────────────────────────────────────────
|
|
2719
2909
|
get columnsWithActions() {
|
|
2720
2910
|
if (!this.hasForm)
|
|
2721
2911
|
return this.columns;
|
|
@@ -2728,6 +2918,7 @@ class OslSetup {
|
|
|
2728
2918
|
onPageChange(eventEmitter, event) {
|
|
2729
2919
|
eventEmitter.emit({ ...event, searchValue: this.searchbar?.searchControl?.value });
|
|
2730
2920
|
}
|
|
2921
|
+
// ── Dialog actions ────────────────────────────────────────────
|
|
2731
2922
|
openAddDialog() {
|
|
2732
2923
|
this.dialogMode = 'add';
|
|
2733
2924
|
if (this.beforeDisplay) {
|
|
@@ -2747,10 +2938,12 @@ class OslSetup {
|
|
|
2747
2938
|
this.dialogMode = 'edit';
|
|
2748
2939
|
if (this.stateKey) {
|
|
2749
2940
|
this._stateService.save(this.stateKey, {
|
|
2750
|
-
page: this.gridRef?.currentPage ?? 1,
|
|
2751
|
-
pageSize: this.gridRef?.pageSize ?? this.pageSize,
|
|
2941
|
+
page: this.viewMode === 'card' ? this.cardPage : (this.gridRef?.currentPage ?? 1),
|
|
2942
|
+
pageSize: this.viewMode === 'card' ? this._effectiveCardPageSize : (this.gridRef?.pageSize ?? this.pageSize),
|
|
2752
2943
|
searchValue: this.searchbar?.searchControl?.value ?? '',
|
|
2753
|
-
scrollTop: this.
|
|
2944
|
+
scrollTop: this.viewMode === 'card'
|
|
2945
|
+
? (this.cardContainerRef?.nativeElement?.scrollTop ?? 0)
|
|
2946
|
+
: (this.gridRef?.getScrollTop() ?? 0),
|
|
2754
2947
|
highlightedRow: row,
|
|
2755
2948
|
});
|
|
2756
2949
|
}
|
|
@@ -2761,7 +2954,6 @@ class OslSetup {
|
|
|
2761
2954
|
}
|
|
2762
2955
|
this._openDialog();
|
|
2763
2956
|
if (this.beforeDisplay) {
|
|
2764
|
-
// this.datasource.find(x=>x[this.primaryKey]==)
|
|
2765
2957
|
this.formLoading = true;
|
|
2766
2958
|
this.dialogModel = await this.beforeDisplay(row);
|
|
2767
2959
|
this.formLoading = false;
|
|
@@ -2813,16 +3005,16 @@ class OslSetup {
|
|
|
2813
3005
|
formFooter: this.customFormFooter ? this.customFooterWrapperTpl : this.formFooterTpl,
|
|
2814
3006
|
},
|
|
2815
3007
|
});
|
|
2816
|
-
this._dialogRef.afterClosed().subscribe(
|
|
3008
|
+
this._dialogRef.afterClosed().subscribe(() => {
|
|
2817
3009
|
this.statemainTain();
|
|
2818
3010
|
});
|
|
2819
3011
|
}
|
|
2820
3012
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslSetup, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2821
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslSetup, isStandalone: false, selector: "osl-setup", inputs: { title: "title", columns: "columns", datasource: "datasource", isPaginated: "isPaginated", pageSize: "pageSize", autoMode: "autoMode", tableHeight: "tableHeight", totalRecords: "totalRecords", loading: "loading", dialogWidth: "dialogWidth", formElements: "formElements", beforeDisplay: "beforeDisplay", onAddEditFn: "onAddEditFn", isLister: "isLister", canAdd: "canAdd", canEdit: "canEdit", canDelete: "canDelete", moreMenuActions: "moreMenuActions", customFormFooter: "customFormFooter", customHeaderTemp: "customHeaderTemp", partialCustomHeaderTemp: "partialCustomHeaderTemp", stateKey: "stateKey", primaryKey: "primaryKey", onSave: "onSave" }, outputs: { onSearch: "onSearch", onAdd: "onAdd", onEdit: "onEdit", onDelete: "onDelete", pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", onRowClick: "onRowClick", onStateRestored: "onStateRestored" }, viewQueries: [{ propertyName: "formBodyTpl", first: true, predicate: ["formBodyTpl"], descendants: true }, { propertyName: "formFooterTpl", first: true, predicate: ["formFooterTpl"], descendants: true }, { propertyName: "customFooterWrapperTpl", first: true, predicate: ["customFooterWrapperTpl"], descendants: true }, { propertyName: "searchbar", first: true, predicate: ["searchbar"], descendants: true }, { propertyName: "gridRef", first: true, predicate: ["gridRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"p-2\">\r\n\r\n <!-- Header -->\r\n <div class=\"osl-setup-header\">\r\n <h5 class=\"mb-0\">{{ title }}</h5>\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <osl-searchbar #searchbar class=\"mx-2\" (onSearch)=\"onSearchSetup($event)\"></osl-searchbar>\r\n @if(!isLister && canAdd){\r\n <osl-button\r\n variant=\"secondary\"\r\n size=\"sm\"\r\n [label]=\"'Add ' + title\"\r\n (clickEv)=\"openAddDialog()\"\r\n ></osl-button>\r\n\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Grid -->\r\n <div class=\"osl-setup-body my-2\">\r\n <osl-grid\r\n #gridRef\r\n [columns]=\"columnsWithActions\"\r\n [(datasource)]=\"datasource\"\r\n [isPaginated]=\"isPaginated\"\r\n [pageSize]=\"pageSize\"\r\n [autoMode]=\"autoMode\"\r\n [tableHeight]=\"tableHeight\"\r\n [totalRecords]=\"totalRecords\"\r\n [loading]=\"loading\"\r\n [moreMenuActions]=\"moreMenuActions\"\r\n [canEdit]=\"canEdit\"\r\n [canDelete]=\"canDelete\"\r\n [highlightedRow]=\"restoredRow\"\r\n [primaryKey]=\"primaryKey\"\r\n (editClick)=\"openEditDialog($event)\"\r\n (deleteClick)=\"onDeleteClick($event)\"\r\n (pageChange)=\"onPageChange(pageChange,$event)\"\r\n (pageSizeChange)=\"onPageChange(pageSizeChange,$event)\"\r\n (sortChange)=\"sortChange.emit($event)\"\r\n [isSelectable]=\"isLister\";\r\n (onRowClick)=\"onRowClick.emit($event)\"\r\n />\r\n </div>\r\n\r\n</div>\r\n\r\n<!-- Dialog: Form Body -->\r\n<ng-template #formBodyTpl>\r\n <osl-dynamic-form\r\n [skeletonLoading]=\"formLoading\"\r\n [elements]=\"formElements\"\r\n [(model)]=\"dialogModel\"\r\n ></osl-dynamic-form>\r\n</ng-template>\r\n\r\n<!-- Dialog: Form Footer -->\r\n<ng-template #formFooterTpl>\r\n <div class=\"osl-setup-dialog-footer\">\r\n\r\n\r\n <osl-button [loading]=\"saveLoading\" variant=\"secondary\" label=\"Save\" (click)=\"saveDialog()\"></osl-button>\r\n\r\n </div>\r\n</ng-template>\r\n\r\n<!-- Wrapper that bridges DialogWrapper's implicit context to the custom footer template -->\r\n<ng-template #customFooterWrapperTpl let-data>\r\n <ng-container *ngTemplateOutlet=\"customFormFooter!; context: { $implicit: { dialogModel: dialogModel, dialogMode: dialogMode, dialogRef: data.dialogRef } }\"></ng-container>\r\n</ng-template>\r\n", styles: [".osl-setup-header{display:flex;align-items:center;justify-content:space-between}.osl-setup-dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;width:100%}.dialog-cancel-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;border-color:var(--osl-border-color, #d1d5db);color:var(--osl-secondary, #6b7280);border-radius:var(--osl-border-radius, 4px);padding:0 16px;transition:all .2s ease}.dialog-cancel-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-cancel-btn:hover{border-color:#9ca3af;background:#f9fafb;color:#374151}.dialog-save-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);color:#fff;border-radius:var(--osl-border-radius, 4px);padding:0 18px;transition:all .2s ease;box-shadow:0 2px 8px #2563eb40}.dialog-save-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-save-btn:hover{background:linear-gradient(135deg,var(--osl-primary-hover, #1d4ed8),#2563eb);box-shadow:0 4px 14px #2563eb66;transform:translateY(-1px)}.dialog-save-btn:active{transform:translateY(0);box-shadow:0 2px 8px #2563eb40}\n"], dependencies: [{ kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: DynamicForm, selector: "osl-dynamic-form", inputs: ["elements", "model", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange"] }, { kind: "component", type: OslButton, selector: "osl-button", inputs: ["label", "icon", "variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clickEv"] }, { kind: "component", type: OslSearchbar, selector: "osl-searchbar", inputs: ["label"], outputs: ["onSearch"] }, { kind: "component", type: OslGrid, selector: "osl-grid", inputs: ["columns", "datasource", "isPaginated", "pageSize", "autoMode", "totalRecords", "tableHeight", "loading", "isSelectable", "moreMenuActions", "canEdit", "canDelete", "highlightedRow", "primaryKey"], outputs: ["datasourceChange", "pageChange", "pageSizeChange", "sortChange", "editClick", "deleteClick", "onRowClick"] }] });
|
|
3013
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: OslSetup, isStandalone: false, selector: "osl-setup", inputs: { title: "title", columns: "columns", datasource: "datasource", isPaginated: "isPaginated", pageSize: "pageSize", autoMode: "autoMode", tableHeight: "tableHeight", totalRecords: "totalRecords", loading: "loading", dialogWidth: "dialogWidth", formElements: "formElements", beforeDisplay: "beforeDisplay", onAddEditFn: "onAddEditFn", isLister: "isLister", canAdd: "canAdd", canEdit: "canEdit", canDelete: "canDelete", moreMenuActions: "moreMenuActions", customFormFooter: "customFormFooter", customHeaderTemp: "customHeaderTemp", partialCustomHeaderTemp: "partialCustomHeaderTemp", stateKey: "stateKey", primaryKey: "primaryKey", onSave: "onSave", cardPageSize: "cardPageSize", cardTemplate: "cardTemplate" }, outputs: { onSearch: "onSearch", onAdd: "onAdd", onEdit: "onEdit", onDelete: "onDelete", pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", onRowClick: "onRowClick", onStateRestored: "onStateRestored" }, host: { listeners: { "document:click": "onDocumentClick()" } }, viewQueries: [{ propertyName: "formBodyTpl", first: true, predicate: ["formBodyTpl"], descendants: true }, { propertyName: "formFooterTpl", first: true, predicate: ["formFooterTpl"], descendants: true }, { propertyName: "customFooterWrapperTpl", first: true, predicate: ["customFooterWrapperTpl"], descendants: true }, { propertyName: "searchbar", first: true, predicate: ["searchbar"], descendants: true }, { propertyName: "gridRef", first: true, predicate: ["gridRef"], descendants: true }, { propertyName: "cardContainerRef", first: true, predicate: ["cardContainerRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"p-2\">\n\n <!-- Header -->\n <div class=\"osl-setup-header\">\n <h5 class=\"mb-0\">{{ title }}</h5>\n <div class=\"d-flex align-items-center gap-2\">\n <osl-searchbar #searchbar class=\"mx-2\" (onSearch)=\"onSearchSetup($event)\"></osl-searchbar>\n\n <!-- View Toggle -->\n <div class=\"osl-view-toggle\">\n <button\n class=\"osl-view-toggle-btn\"\n [class.osl-view-toggle-btn--active]=\"viewMode === 'table'\"\n (click)=\"toggleView('table')\"\n title=\"Table view\">\n <mat-icon>table_rows</mat-icon>\n </button>\n <button\n class=\"osl-view-toggle-btn\"\n [class.osl-view-toggle-btn--active]=\"viewMode === 'card'\"\n (click)=\"toggleView('card')\"\n title=\"Card view\">\n <mat-icon>grid_view</mat-icon>\n </button>\n </div>\n\n @if (!isLister && canAdd) {\n <osl-button\n variant=\"secondary\"\n size=\"sm\"\n [label]=\"'Add ' + title\"\n (clickEv)=\"openAddDialog()\"\n ></osl-button>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Table View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'table') {\n <div class=\"osl-setup-body my-2\">\n <osl-grid\n #gridRef\n [columns]=\"columnsWithActions\"\n [(datasource)]=\"datasource\"\n [isPaginated]=\"isPaginated\"\n [pageSize]=\"pageSize\"\n [autoMode]=\"autoMode\"\n [tableHeight]=\"tableHeight\"\n [totalRecords]=\"totalRecords\"\n [loading]=\"loading\"\n [moreMenuActions]=\"moreMenuActions\"\n [canEdit]=\"canEdit\"\n [canDelete]=\"canDelete\"\n [highlightedRow]=\"restoredRow\"\n [primaryKey]=\"primaryKey\"\n (editClick)=\"openEditDialog($event)\"\n (deleteClick)=\"onDeleteClick($event)\"\n (pageChange)=\"onPageChange(pageChange, $event)\"\n (pageSizeChange)=\"onPageChange(pageSizeChange, $event)\"\n (sortChange)=\"sortChange.emit($event)\"\n [isSelectable]=\"isLister\"\n (onRowClick)=\"onRowClick.emit($event)\"\n />\n </div>\n }\n\n <!-- \u2500\u2500 Card View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'card') {\n <div\n #cardContainerRef\n class=\"osl-card-container my-2\"\n [style.height]=\"tableHeight\"\n (scroll)=\"onCardScroll($event)\">\n\n <!-- Skeleton: initial loading with no data yet -->\n @if (loading && cardDatasource.length === 0) {\n <div class=\"osl-card-grid\">\n @for (sk of skeletonCardRows; track $index) {\n <div class=\"osl-card osl-card--skeleton\">\n <div class=\"osl-card-header\">\n <div class=\"osl-skeleton osl-skeleton--card-title\"></div>\n </div>\n <div class=\"osl-card-body\">\n <div class=\"osl-skeleton osl-skeleton--card-field\"></div>\n <div class=\"osl-skeleton osl-skeleton--card-field\"></div>\n <div class=\"osl-skeleton osl-skeleton--card-field\"></div>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Empty state -->\n @else if (cardDatasource.length === 0 && !loading) {\n <div class=\"osl-card-empty\">\n <div class=\"d-flex align-items-center justify-content-center floating-element\">\n <div class=\"mx-4\">\n <p class=\"f-s-20 f-w-600 text-start\">No Records match the <br> current filters.</p>\n <p class=\"text-start\">Expecting to see new Records? Try again in <br> a few seconds as the system catches up</p>\n </div>\n <i class=\"icon\"></i>\n </div>\n </div>\n }\n\n <!-- Cards -->\n @else {\n <div class=\"osl-card-grid\">\n\n <!-- Custom card template -->\n @if (cardTemplate) {\n @for (row of cardDatasource; track row[primaryKey]; let i = $index) {\n <ng-container *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, index: i }\"></ng-container>\n }\n }\n\n <!-- Auto-generated card -->\n @else {\n @for (row of cardDatasource; track row[primaryKey]; let i = $index) {\n <div class=\"osl-card\"\n [class.osl-card--highlighted]=\"isHighlightedCard(row)\"\n [class.pointer]=\"isLister\"\n (click)=\"isLister ? onRowClick.emit(row) : null\">\n\n <!-- Card header: title + action buttons -->\n <div class=\"osl-card-header\">\n @if (cardTitleColumn) {\n <span class=\"osl-card-title\">{{ getCellValue(row, cardTitleColumn) }}</span>\n }\n\n @if (!isLister && (canEdit || canDelete || (moreMenuActions.length > 0 && hasVisibleActions(row)))) {\n <div class=\"osl-card-actions\">\n\n <!-- More-actions dropdown -->\n @if (moreMenuActions.length > 0 && hasVisibleActions(row)) {\n <div class=\"osl-menu-wrapper\">\n <button class=\"osl-grid-icon-btn\" (click)=\"toggleCardMenu(i, $event)\" title=\"More actions\">\n <mat-icon>more_vert</mat-icon>\n </button>\n @if (cardOpenMenuIndex === i) {\n <div class=\"osl-custom-menu\"\n [style.top.px]=\"cardMenuPosition.top\"\n [style.left.px]=\"cardMenuPosition.left\">\n <div class=\"osl-custom-menu-header\">Actions</div>\n @for (action of moreMenuActions; track $index) {\n @if (!action.hideIf || !action.hideIf(row)) {\n <button class=\"osl-custom-menu-item\"\n (click)=\"action.click(row); cardOpenMenuIndex = null; $event.stopPropagation()\">\n <span class=\"osl-custom-menu-dot\"></span>\n {{ action.labelIf ? action.labelIf(row) : action.label }}\n </button>\n }\n }\n </div>\n }\n </div>\n }\n\n @if (canEdit) {\n <button class=\"osl-grid-icon-btn\"\n (click)=\"$event.stopPropagation(); openEditDialog(row)\"\n title=\"Edit\">\n <mat-icon>mode_edit_outline</mat-icon>\n </button>\n }\n @if (canDelete) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"$event.stopPropagation(); onDeleteClick(row)\"\n title=\"Delete\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Card body: remaining column values -->\n <div class=\"osl-card-body\">\n @for (col of cardBodyColumns; track col.key) {\n <div class=\"osl-card-field\">\n <span class=\"osl-card-label\">{{ col.label }}</span>\n <span class=\"osl-card-value\">{{ getCellValue(row, col) }}</span>\n </div>\n }\n </div>\n\n </div>\n }\n }\n </div>\n\n <!-- Bottom spinner while loading next page -->\n @if (loading) {\n <div class=\"osl-card-loader\">\n <div class=\"osl-card-loader-spinner\"></div>\n </div>\n }\n\n <!-- All records loaded indicator -->\n @if (allCardsLoaded && !loading && cardDatasource.length > 0) {\n <div class=\"osl-card-all-loaded\">\n All {{ totalRecords || cardDatasource.length }} records loaded\n </div>\n }\n }\n </div>\n }\n\n</div>\n\n<!-- Dialog: Form Body -->\n<ng-template #formBodyTpl>\n <osl-dynamic-form\n [skeletonLoading]=\"formLoading\"\n [elements]=\"formElements\"\n [(model)]=\"dialogModel\"\n ></osl-dynamic-form>\n</ng-template>\n\n<!-- Dialog: Form Footer -->\n<ng-template #formFooterTpl>\n <div class=\"osl-setup-dialog-footer\">\n <osl-button [loading]=\"saveLoading\" variant=\"secondary\" label=\"Save\" (click)=\"saveDialog()\"></osl-button>\n </div>\n</ng-template>\n\n<!-- Wrapper that bridges DialogWrapper's implicit context to the custom footer template -->\n<ng-template #customFooterWrapperTpl let-data>\n <ng-container *ngTemplateOutlet=\"customFormFooter!; context: { $implicit: { dialogModel: dialogModel, dialogMode: dialogMode, dialogRef: data.dialogRef } }\"></ng-container>\n</ng-template>\n", styles: [".osl-setup-header{display:flex;align-items:center;justify-content:space-between}.osl-view-toggle{display:flex;border:1px solid var(--osl-border-color);border-radius:var(--osl-border-radius);overflow:hidden;flex-shrink:0}.osl-view-toggle-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;background:transparent;cursor:pointer;color:#6b7280;transition:background .12s,color .12s;padding:0}.osl-view-toggle-btn mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.osl-view-toggle-btn:first-child{border-right:1px solid var(--osl-border-color)}.osl-view-toggle-btn--active{background:var(--osl-primary);color:#fff}.osl-view-toggle-btn:not(.osl-view-toggle-btn--active):hover{background:#f3f4f6}.osl-card-container{overflow-y:auto;overflow-x:hidden;border:1px solid var(--osl-border-color);border-radius:var(--osl-border-radius);background:#f9fafb;padding:16px;box-sizing:border-box}.osl-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.osl-card{background:#fff;border:1px solid var(--osl-border-color);border-radius:var(--osl-border-radius);padding:16px;transition:box-shadow .15s,border-color .15s;display:flex;flex-direction:column}.osl-card:hover{box-shadow:0 4px 12px #00000014;border-color:#9ca3af}.osl-card--highlighted{box-shadow:inset 4px 0 0 var(--osl-primary)!important;animation:osl-card-highlight-fade 3s ease-out forwards}.osl-card--skeleton{pointer-events:none}.osl-card--skeleton:hover{box-shadow:none;border-color:var(--osl-border-color)}@keyframes osl-card-highlight-fade{0%,60%{background:#6366f10d}to{background:#fff}}.osl-card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid #f3f4f6}.osl-card-title{font-weight:600;font-size:14px;color:#111827;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0}.osl-card-body{display:flex;flex-direction:column;gap:8px;flex:1}.osl-card-field{display:flex;align-items:baseline;gap:8px;font-size:13px;line-height:1.4}.osl-card-label{color:#6b7280;font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.04em;min-width:90px;flex-shrink:0}.osl-card-value{color:#111827;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-card-empty{display:flex;align-items:center;justify-content:center;min-height:300px;height:100%}.osl-card-loader{display:flex;justify-content:center;align-items:center;padding:24px}.osl-card-loader-spinner{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:var(--osl-primary);border-radius:50%;animation:osl-spin .8s linear infinite}@keyframes osl-spin{to{transform:rotate(360deg)}}.osl-card-all-loaded{text-align:center;padding:16px;font-size:12px;color:#9ca3af;border-top:1px solid #f3f4f6;margin-top:8px}.osl-skeleton--card-title{height:16px;width:65%;background:#e5e7eb;border-radius:4px;animation:osl-skeleton-pulse 1.4s ease-in-out infinite}.osl-skeleton--card-field{height:12px;width:85%;background:#e5e7eb;border-radius:4px;animation:osl-skeleton-pulse 1.4s ease-in-out infinite}@keyframes osl-skeleton-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-setup-dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;width:100%}.dialog-cancel-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;border-color:var(--osl-border-color, #d1d5db);color:var(--osl-secondary, #6b7280);border-radius:var(--osl-border-radius, 4px);padding:0 16px;transition:all .2s ease}.dialog-cancel-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-cancel-btn:hover{border-color:#9ca3af;background:#f9fafb;color:#374151}.dialog-save-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);color:#fff;border-radius:var(--osl-border-radius, 4px);padding:0 18px;transition:all .2s ease;box-shadow:0 2px 8px #2563eb40}.dialog-save-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-save-btn:hover{background:linear-gradient(135deg,var(--osl-primary-hover, #1d4ed8),#2563eb);box-shadow:0 4px 14px #2563eb66;transform:translateY(-1px)}.dialog-save-btn:active{transform:translateY(0);box-shadow:0 2px 8px #2563eb40}\n"], dependencies: [{ kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DynamicForm, selector: "osl-dynamic-form", inputs: ["elements", "model", "skeletonLoading", "skeletonTheme"], outputs: ["modelChange"] }, { kind: "component", type: OslButton, selector: "osl-button", inputs: ["label", "icon", "variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clickEv"] }, { kind: "component", type: OslSearchbar, selector: "osl-searchbar", inputs: ["label"], outputs: ["onSearch"] }, { kind: "component", type: OslGrid, selector: "osl-grid", inputs: ["columns", "datasource", "isPaginated", "pageSize", "autoMode", "totalRecords", "tableHeight", "loading", "isSelectable", "moreMenuActions", "canEdit", "canDelete", "highlightedRow", "primaryKey"], outputs: ["datasourceChange", "pageChange", "pageSizeChange", "sortChange", "editClick", "deleteClick", "onRowClick"] }] });
|
|
2822
3014
|
}
|
|
2823
3015
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslSetup, decorators: [{
|
|
2824
3016
|
type: Component,
|
|
2825
|
-
args: [{ selector: 'osl-setup', standalone: false, template: "<div class=\"p-2\">\r\n\r\n <!-- Header -->\r\n <div class=\"osl-setup-header\">\r\n <h5 class=\"mb-0\">{{ title }}</h5>\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <osl-searchbar #searchbar class=\"mx-2\" (onSearch)=\"onSearchSetup($event)\"></osl-searchbar>\r\n @if(!isLister && canAdd){\r\n <osl-button\r\n variant=\"secondary\"\r\n size=\"sm\"\r\n [label]=\"'Add ' + title\"\r\n (clickEv)=\"openAddDialog()\"\r\n ></osl-button>\r\n\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Grid -->\r\n <div class=\"osl-setup-body my-2\">\r\n <osl-grid\r\n #gridRef\r\n [columns]=\"columnsWithActions\"\r\n [(datasource)]=\"datasource\"\r\n [isPaginated]=\"isPaginated\"\r\n [pageSize]=\"pageSize\"\r\n [autoMode]=\"autoMode\"\r\n [tableHeight]=\"tableHeight\"\r\n [totalRecords]=\"totalRecords\"\r\n [loading]=\"loading\"\r\n [moreMenuActions]=\"moreMenuActions\"\r\n [canEdit]=\"canEdit\"\r\n [canDelete]=\"canDelete\"\r\n [highlightedRow]=\"restoredRow\"\r\n [primaryKey]=\"primaryKey\"\r\n (editClick)=\"openEditDialog($event)\"\r\n (deleteClick)=\"onDeleteClick($event)\"\r\n (pageChange)=\"onPageChange(pageChange,$event)\"\r\n (pageSizeChange)=\"onPageChange(pageSizeChange,$event)\"\r\n (sortChange)=\"sortChange.emit($event)\"\r\n [isSelectable]=\"isLister\";\r\n (onRowClick)=\"onRowClick.emit($event)\"\r\n />\r\n </div>\r\n\r\n</div>\r\n\r\n<!-- Dialog: Form Body -->\r\n<ng-template #formBodyTpl>\r\n <osl-dynamic-form\r\n [skeletonLoading]=\"formLoading\"\r\n [elements]=\"formElements\"\r\n [(model)]=\"dialogModel\"\r\n ></osl-dynamic-form>\r\n</ng-template>\r\n\r\n<!-- Dialog: Form Footer -->\r\n<ng-template #formFooterTpl>\r\n <div class=\"osl-setup-dialog-footer\">\r\n\r\n\r\n <osl-button [loading]=\"saveLoading\" variant=\"secondary\" label=\"Save\" (click)=\"saveDialog()\"></osl-button>\r\n\r\n </div>\r\n</ng-template>\r\n\r\n<!-- Wrapper that bridges DialogWrapper's implicit context to the custom footer template -->\r\n<ng-template #customFooterWrapperTpl let-data>\r\n <ng-container *ngTemplateOutlet=\"customFormFooter!; context: { $implicit: { dialogModel: dialogModel, dialogMode: dialogMode, dialogRef: data.dialogRef } }\"></ng-container>\r\n</ng-template>\r\n", styles: [".osl-setup-header{display:flex;align-items:center;justify-content:space-between}.osl-setup-dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;width:100%}.dialog-cancel-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;border-color:var(--osl-border-color, #d1d5db);color:var(--osl-secondary, #6b7280);border-radius:var(--osl-border-radius, 4px);padding:0 16px;transition:all .2s ease}.dialog-cancel-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-cancel-btn:hover{border-color:#9ca3af;background:#f9fafb;color:#374151}.dialog-save-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);color:#fff;border-radius:var(--osl-border-radius, 4px);padding:0 18px;transition:all .2s ease;box-shadow:0 2px 8px #2563eb40}.dialog-save-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-save-btn:hover{background:linear-gradient(135deg,var(--osl-primary-hover, #1d4ed8),#2563eb);box-shadow:0 4px 14px #2563eb66;transform:translateY(-1px)}.dialog-save-btn:active{transform:translateY(0);box-shadow:0 2px 8px #2563eb40}\n"] }]
|
|
3017
|
+
args: [{ selector: 'osl-setup', standalone: false, template: "<div class=\"p-2\">\n\n <!-- Header -->\n <div class=\"osl-setup-header\">\n <h5 class=\"mb-0\">{{ title }}</h5>\n <div class=\"d-flex align-items-center gap-2\">\n <osl-searchbar #searchbar class=\"mx-2\" (onSearch)=\"onSearchSetup($event)\"></osl-searchbar>\n\n <!-- View Toggle -->\n <div class=\"osl-view-toggle\">\n <button\n class=\"osl-view-toggle-btn\"\n [class.osl-view-toggle-btn--active]=\"viewMode === 'table'\"\n (click)=\"toggleView('table')\"\n title=\"Table view\">\n <mat-icon>table_rows</mat-icon>\n </button>\n <button\n class=\"osl-view-toggle-btn\"\n [class.osl-view-toggle-btn--active]=\"viewMode === 'card'\"\n (click)=\"toggleView('card')\"\n title=\"Card view\">\n <mat-icon>grid_view</mat-icon>\n </button>\n </div>\n\n @if (!isLister && canAdd) {\n <osl-button\n variant=\"secondary\"\n size=\"sm\"\n [label]=\"'Add ' + title\"\n (clickEv)=\"openAddDialog()\"\n ></osl-button>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Table View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'table') {\n <div class=\"osl-setup-body my-2\">\n <osl-grid\n #gridRef\n [columns]=\"columnsWithActions\"\n [(datasource)]=\"datasource\"\n [isPaginated]=\"isPaginated\"\n [pageSize]=\"pageSize\"\n [autoMode]=\"autoMode\"\n [tableHeight]=\"tableHeight\"\n [totalRecords]=\"totalRecords\"\n [loading]=\"loading\"\n [moreMenuActions]=\"moreMenuActions\"\n [canEdit]=\"canEdit\"\n [canDelete]=\"canDelete\"\n [highlightedRow]=\"restoredRow\"\n [primaryKey]=\"primaryKey\"\n (editClick)=\"openEditDialog($event)\"\n (deleteClick)=\"onDeleteClick($event)\"\n (pageChange)=\"onPageChange(pageChange, $event)\"\n (pageSizeChange)=\"onPageChange(pageSizeChange, $event)\"\n (sortChange)=\"sortChange.emit($event)\"\n [isSelectable]=\"isLister\"\n (onRowClick)=\"onRowClick.emit($event)\"\n />\n </div>\n }\n\n <!-- \u2500\u2500 Card View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode === 'card') {\n <div\n #cardContainerRef\n class=\"osl-card-container my-2\"\n [style.height]=\"tableHeight\"\n (scroll)=\"onCardScroll($event)\">\n\n <!-- Skeleton: initial loading with no data yet -->\n @if (loading && cardDatasource.length === 0) {\n <div class=\"osl-card-grid\">\n @for (sk of skeletonCardRows; track $index) {\n <div class=\"osl-card osl-card--skeleton\">\n <div class=\"osl-card-header\">\n <div class=\"osl-skeleton osl-skeleton--card-title\"></div>\n </div>\n <div class=\"osl-card-body\">\n <div class=\"osl-skeleton osl-skeleton--card-field\"></div>\n <div class=\"osl-skeleton osl-skeleton--card-field\"></div>\n <div class=\"osl-skeleton osl-skeleton--card-field\"></div>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Empty state -->\n @else if (cardDatasource.length === 0 && !loading) {\n <div class=\"osl-card-empty\">\n <div class=\"d-flex align-items-center justify-content-center floating-element\">\n <div class=\"mx-4\">\n <p class=\"f-s-20 f-w-600 text-start\">No Records match the <br> current filters.</p>\n <p class=\"text-start\">Expecting to see new Records? Try again in <br> a few seconds as the system catches up</p>\n </div>\n <i class=\"icon\"></i>\n </div>\n </div>\n }\n\n <!-- Cards -->\n @else {\n <div class=\"osl-card-grid\">\n\n <!-- Custom card template -->\n @if (cardTemplate) {\n @for (row of cardDatasource; track row[primaryKey]; let i = $index) {\n <ng-container *ngTemplateOutlet=\"cardTemplate; context: { $implicit: row, index: i }\"></ng-container>\n }\n }\n\n <!-- Auto-generated card -->\n @else {\n @for (row of cardDatasource; track row[primaryKey]; let i = $index) {\n <div class=\"osl-card\"\n [class.osl-card--highlighted]=\"isHighlightedCard(row)\"\n [class.pointer]=\"isLister\"\n (click)=\"isLister ? onRowClick.emit(row) : null\">\n\n <!-- Card header: title + action buttons -->\n <div class=\"osl-card-header\">\n @if (cardTitleColumn) {\n <span class=\"osl-card-title\">{{ getCellValue(row, cardTitleColumn) }}</span>\n }\n\n @if (!isLister && (canEdit || canDelete || (moreMenuActions.length > 0 && hasVisibleActions(row)))) {\n <div class=\"osl-card-actions\">\n\n <!-- More-actions dropdown -->\n @if (moreMenuActions.length > 0 && hasVisibleActions(row)) {\n <div class=\"osl-menu-wrapper\">\n <button class=\"osl-grid-icon-btn\" (click)=\"toggleCardMenu(i, $event)\" title=\"More actions\">\n <mat-icon>more_vert</mat-icon>\n </button>\n @if (cardOpenMenuIndex === i) {\n <div class=\"osl-custom-menu\"\n [style.top.px]=\"cardMenuPosition.top\"\n [style.left.px]=\"cardMenuPosition.left\">\n <div class=\"osl-custom-menu-header\">Actions</div>\n @for (action of moreMenuActions; track $index) {\n @if (!action.hideIf || !action.hideIf(row)) {\n <button class=\"osl-custom-menu-item\"\n (click)=\"action.click(row); cardOpenMenuIndex = null; $event.stopPropagation()\">\n <span class=\"osl-custom-menu-dot\"></span>\n {{ action.labelIf ? action.labelIf(row) : action.label }}\n </button>\n }\n }\n </div>\n }\n </div>\n }\n\n @if (canEdit) {\n <button class=\"osl-grid-icon-btn\"\n (click)=\"$event.stopPropagation(); openEditDialog(row)\"\n title=\"Edit\">\n <mat-icon>mode_edit_outline</mat-icon>\n </button>\n }\n @if (canDelete) {\n <button class=\"osl-grid-icon-btn osl-grid-icon-btn--danger\"\n (click)=\"$event.stopPropagation(); onDeleteClick(row)\"\n title=\"Delete\">\n <mat-icon>delete_outline</mat-icon>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Card body: remaining column values -->\n <div class=\"osl-card-body\">\n @for (col of cardBodyColumns; track col.key) {\n <div class=\"osl-card-field\">\n <span class=\"osl-card-label\">{{ col.label }}</span>\n <span class=\"osl-card-value\">{{ getCellValue(row, col) }}</span>\n </div>\n }\n </div>\n\n </div>\n }\n }\n </div>\n\n <!-- Bottom spinner while loading next page -->\n @if (loading) {\n <div class=\"osl-card-loader\">\n <div class=\"osl-card-loader-spinner\"></div>\n </div>\n }\n\n <!-- All records loaded indicator -->\n @if (allCardsLoaded && !loading && cardDatasource.length > 0) {\n <div class=\"osl-card-all-loaded\">\n All {{ totalRecords || cardDatasource.length }} records loaded\n </div>\n }\n }\n </div>\n }\n\n</div>\n\n<!-- Dialog: Form Body -->\n<ng-template #formBodyTpl>\n <osl-dynamic-form\n [skeletonLoading]=\"formLoading\"\n [elements]=\"formElements\"\n [(model)]=\"dialogModel\"\n ></osl-dynamic-form>\n</ng-template>\n\n<!-- Dialog: Form Footer -->\n<ng-template #formFooterTpl>\n <div class=\"osl-setup-dialog-footer\">\n <osl-button [loading]=\"saveLoading\" variant=\"secondary\" label=\"Save\" (click)=\"saveDialog()\"></osl-button>\n </div>\n</ng-template>\n\n<!-- Wrapper that bridges DialogWrapper's implicit context to the custom footer template -->\n<ng-template #customFooterWrapperTpl let-data>\n <ng-container *ngTemplateOutlet=\"customFormFooter!; context: { $implicit: { dialogModel: dialogModel, dialogMode: dialogMode, dialogRef: data.dialogRef } }\"></ng-container>\n</ng-template>\n", styles: [".osl-setup-header{display:flex;align-items:center;justify-content:space-between}.osl-view-toggle{display:flex;border:1px solid var(--osl-border-color);border-radius:var(--osl-border-radius);overflow:hidden;flex-shrink:0}.osl-view-toggle-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;background:transparent;cursor:pointer;color:#6b7280;transition:background .12s,color .12s;padding:0}.osl-view-toggle-btn mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.osl-view-toggle-btn:first-child{border-right:1px solid var(--osl-border-color)}.osl-view-toggle-btn--active{background:var(--osl-primary);color:#fff}.osl-view-toggle-btn:not(.osl-view-toggle-btn--active):hover{background:#f3f4f6}.osl-card-container{overflow-y:auto;overflow-x:hidden;border:1px solid var(--osl-border-color);border-radius:var(--osl-border-radius);background:#f9fafb;padding:16px;box-sizing:border-box}.osl-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.osl-card{background:#fff;border:1px solid var(--osl-border-color);border-radius:var(--osl-border-radius);padding:16px;transition:box-shadow .15s,border-color .15s;display:flex;flex-direction:column}.osl-card:hover{box-shadow:0 4px 12px #00000014;border-color:#9ca3af}.osl-card--highlighted{box-shadow:inset 4px 0 0 var(--osl-primary)!important;animation:osl-card-highlight-fade 3s ease-out forwards}.osl-card--skeleton{pointer-events:none}.osl-card--skeleton:hover{box-shadow:none;border-color:var(--osl-border-color)}@keyframes osl-card-highlight-fade{0%,60%{background:#6366f10d}to{background:#fff}}.osl-card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid #f3f4f6}.osl-card-title{font-weight:600;font-size:14px;color:#111827;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0}.osl-card-body{display:flex;flex-direction:column;gap:8px;flex:1}.osl-card-field{display:flex;align-items:baseline;gap:8px;font-size:13px;line-height:1.4}.osl-card-label{color:#6b7280;font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.04em;min-width:90px;flex-shrink:0}.osl-card-value{color:#111827;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osl-card-empty{display:flex;align-items:center;justify-content:center;min-height:300px;height:100%}.osl-card-loader{display:flex;justify-content:center;align-items:center;padding:24px}.osl-card-loader-spinner{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:var(--osl-primary);border-radius:50%;animation:osl-spin .8s linear infinite}@keyframes osl-spin{to{transform:rotate(360deg)}}.osl-card-all-loaded{text-align:center;padding:16px;font-size:12px;color:#9ca3af;border-top:1px solid #f3f4f6;margin-top:8px}.osl-skeleton--card-title{height:16px;width:65%;background:#e5e7eb;border-radius:4px;animation:osl-skeleton-pulse 1.4s ease-in-out infinite}.osl-skeleton--card-field{height:12px;width:85%;background:#e5e7eb;border-radius:4px;animation:osl-skeleton-pulse 1.4s ease-in-out infinite}@keyframes osl-skeleton-pulse{0%,to{opacity:1}50%{opacity:.4}}.osl-setup-dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;width:100%}.dialog-cancel-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;border-color:var(--osl-border-color, #d1d5db);color:var(--osl-secondary, #6b7280);border-radius:var(--osl-border-radius, 4px);padding:0 16px;transition:all .2s ease}.dialog-cancel-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-cancel-btn:hover{border-color:#9ca3af;background:#f9fafb;color:#374151}.dialog-save-btn{display:flex;align-items:center;gap:6px;height:38px;font-size:13px;font-weight:500;background:linear-gradient(135deg,var(--osl-primary, #2563eb),#3b82f6);color:#fff;border-radius:var(--osl-border-radius, 4px);padding:0 18px;transition:all .2s ease;box-shadow:0 2px 8px #2563eb40}.dialog-save-btn mat-icon{font-size:17px;width:17px;height:17px}.dialog-save-btn:hover{background:linear-gradient(135deg,var(--osl-primary-hover, #1d4ed8),#2563eb);box-shadow:0 4px 14px #2563eb66;transform:translateY(-1px)}.dialog-save-btn:active{transform:translateY(0);box-shadow:0 2px 8px #2563eb40}\n"] }]
|
|
2826
3018
|
}], propDecorators: { formBodyTpl: [{
|
|
2827
3019
|
type: ViewChild,
|
|
2828
3020
|
args: ['formBodyTpl']
|
|
@@ -2835,6 +3027,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
2835
3027
|
}], searchbar: [{
|
|
2836
3028
|
type: ViewChild,
|
|
2837
3029
|
args: ['searchbar']
|
|
3030
|
+
}], gridRef: [{
|
|
3031
|
+
type: ViewChild,
|
|
3032
|
+
args: ['gridRef']
|
|
3033
|
+
}], cardContainerRef: [{
|
|
3034
|
+
type: ViewChild,
|
|
3035
|
+
args: ['cardContainerRef']
|
|
2838
3036
|
}], title: [{
|
|
2839
3037
|
type: Input,
|
|
2840
3038
|
args: ['title']
|
|
@@ -2904,6 +3102,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
2904
3102
|
}], primaryKey: [{
|
|
2905
3103
|
type: Input,
|
|
2906
3104
|
args: ['primaryKey']
|
|
3105
|
+
}], onSave: [{
|
|
3106
|
+
type: Input,
|
|
3107
|
+
args: ['onSave']
|
|
3108
|
+
}], cardPageSize: [{
|
|
3109
|
+
type: Input,
|
|
3110
|
+
args: ['cardPageSize']
|
|
3111
|
+
}], cardTemplate: [{
|
|
3112
|
+
type: Input,
|
|
3113
|
+
args: ['cardTemplate']
|
|
2907
3114
|
}], onSearch: [{
|
|
2908
3115
|
type: Output
|
|
2909
3116
|
}], onAdd: [{
|
|
@@ -2922,12 +3129,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
2922
3129
|
type: Output
|
|
2923
3130
|
}], onStateRestored: [{
|
|
2924
3131
|
type: Output
|
|
2925
|
-
}],
|
|
2926
|
-
type:
|
|
2927
|
-
args: ['
|
|
2928
|
-
}], onSave: [{
|
|
2929
|
-
type: Input,
|
|
2930
|
-
args: ['onSave']
|
|
3132
|
+
}], onDocumentClick: [{
|
|
3133
|
+
type: HostListener,
|
|
3134
|
+
args: ['document:click']
|
|
2931
3135
|
}] } });
|
|
2932
3136
|
|
|
2933
3137
|
class OslFormGrid {
|
|
@@ -3148,7 +3352,7 @@ class OslAutocompleteLister {
|
|
|
3148
3352
|
this.dialogRef.close(event);
|
|
3149
3353
|
}
|
|
3150
3354
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslAutocompleteLister, deps: [{ token: i1.MatDialogRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
3151
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: OslAutocompleteLister, isStandalone: false, selector: "osl-autocomplete-lister", inputs: { data: "data" }, ngImport: i0, template: "<div class=\"p-4\">\r\n <osl-setup [loading]=\"loader\" [autoMode]=\"false\" [title]=\"autocompleteData?.title\" [totalRecords]=\"recordCount\" (pageSizeChange)=\"onPageChange($event)\" (pageChange)=\"onPageChange($event)\" (onRowClick)=\"onRowClick($event)\" [columns]=\"column\" [datasource]=\"datasource\" [isLister]=\"true\"></osl-setup>\r\n \r\n</div>", styles: [""], dependencies: [{ kind: "component", type: OslSetup, selector: "osl-setup", inputs: ["title", "columns", "datasource", "isPaginated", "pageSize", "autoMode", "tableHeight", "totalRecords", "loading", "dialogWidth", "formElements", "beforeDisplay", "onAddEditFn", "isLister", "canAdd", "canEdit", "canDelete", "moreMenuActions", "customFormFooter", "customHeaderTemp", "partialCustomHeaderTemp", "stateKey", "primaryKey", "onSave"], outputs: ["onSearch", "onAdd", "onEdit", "onDelete", "pageChange", "pageSizeChange", "sortChange", "onRowClick", "onStateRestored"] }] });
|
|
3355
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: OslAutocompleteLister, isStandalone: false, selector: "osl-autocomplete-lister", inputs: { data: "data" }, ngImport: i0, template: "<div class=\"p-4\">\r\n <osl-setup [loading]=\"loader\" [autoMode]=\"false\" [title]=\"autocompleteData?.title\" [totalRecords]=\"recordCount\" (pageSizeChange)=\"onPageChange($event)\" (pageChange)=\"onPageChange($event)\" (onRowClick)=\"onRowClick($event)\" [columns]=\"column\" [datasource]=\"datasource\" [isLister]=\"true\"></osl-setup>\r\n \r\n</div>", styles: [""], dependencies: [{ kind: "component", type: OslSetup, selector: "osl-setup", inputs: ["title", "columns", "datasource", "isPaginated", "pageSize", "autoMode", "tableHeight", "totalRecords", "loading", "dialogWidth", "formElements", "beforeDisplay", "onAddEditFn", "isLister", "canAdd", "canEdit", "canDelete", "moreMenuActions", "customFormFooter", "customHeaderTemp", "partialCustomHeaderTemp", "stateKey", "primaryKey", "onSave", "cardPageSize", "cardTemplate"], outputs: ["onSearch", "onAdd", "onEdit", "onDelete", "pageChange", "pageSizeChange", "sortChange", "onRowClick", "onStateRestored"] }] });
|
|
3152
3356
|
}
|
|
3153
3357
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: OslAutocompleteLister, decorators: [{
|
|
3154
3358
|
type: Component,
|