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 = "50vw";
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.gridRef) {
2660
- this.gridRef.currentPage = state.page;
2661
- this.gridRef.pageSize = state.pageSize;
2662
- this.gridRef.setRestorePage(state.page);
2663
- }
2664
- if (this.searchbar && state.searchValue) {
2665
- this._isRestoring = true;
2666
- this.searchbar.searchControl.setValue(state.searchValue, { emitEvent: false });
2667
- }
2668
- // Trigger the same code path as a real search so the parent re-fetches with the correct
2669
- // page + searchValue in a single unified event — no new events, no application changes needed.
2670
- if (state.searchValue) {
2671
- this.onSearchSetup(state.searchValue);
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
- // No search was active — just restore the page via pageChange.
2675
- this.pageChange.emit({ page: state.page, pageSize: state.pageSize, searchValue: '' });
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
- ngAfterViewInit() {
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
- // Called from state restore keep the saved page, don't reset to 1
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({ page: restoredPage, pageSize: this.gridRef?.pageSize ?? this.pageSize, searchValue: event });
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
- /** Prepends the actions column when formElements are provided. */
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.gridRef?.getScrollTop() ?? 0,
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(close => {
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
- }], gridRef: [{
2926
- type: ViewChild,
2927
- args: ['gridRef']
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,