ngx-thin-admin 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,961 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Component, EventEmitter, ChangeDetectorRef, Output, Input, Injectable, ViewChild } from '@angular/core';
3
+ import { lastValueFrom, Subject } from 'rxjs';
4
+ import { debounceTime } from 'rxjs/operators';
5
+ import * as i4 from '@angular/material/button';
6
+ import { MatButtonModule, MatIconButton, MatButton } from '@angular/material/button';
7
+ import { MatIcon } from '@angular/material/icon';
8
+ import * as i2 from '@angular/material/input';
9
+ import { MatInputModule, MatInput, MatPrefix, MatSuffix } from '@angular/material/input';
10
+ import { MatFormField } from '@angular/material/select';
11
+ import { MatTooltip } from '@angular/material/tooltip';
12
+ import * as i2$1 from '@ng-matero/extensions/grid';
13
+ import { MtxGridModule } from '@ng-matero/extensions/grid';
14
+ import * as i1$1 from '@angular/forms';
15
+ import { FormsModule, FormGroup, ReactiveFormsModule } from '@angular/forms';
16
+ import { MatSelectionList, MatListOption } from '@angular/material/list';
17
+ import { MatMenu, MatMenuItem, MatMenuTrigger, MatMenuContent } from '@angular/material/menu';
18
+ import { MatSnackBar } from '@angular/material/snack-bar';
19
+ import { MatDialogRef, MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog } from '@angular/material/dialog';
20
+ import * as i1 from '@angular/material/form-field';
21
+ import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
22
+ import { stringify } from 'csv-stringify/browser/esm/sync';
23
+ import { RouterLink } from '@angular/router';
24
+ import { MatCard, MatCardHeader, MatCardContent } from '@angular/material/card';
25
+ import { FieldWrapper, FormlyForm, provideFormlyCore } from '@ngx-formly/core';
26
+ import { withFormlyMaterial } from '@ngx-formly/material';
27
+ import { MatProgressBar } from '@angular/material/progress-bar';
28
+
29
+ class CategoryEditorDialog {
30
+ dialogRef = inject((MatDialogRef));
31
+ data = inject(MAT_DIALOG_DATA);
32
+ categoryLabel;
33
+ save() {
34
+ if (!this.categoryLabel) {
35
+ return;
36
+ }
37
+ this.dialogRef.close({
38
+ label: this.categoryLabel,
39
+ });
40
+ }
41
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CategoryEditorDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
42
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: CategoryEditorDialog, isStandalone: true, selector: "lib-category-editor-dialog", ngImport: i0, template: "<h2 mat-dialog-title>\n @if (data.editCategory) {\n Edit {{ data.config.singularLabel ?? 'Category' }}: \"{{ data.editCategory.label }}\"\n } @else {\n Create {{ data.config.singularLabel ?? 'Category' }}\n }\n</h2>\n<form (ngSubmit)=\"save()\">\n <mat-dialog-content>\n <mat-form-field>\n <mat-label>{{ data.config.exampleLabel ?? data.config.singularLabel ?? '' }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"categoryLabel\"\n name=\"categoryLabel\"\n cdkFocusInitial\n [value]=\"data.editCategory ? data.editCategory.label : ''\"\n />\n </mat-form-field>\n </mat-dialog-content>\n <mat-dialog-actions>\n <button type=\"button\" class=\"cancel-btn\" matButton [mat-dialog-close]=\"undefined\">\n {{ data.cancelButtonText ?? 'Cancel' }}\n </button>\n <button type=\"submit\" matButton>{{ data.positiveButtonText ?? 'Save' }}</button>\n </mat-dialog-actions>\n</form>\n", styles: [".cancel-btn{--mat-button-text-label-text-color: #333}\n"], dependencies: [{ kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }] });
43
+ }
44
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CategoryEditorDialog, decorators: [{
45
+ type: Component,
46
+ args: [{ selector: 'lib-category-editor-dialog', imports: [
47
+ MatFormFieldModule,
48
+ MatInputModule,
49
+ FormsModule,
50
+ MatButtonModule,
51
+ MatDialogTitle,
52
+ MatDialogContent,
53
+ MatDialogActions,
54
+ MatDialogClose,
55
+ ], template: "<h2 mat-dialog-title>\n @if (data.editCategory) {\n Edit {{ data.config.singularLabel ?? 'Category' }}: \"{{ data.editCategory.label }}\"\n } @else {\n Create {{ data.config.singularLabel ?? 'Category' }}\n }\n</h2>\n<form (ngSubmit)=\"save()\">\n <mat-dialog-content>\n <mat-form-field>\n <mat-label>{{ data.config.exampleLabel ?? data.config.singularLabel ?? '' }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"categoryLabel\"\n name=\"categoryLabel\"\n cdkFocusInitial\n [value]=\"data.editCategory ? data.editCategory.label : ''\"\n />\n </mat-form-field>\n </mat-dialog-content>\n <mat-dialog-actions>\n <button type=\"button\" class=\"cancel-btn\" matButton [mat-dialog-close]=\"undefined\">\n {{ data.cancelButtonText ?? 'Cancel' }}\n </button>\n <button type=\"submit\" matButton>{{ data.positiveButtonText ?? 'Save' }}</button>\n </mat-dialog-actions>\n</form>\n", styles: [".cancel-btn{--mat-button-text-label-text-color: #333}\n"] }]
56
+ }] });
57
+
58
+ class ConfirmDialog {
59
+ dialogRef = inject((MatDialogRef));
60
+ data = inject(MAT_DIALOG_DATA);
61
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ConfirmDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
62
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.5", type: ConfirmDialog, isStandalone: true, selector: "lib-confirm-dialog", ngImport: i0, template: "<h2 mat-dialog-title>{{ data.title }}</h2>\n<mat-dialog-content>\n {{ data.message }}\n</mat-dialog-content>\n<mat-dialog-actions>\n <button class=\"cancel-btn\" matButton [mat-dialog-close]=\"undefined\">\n {{ data.cancelButtonText || 'Cancel' }}\n </button>\n <button matButton [mat-dialog-close]=\"true\" cdkFocusInitial>\n {{ data.positiveButtonText || 'OK' }}\n </button>\n</mat-dialog-actions>\n", styles: [".cancel-btn{--mat-button-text-label-text-color: #333}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }] });
63
+ }
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ConfirmDialog, decorators: [{
65
+ type: Component,
66
+ args: [{ selector: 'lib-confirm-dialog', imports: [
67
+ FormsModule,
68
+ MatButtonModule,
69
+ MatDialogTitle,
70
+ MatDialogContent,
71
+ MatDialogActions,
72
+ MatDialogClose,
73
+ ], template: "<h2 mat-dialog-title>{{ data.title }}</h2>\n<mat-dialog-content>\n {{ data.message }}\n</mat-dialog-content>\n<mat-dialog-actions>\n <button class=\"cancel-btn\" matButton [mat-dialog-close]=\"undefined\">\n {{ data.cancelButtonText || 'Cancel' }}\n </button>\n <button matButton [mat-dialog-close]=\"true\" cdkFocusInitial>\n {{ data.positiveButtonText || 'OK' }}\n </button>\n</mat-dialog-actions>\n", styles: [".cancel-btn{--mat-button-text-label-text-color: #333}\n"] }]
74
+ }] });
75
+
76
+ class ListCategorySelector {
77
+ /**
78
+ * Config for the category selector
79
+ */
80
+ config;
81
+ /**
82
+ * Selected category. This will be updated when the user selects a category from the list.
83
+ */
84
+ selectedCategory;
85
+ /**
86
+ * Event emitted when the selected category changes.
87
+ */
88
+ selectedCategoryChange = new EventEmitter();
89
+ /**
90
+ * Categories to be displayed in the category selector.
91
+ */
92
+ categories = [];
93
+ /**
94
+ * Services
95
+ */
96
+ snackbar = inject(MatSnackBar);
97
+ dialog = inject(MatDialog);
98
+ cdr = inject(ChangeDetectorRef);
99
+ ngOnChanges(changes) {
100
+ if (changes.config &&
101
+ changes.config.currentValue?.fetcher !== changes.config.previousValue?.fetcher) {
102
+ this.fetchCategories();
103
+ }
104
+ }
105
+ async fetchCategories() {
106
+ if (!this.config?.fetcher) {
107
+ return;
108
+ }
109
+ else if (!(this.config.fetcher instanceof Function)) {
110
+ throw new Error('categoryFetcher must be a function');
111
+ }
112
+ console.log('Fetching categories');
113
+ try {
114
+ const result = await Promise.resolve(this.config.fetcher());
115
+ console.log(result);
116
+ this.categories = result.categories;
117
+ this.cdr.markForCheck();
118
+ console.log('Fetched categories:', this.categories);
119
+ }
120
+ catch (e) {
121
+ console.error('Error fetching categories:', e);
122
+ }
123
+ }
124
+ async createCategory(categoryLabel) {
125
+ if (!this.config?.creator) {
126
+ return;
127
+ }
128
+ // Call the category creator function
129
+ try {
130
+ await this.config.creator(categoryLabel);
131
+ }
132
+ catch (e) {
133
+ this.snackbar.open(`Error: ${e?.message ?? 'Could not create category'}`, undefined, {
134
+ duration: 3000,
135
+ });
136
+ }
137
+ // Refresh category list
138
+ this.fetchCategories();
139
+ }
140
+ async updateCategory(category) {
141
+ if (!this.config?.updater) {
142
+ return;
143
+ }
144
+ // Call the category updater function
145
+ try {
146
+ await this.config.updater(category);
147
+ }
148
+ catch (e) {
149
+ this.snackbar.open(`Error: ${e?.message ?? `Failed to update category "${category.label}"`}`, undefined, {
150
+ duration: 3000,
151
+ });
152
+ }
153
+ // Refresh category list
154
+ this.fetchCategories();
155
+ }
156
+ async deleteCategory(category) {
157
+ if (!this.config?.deleter) {
158
+ return;
159
+ }
160
+ // Call the category deleter function
161
+ try {
162
+ await this.config.deleter(category);
163
+ }
164
+ catch (e) {
165
+ this.snackbar.open(`Error: ${e?.message ?? `Failed to delete category: "${category.label}"`}`, undefined, {
166
+ duration: 3000,
167
+ });
168
+ }
169
+ // Refresh category list
170
+ this.fetchCategories();
171
+ }
172
+ async openCategoryEditorDialog(editCategory) {
173
+ // Open a dialog for category creation / editing
174
+ const dialogRef = this.dialog.open(CategoryEditorDialog, {
175
+ width: '400px',
176
+ data: {
177
+ config: this.config,
178
+ editCategory: editCategory,
179
+ positiveButtonText: editCategory ? 'Save' : 'Create',
180
+ cancelButtonText: 'Cancel',
181
+ },
182
+ });
183
+ const result = await lastValueFrom(dialogRef.afterClosed());
184
+ if (!result || !result.label) {
185
+ return;
186
+ }
187
+ // Call the category creator / updater function
188
+ try {
189
+ if (editCategory) {
190
+ await this.updateCategory({ ...editCategory, label: result.label });
191
+ }
192
+ else {
193
+ await this.createCategory(result.label);
194
+ }
195
+ }
196
+ catch (e) {
197
+ this.snackbar.open(`Error: ${e?.message ?? 'Could not save category'}`, undefined, {
198
+ duration: 3000,
199
+ });
200
+ }
201
+ // Refresh category list
202
+ this.fetchCategories();
203
+ }
204
+ async openCategoryDeletionDialog(category) {
205
+ // Open a dialog for category deletion
206
+ const dialogRef = this.dialog.open(ConfirmDialog, {
207
+ width: '400px',
208
+ data: {
209
+ title: 'Confirm Deletion',
210
+ message: `Are you sure you want to delete the ${this.config?.singularLabel ?? 'category'} "${category.label}"? This action cannot be undone.`,
211
+ positiveButtonText: 'Delete',
212
+ cancelButtonText: 'Cancel',
213
+ },
214
+ });
215
+ const result = await lastValueFrom(dialogRef.afterClosed());
216
+ if (!result) {
217
+ return;
218
+ }
219
+ // Call the category deleter function
220
+ try {
221
+ await this.deleteCategory(category);
222
+ }
223
+ catch (e) {
224
+ this.snackbar.open(`Error: ${e?.message ?? `Failed to delete category "${category.label}"`}`, undefined, {
225
+ duration: 3000,
226
+ });
227
+ }
228
+ // Refresh category list
229
+ this.fetchCategories();
230
+ }
231
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ListCategorySelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
232
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: ListCategorySelector, isStandalone: true, selector: "lib-list-category-selector", inputs: { config: "config", selectedCategory: "selectedCategory" }, outputs: { selectedCategoryChange: "selectedCategoryChange" }, usesOnChanges: true, ngImport: i0, template: "<header>\n <!-- Category selector title -->\n <span class=\"category-selector-title\"> {{ config?.title ?? 'Categories' }} </span>\n <!---->\n\n <!-- Category Creation button -->\n @if (config?.creator) {\n <button\n mat-icon-button\n (click)=\"openCategoryEditorDialog(undefined)\"\n class=\"create-category-button\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n <!---->\n</header>\n\n<mat-selection-list\n #categorySelector\n [multiple]=\"false\"\n [hideSingleSelectionIndicator]=\"true\"\n [(ngModel)]=\"selectedCategory\"\n (selectionChange)=\"this.selectedCategoryChange.emit(selectedCategory)\"\n>\n <!-- \"All\" Category -->\n <mat-list-option\n [value]=\"undefined\"\n [class.active]=\"selectedCategory && selectedCategory[0] === undefined\"\n >\n <div matListItemTitle>\n <!-- Category label -->\n All&nbsp;\n <!---->\n </div>\n </mat-list-option>\n <!---->\n\n <!-- Each category -->\n @for (category of categories; track category.id) {\n <mat-list-option\n [value]=\"category\"\n [class.active]=\"selectedCategory && selectedCategory[0]?.id === category.id\"\n >\n <div matListItemTitle>\n <!-- Category label -->\n {{ category.label }}&nbsp;\n <!---->\n\n <!-- Count of items in the category -->\n @if (category.itemCount !== undefined) {\n <small> ({{ category.itemCount }}) </small>\n }\n <!---->\n </div>\n\n <!-- Category menu button -->\n @if (\n selectedCategory &&\n selectedCategory[0] !== undefined &&\n selectedCategory[0].id === category.id &&\n (config?.updater || config?.deleter)\n ) {\n <button\n matIconButton\n [matMenuTriggerFor]=\"menu\"\n [matMenuTriggerData]=\"{ category: category }\"\n class=\"category-menu-button\"\n >\n <mat-icon>more_vert</mat-icon>\n </button>\n }\n <!---->\n </mat-list-option>\n }\n <!---->\n</mat-selection-list>\n\n<!-- Category Menu -->\n<mat-menu #menu=\"matMenu\">\n <ng-template matMenuContent let-category=\"category\">\n @if (config?.updater) {\n <button mat-menu-item (click)=\"openCategoryEditorDialog(category)\">Edit</button>\n }\n @if (config?.deleter) {\n <button mat-menu-item (click)=\"openCategoryDeletionDialog(category)\">Delete</button>\n }\n </ng-template>\n</mat-menu>\n<!---->\n", styles: [":host{display:block;border-radius:9px;border:1px solid rgba(198,198,201,.1254901961)}header{display:flex;align-items:center;border-bottom:1px solid #f5f6f8;justify-content:space-between;padding:0 .2rem 0 .1rem}header .category-selector-title{font-size:.9rem;padding:1rem .8rem}header button{transform:translateY(-1px)}header button mat-icon{color:#496487}mat-selection-list mat-list-option div[matListItemTitle]{font-size:.85rem;position:relative}mat-selection-list mat-list-option.active{background-color:#f0f0f0}mat-selection-list mat-list-option.active div[matListItemTitle]{color:#3d649c}mat-selection-list mat-list-option:hover{cursor:pointer;background-color:#f0f0f0}mat-selection-list mat-list-option .category-menu-button{position:absolute;right:0;top:0;height:100%;padding-top:.8rem;vertical-align:middle;z-index:1}mat-selection-list mat-list-option{--mat-icon-button-container-shape: 1px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatSelectionList, selector: "mat-selection-list", inputs: ["color", "compareWith", "multiple", "hideSingleSelectionIndicator", "disabled"], outputs: ["selectionChange"], exportAs: ["matSelectionList"] }, { kind: "component", type: MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: MatListOption, selector: "mat-list-option", inputs: ["togglePosition", "checkboxPosition", "color", "value", "selected"], outputs: ["selectedChange"], exportAs: ["matListOption"] }, { kind: "directive", type: MatMenuContent, selector: "ng-template[matMenuContent]" }] });
233
+ }
234
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ListCategorySelector, decorators: [{
235
+ type: Component,
236
+ args: [{ selector: 'lib-list-category-selector', imports: [
237
+ FormsModule,
238
+ MatIcon,
239
+ MatIconButton,
240
+ MatSelectionList,
241
+ MatMenu,
242
+ MatMenuItem,
243
+ MatMenuTrigger,
244
+ MatListOption,
245
+ MatMenuContent,
246
+ ], template: "<header>\n <!-- Category selector title -->\n <span class=\"category-selector-title\"> {{ config?.title ?? 'Categories' }} </span>\n <!---->\n\n <!-- Category Creation button -->\n @if (config?.creator) {\n <button\n mat-icon-button\n (click)=\"openCategoryEditorDialog(undefined)\"\n class=\"create-category-button\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n <!---->\n</header>\n\n<mat-selection-list\n #categorySelector\n [multiple]=\"false\"\n [hideSingleSelectionIndicator]=\"true\"\n [(ngModel)]=\"selectedCategory\"\n (selectionChange)=\"this.selectedCategoryChange.emit(selectedCategory)\"\n>\n <!-- \"All\" Category -->\n <mat-list-option\n [value]=\"undefined\"\n [class.active]=\"selectedCategory && selectedCategory[0] === undefined\"\n >\n <div matListItemTitle>\n <!-- Category label -->\n All&nbsp;\n <!---->\n </div>\n </mat-list-option>\n <!---->\n\n <!-- Each category -->\n @for (category of categories; track category.id) {\n <mat-list-option\n [value]=\"category\"\n [class.active]=\"selectedCategory && selectedCategory[0]?.id === category.id\"\n >\n <div matListItemTitle>\n <!-- Category label -->\n {{ category.label }}&nbsp;\n <!---->\n\n <!-- Count of items in the category -->\n @if (category.itemCount !== undefined) {\n <small> ({{ category.itemCount }}) </small>\n }\n <!---->\n </div>\n\n <!-- Category menu button -->\n @if (\n selectedCategory &&\n selectedCategory[0] !== undefined &&\n selectedCategory[0].id === category.id &&\n (config?.updater || config?.deleter)\n ) {\n <button\n matIconButton\n [matMenuTriggerFor]=\"menu\"\n [matMenuTriggerData]=\"{ category: category }\"\n class=\"category-menu-button\"\n >\n <mat-icon>more_vert</mat-icon>\n </button>\n }\n <!---->\n </mat-list-option>\n }\n <!---->\n</mat-selection-list>\n\n<!-- Category Menu -->\n<mat-menu #menu=\"matMenu\">\n <ng-template matMenuContent let-category=\"category\">\n @if (config?.updater) {\n <button mat-menu-item (click)=\"openCategoryEditorDialog(category)\">Edit</button>\n }\n @if (config?.deleter) {\n <button mat-menu-item (click)=\"openCategoryDeletionDialog(category)\">Delete</button>\n }\n </ng-template>\n</mat-menu>\n<!---->\n", styles: [":host{display:block;border-radius:9px;border:1px solid rgba(198,198,201,.1254901961)}header{display:flex;align-items:center;border-bottom:1px solid #f5f6f8;justify-content:space-between;padding:0 .2rem 0 .1rem}header .category-selector-title{font-size:.9rem;padding:1rem .8rem}header button{transform:translateY(-1px)}header button mat-icon{color:#496487}mat-selection-list mat-list-option div[matListItemTitle]{font-size:.85rem;position:relative}mat-selection-list mat-list-option.active{background-color:#f0f0f0}mat-selection-list mat-list-option.active div[matListItemTitle]{color:#3d649c}mat-selection-list mat-list-option:hover{cursor:pointer;background-color:#f0f0f0}mat-selection-list mat-list-option .category-menu-button{position:absolute;right:0;top:0;height:100%;padding-top:.8rem;vertical-align:middle;z-index:1}mat-selection-list mat-list-option{--mat-icon-button-container-shape: 1px}\n"] }]
247
+ }], propDecorators: { config: [{
248
+ type: Input
249
+ }], selectedCategory: [{
250
+ type: Input
251
+ }], selectedCategoryChange: [{
252
+ type: Output
253
+ }] } });
254
+
255
+ class CsvExportService {
256
+ snackbar = inject(MatSnackBar);
257
+ encoders = [
258
+ {
259
+ key: 'utf8',
260
+ label: 'UTF-8',
261
+ mimeCharset: 'UTF-8',
262
+ encoder: undefined,
263
+ },
264
+ ];
265
+ constructor() {
266
+ this.loadOptionalEncoders();
267
+ }
268
+ async loadOptionalEncoders() {
269
+ // Optional charset support for Shift_JIS (commonly used in Japan)
270
+ try {
271
+ // @ts-ignore
272
+ const Encoding = (await import('encoding-japanese'));
273
+ this.encoders.push({
274
+ key: 'sjis',
275
+ label: 'Shift_JIS',
276
+ mimeCharset: 'Shift_JIS',
277
+ encoder: (input) => {
278
+ const sjisArrayBuffer = Encoding.convert(input, {
279
+ from: 'UNICODE',
280
+ to: 'SJIS',
281
+ type: 'arraybuffer',
282
+ });
283
+ return new Uint8Array(sjisArrayBuffer).buffer;
284
+ },
285
+ });
286
+ }
287
+ catch (error) {
288
+ console.error('Shift_JIS encoder is not available. If you need it, please install "encoding-japanese" package.', error);
289
+ // Do nothing
290
+ }
291
+ }
292
+ getAvailableEncoders() {
293
+ return this.encoders.map((encoder) => ({
294
+ key: encoder.key,
295
+ label: encoder.label,
296
+ }));
297
+ }
298
+ async exportListAsCsv(encoderKey, fileNamePrefix, fetcher, listQuery, columns) {
299
+ const CSV_EXPORT_MAX_PAGE_INDEX = 1000;
300
+ const CSV_EXPORT_PAGE_SIZE = 25;
301
+ console.log('Starting CSV export', listQuery, encoderKey);
302
+ // Get encoder
303
+ const encoderInfo = this.encoders.find((e) => e.key === encoderKey);
304
+ if (!encoderInfo) {
305
+ console.error(`exportListAsCsv - Encoder not found for key: ${encoderKey}`);
306
+ return;
307
+ }
308
+ const mimeCharset = encoderInfo.mimeCharset;
309
+ const encoder = encoderInfo.encoder;
310
+ // Initialize
311
+ const items = [];
312
+ let errorMessage = undefined;
313
+ // Show processing message
314
+ const mes = this.snackbar.open('Exporting CSV...');
315
+ // Replace characters in the file name prefix that cannot be used in file names
316
+ fileNamePrefix = fileNamePrefix.replace(/[/¥\\*?:"<>|@\s;^.]/g, '_');
317
+ // Truncate the file name prefix if it is too long
318
+ const FILE_NAME_PREFIX_MAX_LENGTH = 100;
319
+ if (fileNamePrefix.length > FILE_NAME_PREFIX_MAX_LENGTH) {
320
+ fileNamePrefix = fileNamePrefix.substring(0, FILE_NAME_PREFIX_MAX_LENGTH);
321
+ }
322
+ // Generate file name
323
+ const now = new Date();
324
+ const year = now.getFullYear();
325
+ const month = String(now.getMonth() + 1).padStart(2, '0');
326
+ const date = String(now.getDate()).padStart(2, '0');
327
+ const hours = String(now.getHours()).padStart(2, '0');
328
+ const minutes = String(now.getMinutes()).padStart(2, '0');
329
+ const seconds = String(now.getSeconds()).padStart(2, '0');
330
+ const datetimeString = `${year}${month}${date}_${hours}${minutes}${seconds}`;
331
+ const csvFileName = `${fileNamePrefix}_${datetimeString}.csv`;
332
+ // Fetch items page by page
333
+ let numOfExportedItems = 0;
334
+ for (let pageIndex = 0; pageIndex < CSV_EXPORT_MAX_PAGE_INDEX; pageIndex++) {
335
+ const query = {
336
+ ...listQuery,
337
+ page: pageIndex,
338
+ perPage: CSV_EXPORT_PAGE_SIZE,
339
+ };
340
+ let result;
341
+ try {
342
+ result = await fetcher(query);
343
+ }
344
+ catch (error) {
345
+ console.error(`exportListAsCsv - Error fetching data:`, error);
346
+ errorMessage = error.message || 'Unknown error';
347
+ break;
348
+ }
349
+ // Wait for few seconds
350
+ await new Promise((resolve) => setTimeout(resolve, 100));
351
+ if (result.items.length === 0) {
352
+ // No items, exit
353
+ break;
354
+ }
355
+ // Add fetched items
356
+ items.push(...result.items);
357
+ numOfExportedItems += result.items.length;
358
+ }
359
+ // Determine CSV columns
360
+ const csvColumns = columns
361
+ .filter((column) => {
362
+ if (column.type === 'button') {
363
+ // Exclude columns that are buttons
364
+ return false;
365
+ }
366
+ return true;
367
+ })
368
+ .map((column) => {
369
+ // Get the column key (e.g., "name")
370
+ const columnKey = column.field;
371
+ // Get the column header string (e.g., "User Name")
372
+ let columnHeader;
373
+ if (typeof column.header === 'string') {
374
+ columnHeader = column.header;
375
+ }
376
+ else {
377
+ columnHeader = columnKey;
378
+ }
379
+ // Return column information for csv-stringify
380
+ return {
381
+ key: column.field,
382
+ header: columnHeader,
383
+ };
384
+ })
385
+ .filter((column) => column !== null);
386
+ // Convert to CSV
387
+ const csv = stringify(items, {
388
+ header: true,
389
+ columns: csvColumns,
390
+ });
391
+ // Generate Blob
392
+ let blob = undefined;
393
+ if (encoder) {
394
+ const encodedData = encoder(csv);
395
+ if (encodedData) {
396
+ blob = new Blob([encodedData], { type: `text/csv; charset=${mimeCharset}` });
397
+ }
398
+ else {
399
+ console.error(`exportListAsCsv - Encoding failed for encoder: ${encoderKey}`);
400
+ }
401
+ }
402
+ else {
403
+ blob = new Blob([csv], { type: `text/csv; charset=${mimeCharset}` });
404
+ }
405
+ if (!blob) {
406
+ console.error(`exportListAsCsv - Blob is not created`);
407
+ mes.dismiss();
408
+ this.snackbar.open(`Failed to generate CSV file.`, undefined, {
409
+ duration: 5000,
410
+ });
411
+ return;
412
+ }
413
+ // Download as CSV
414
+ const objectUrl = URL.createObjectURL(blob);
415
+ const a = document.createElement('a');
416
+ a.download = csvFileName;
417
+ a.href = objectUrl;
418
+ a.click();
419
+ // Revoke object URL after download
420
+ URL.revokeObjectURL(objectUrl);
421
+ // Dissmiss processing message
422
+ mes.dismiss();
423
+ // Show result message
424
+ if (errorMessage) {
425
+ this.snackbar.open(`CSV exported up to ${numOfExportedItems} items. but errors occurred: ${errorMessage}`, undefined, {
426
+ duration: 5000,
427
+ });
428
+ }
429
+ else {
430
+ this.snackbar.open(`CSV export completed. Exported ${numOfExportedItems} items.`, undefined, {
431
+ duration: 3000,
432
+ });
433
+ }
434
+ }
435
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CsvExportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
436
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CsvExportService, providedIn: 'root' });
437
+ }
438
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CsvExportService, decorators: [{
439
+ type: Injectable,
440
+ args: [{
441
+ providedIn: 'root',
442
+ }]
443
+ }], ctorParameters: () => [] });
444
+
445
+ class NgxThinAdminList {
446
+ /**
447
+ * Config for list
448
+ */
449
+ listConfig;
450
+ /**
451
+ * Config for category selector. If provided, a category selector will be displayed in the UI.
452
+ */
453
+ categorySelectorConfig;
454
+ /**
455
+ * Column schema (extended type of MtxGridColumn)
456
+ * @see https://ng-matero.github.io/extensions/components/grid/api
457
+ */
458
+ listColumns;
459
+ /**
460
+ * Query (Optional)
461
+ */
462
+ query = {
463
+ keyword: '',
464
+ page: 0,
465
+ perPage: 10,
466
+ categoryId: undefined,
467
+ };
468
+ /**
469
+ * Fetcher function to retrieve list data.
470
+ * This should be provided as part of listConfig.
471
+ */
472
+ listFetcher;
473
+ /**
474
+ * Internal state for grid
475
+ */
476
+ isLoading = false;
477
+ gridColumns = [];
478
+ gridData = [];
479
+ totalCount = 0;
480
+ /**
481
+ * Internal state for category list
482
+ */
483
+ categories = [];
484
+ selectedCategory = [];
485
+ /**
486
+ * Internal subject to trigger keyword change with debounce
487
+ */
488
+ keywordChanged$ = new Subject();
489
+ ngOnInit() {
490
+ this.keywordChanged$.pipe(debounceTime(500)).subscribe(() => {
491
+ this.fetchList();
492
+ });
493
+ }
494
+ /**
495
+ * Template for cells
496
+ */
497
+ cellTplWithLink;
498
+ /**
499
+ * Services
500
+ */
501
+ cdr = inject(ChangeDetectorRef);
502
+ csvExportService = inject(CsvExportService);
503
+ ngOnChanges(changes) {
504
+ // Columns
505
+ if (changes.listColumns?.currentValue) {
506
+ this.gridColumns = changes.listColumns.currentValue.map((col) => {
507
+ if (col.link || col.routerLink) {
508
+ return { ...col, cellTemplate: this.cellTplWithLink };
509
+ }
510
+ return col;
511
+ });
512
+ }
513
+ // List - trigger strictly when listFetcher is newly provided or explicitly changed
514
+ if (changes.listConfig?.currentValue?.fetcher !== changes.listConfig?.previousValue?.fetcher) {
515
+ this.listFetcher = changes.listConfig?.currentValue?.fetcher;
516
+ this.fetchList();
517
+ }
518
+ }
519
+ async fetchList() {
520
+ if (!this.listFetcher) {
521
+ return;
522
+ }
523
+ else if (!(this.listFetcher instanceof Function)) {
524
+ throw new Error('listFetcher must be a function');
525
+ }
526
+ console.log('Fetching data with query:', this.query);
527
+ this.isLoading = true;
528
+ try {
529
+ const result = await Promise.resolve(this.listFetcher(this.query));
530
+ this.gridData = result.items;
531
+ this.totalCount = result.totalCount;
532
+ }
533
+ catch (e) {
534
+ console.error('Error fetching list data:', e);
535
+ }
536
+ finally {
537
+ this.isLoading = false;
538
+ this.cdr.markForCheck();
539
+ }
540
+ }
541
+ clearFilters() {
542
+ this.query.keyword = '';
543
+ this.query.categoryId = undefined;
544
+ this.selectedCategory = [];
545
+ this.fetchList();
546
+ }
547
+ onKeywordInput() {
548
+ this.keywordChanged$.next();
549
+ }
550
+ async exportAsCsv(encoderKey) {
551
+ if (!this.listFetcher) {
552
+ return;
553
+ }
554
+ // Determine file name prefix
555
+ let fileNamePrefix = this.listConfig?.title ? this.listConfig.title : 'exported';
556
+ if (this.selectedCategory && this.selectedCategory[0]?.id) {
557
+ // If a category is selected, include the category label in the file name prefix
558
+ fileNamePrefix += `_${this.selectedCategory[0]?.label}`;
559
+ }
560
+ if (this.query.keyword && this.query.keyword.length > 0) {
561
+ // If a search keyword is entered, include it in the file name prefix
562
+ fileNamePrefix += `_${this.query.keyword}`;
563
+ }
564
+ // Execute export
565
+ await this.csvExportService.exportListAsCsv(encoderKey, fileNamePrefix, this.listFetcher, {
566
+ categoryId: this.selectedCategory ? this.selectedCategory[0]?.id : undefined,
567
+ keyword: this.query.keyword,
568
+ }, this.listColumns);
569
+ }
570
+ onPageChange(event) {
571
+ this.query.page = event.pageIndex;
572
+ this.query.perPage = event.pageSize;
573
+ this.fetchList();
574
+ }
575
+ onSelectedCategoryChange($event) {
576
+ console.log('Selected category changed:', $event);
577
+ const categoryId = $event && $event.length >= 1 && $event[0] ? $event[0].id : undefined;
578
+ this.query.categoryId = categoryId;
579
+ this.fetchList();
580
+ }
581
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminList, deps: [], target: i0.ɵɵFactoryTarget.Component });
582
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: NgxThinAdminList, isStandalone: true, selector: "ngx-thin-admin-list", inputs: { listConfig: "listConfig", categorySelectorConfig: "categorySelectorConfig", listColumns: "listColumns", query: "query" }, viewQueries: [{ propertyName: "cellTplWithLink", first: true, predicate: ["cellTplWithLink"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Category Selector -->\n@if (categorySelectorConfig) {\n <lib-list-category-selector\n [config]=\"categorySelectorConfig\"\n [(selectedCategory)]=\"selectedCategory\"\n (selectedCategoryChange)=\"onSelectedCategoryChange($event)\"\n ></lib-list-category-selector>\n}\n<!---->\n\n<!-- Toolbar Template -->\n<ng-template #toolbarTpl>\n <div class=\"custom-toolbar\">\n <!-- List Title -->\n <span class=\"list-title\">\n @if (listConfig?.title; as listTitle) {\n @if (query.keyword || query.categoryId) {\n <!-- Title (with link for reset filter) -->\n <a\n href=\"javascript:void(0)\"\n (click)=\"query.keyword = ''; query.categoryId = undefined; this.fetchList()\"\n matTooltip=\"Reset filters\"\n >{{ listTitle }}</a\n >\n <!---->\n } @else {\n <!-- Title -->\n {{ listTitle }}\n <!---->\n }\n } @else {\n <!-- Default Title -->\n List\n <!---->\n }\n\n @if (query.categoryId && selectedCategory?.[0]; as categoryLabel) {\n <span class=\"material-icons list-title-separator\"> keyboard_arrow_right </span>\n <!-- Category -->\n {{ categoryLabel.label }}\n <!---->\n }\n\n @if (query.keyword) {\n <span class=\"material-icons list-title-separator\"> keyboard_arrow_right </span>\n <!-- Keyword -->\n \"{{ query.keyword }}\"\n <!---->\n }\n </span>\n <!---->\n\n @if (query.keyword || query.categoryId) {\n <span style=\"width: 8px\"></span>\n <!-- Clear Filters Button -->\n <button\n mat-icon-button\n class=\"clear-filters-button\"\n matTooltip=\"Clear filters\"\n (click)=\"clearFilters()\"\n >\n <mat-icon>filter_alt_off</mat-icon>\n </button>\n <!---->\n }\n\n <span style=\"flex: 1\"></span>\n\n <!-- Create Button -->\n @if (listConfig?.createButton; as createBtn) {\n @if ($any(createBtn).routerLink; as routerLinkUrl) {\n <!-- Create Button with Router Link -->\n <a\n [routerLink]=\"routerLinkUrl\"\n mat-flat-button\n class=\"primary-button\"\n style=\"color: white; transform: translateY(-1px)\"\n >\n {{ createBtn.label }}\n <mat-icon style=\"margin-bottom: 1px\">add</mat-icon>\n </a>\n } @else if ($any(createBtn).link; as linkUrl) {\n <!-- Create Button with Link -->\n <a\n [href]=\"linkUrl\"\n mat-flat-button\n class=\"primary-button\"\n style=\"color: white; transform: translateY(-1px)\"\n >\n {{ createBtn.label }}\n <mat-icon style=\"margin-bottom: 1px\">add</mat-icon>\n </a>\n } @else if ($any(createBtn).click; as handler) {\n <!-- Create Button with Handler -->\n <button\n mat-flat-button\n class=\"primary-button\"\n style=\"color: white; transform: translateY(-1px)\"\n (click)=\"handler()\"\n >\n {{ createBtn?.label }}\n <mat-icon style=\"margin-bottom: 1px\">add</mat-icon>\n </button>\n }\n }\n <!---->\n\n <span style=\"width: 30px\"></span>\n\n <!-- Search Field -->\n <mat-form-field class=\"search-keyword xs-input\" appearance=\"outline\">\n <mat-icon matPrefix>search</mat-icon>\n <input\n matInput\n [placeholder]=\"\n selectedCategory?.[0]?.label\n ? 'Search in ' + selectedCategory?.[0]?.label + '...'\n : 'Search...'\n \"\n [(ngModel)]=\"query.keyword\"\n (keyup)=\"onKeywordInput()\"\n style=\"padding-top: 3px\"\n />\n\n <!-- Search clear button -->\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear search keyword\"\n matTooltip=\"Clear search keyword\"\n (click)=\"query.keyword = ''; this.fetchList()\"\n [style.visibility]=\"query.keyword ? 'visible' : 'hidden'\"\n >\n <mat-icon>highlight_off</mat-icon>\n </button>\n <!---->\n </mat-form-field>\n <!---->\n\n <span style=\"width: 8px\"></span>\n\n <!-- Refresh Button -->\n <button mat-icon-button matTooltip=\"Refresh\" (click)=\"fetchList()\">\n <mat-icon>refresh</mat-icon>\n </button>\n <!---->\n\n <span style=\"width: 5px\"></span>\n\n <!-- CSV Export Button -->\n <button mat-icon-button [matMenuTriggerFor]=\"csvExportMenu\" matTooltip=\"Export as CSV\">\n <mat-icon>download</mat-icon>\n </button>\n <!---->\n\n <!-- CSV Export Menu -->\n <mat-menu #csvExportMenu=\"matMenu\">\n @for (encoder of csvExportService.getAvailableEncoders(); track encoder.key) {\n <!-- Export with specific charset -->\n <p style=\"color: #333; font-size: 0.8rem; margin: 1rem 0.75rem 1rem 0.7rem\">\n CSV ({{ encoder.label }})\n </p>\n <button mat-menu-item (click)=\"exportAsCsv(encoder.key)\">\n <mat-icon>download</mat-icon>\n <span>Export {{ totalCount }} items</span>\n </button>\n <!---->\n }\n </mat-menu>\n <!---->\n </div>\n</ng-template>\n<!---->\n\n<!-- List (Grid) -->\n<mtx-grid\n [data]=\"gridData\"\n [columns]=\"gridColumns\"\n [showToolbar]=\"true\"\n [toolbarTemplate]=\"toolbarTpl\"\n [length]=\"totalCount\"\n [loading]=\"isLoading\"\n [pageOnFront]=\"false\"\n [pageIndex]=\"query.page\"\n [pageSize]=\"query.perPage\"\n [pageSizeOptions]=\"listConfig?.pageSizes ?? [10, 25, 50, 100]\"\n columnMenuButtonType=\"icon\"\n columnMenuButtonClass=\"column-menu-button\"\n columnMenuButtonIcon=\"view_column\"\n (page)=\"onPageChange($event)\"\n></mtx-grid>\n<!---->\n\n<!-- Cell Template with Link -->\n<ng-template #cellTplWithLink let-row let-index=\"index\" let-col=\"colDef\">\n @if (col.link) {\n <a [href]=\"col.link(row)\">\n {{ row[col.field] }}\n </a>\n } @else if (col.routerLink) {\n <a [routerLink]=\"col.routerLink(row)\">\n {{ row[col.field] }}\n </a>\n } @else {\n {{ row[col.field] }}\n }\n</ng-template>\n<!---->\n", styles: [":host{display:flex;gap:1rem;padding:1rem;box-sizing:border-box}lib-list-category-selector{display:block;width:200px;background-color:#fcfcfc;border-radius:9px;padding:.5rem;box-sizing:border-box}lib-list-category-selector .category-list{max-height:300px;overflow-y:auto}::ng-deep .mtx-grid-toolbar{display:block;background-color:#fcfcfc;border-top-left-radius:9px;border-top-right-radius:9px;margin-bottom:1px}::ng-deep .mtx-grid-toolbar .custom-toolbar{display:flex;align-items:center;justify-content:space-between;height:37px}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title{font-size:1rem;line-height:37px}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title a{color:var(--mat-theme-primary)}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title a:hover{text-decoration:underline}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title .list-title-separator{color:#aaa;vertical-align:middle;margin:auto .2rem 4px}::ng-deep .mtx-grid-toolbar .custom-toolbar .clear-filters-button{color:var(--mat-sys-on-primary-container);min-width:32px;padding:0;vertical-align:middle}::ng-deep .mtx-grid-toolbar .custom-toolbar .clear-filters-button mat-icon{vertical-align:middle;margin-bottom:3px}::ng-deep .mtx-grid-toolbar .custom-toolbar .search-keyword{width:15rem;--mat-form-field-container-height: 18.5px;--mat-form-field-container-text-line-height: 18.5px;--mat-form-field-container-vertical-padding: 8.5px;--mat-form-field-outlined-container-shape: 28px;--mat-form-field-subscript-text-line-height: 0px}::ng-deep .mtx-grid-toolbar .custom-toolbar button{color:var(--mat-sys-on-primary-container)}::ng-deep .mtx-grid-toolbar ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}::ng-deep .mtx-grid-toolbar ::ng-deep mtx-grid-column-menu button[mat-icon-button]{color:var(--mat-sys-on-primary-container);margin-right:.5rem}::ng-deep .mtx-grid-main{background-color:#fcfcfc}table{width:100%}::ng-deep table thead,::ng-deep table tbody{background-color:#fcfcfc!important}::ng-deep .mtx-grid-footer,::ng-deep .mat-mdc-paginator{display:block;background-color:#fcfcfc!important;border-bottom-left-radius:9px!important;border-bottom-right-radius:9px!important}a,::ng-deep table a,::ng-deep table a:visited{color:var(--mat-sys-on-primary-container);text-decoration:none}a:hover,::ng-deep table a:hover,::ng-deep table a:visited:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MtxGridModule }, { kind: "component", type: i2$1.MtxGrid, selector: "mtx-grid", inputs: ["displayedColumns", "columns", "data", "length", "loading", "trackBy", "columnResizable", "emptyValuePlaceholder", "pageOnFront", "showPaginator", "pageDisabled", "showFirstLastButtons", "pageIndex", "pageSize", "pageSizeOptions", "hidePageSize", "paginationTemplate", "sortOnFront", "sortActive", "sortDirection", "sortDisableClear", "sortDisabled", "sortStart", "rowHover", "rowStriped", "expandable", "expansionTemplate", "multiSelectable", "multiSelectionWithClick", "rowSelectable", "hideRowSelectionCheckbox", "disableRowClickSelection", "rowSelectionFormatter", "rowClassFormatter", "rowSelected", "cellSelectable", "showToolbar", "toolbarTitle", "toolbarTemplate", "columnHideable", "columnHideableChecked", "columnSortable", "columnPinnable", "columnPinOptions", "showColumnMenuButton", "columnMenuButtonText", "columnMenuButtonType", "columnMenuButtonColor", "columnMenuButtonClass", "columnMenuButtonIcon", "columnMenuButtonFontIcon", "columnMenuButtonSvgIcon", "showColumnMenuHeader", "columnMenuHeaderText", "columnMenuHeaderTemplate", "showColumnMenuFooter", "columnMenuFooterText", "columnMenuFooterTemplate", "noResultText", "noResultTemplate", "headerTemplate", "headerExtraTemplate", "cellTemplate", "useContentRowTemplate", "useContentHeaderRowTemplate", "useContentFooterRowTemplate", "showSummary", "summaryTemplate", "showSidebar", "sidebarTemplate", "showStatusbar", "statusbarTemplate"], outputs: ["page", "sortChange", "rowClick", "rowContextMenu", "expansionChange", "rowSelectedChange", "cellSelectedChange", "columnChange"], exportAs: ["mtxGrid"] }, { kind: "component", type: MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: ListCategorySelector, selector: "lib-list-category-selector", inputs: ["config", "selectedCategory"], outputs: ["selectedCategoryChange"] }, { kind: "component", type: MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] });
583
+ }
584
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminList, decorators: [{
585
+ type: Component,
586
+ args: [{ selector: 'ngx-thin-admin-list', imports: [
587
+ FormsModule,
588
+ MtxGridModule,
589
+ MatButton,
590
+ MatIcon,
591
+ MatIconButton,
592
+ MatTooltip,
593
+ MatFormField,
594
+ MatInput,
595
+ MatPrefix,
596
+ MatSuffix,
597
+ ListCategorySelector,
598
+ MatMenu,
599
+ MatMenuItem,
600
+ MatMenuTrigger,
601
+ RouterLink,
602
+ ], template: "<!-- Category Selector -->\n@if (categorySelectorConfig) {\n <lib-list-category-selector\n [config]=\"categorySelectorConfig\"\n [(selectedCategory)]=\"selectedCategory\"\n (selectedCategoryChange)=\"onSelectedCategoryChange($event)\"\n ></lib-list-category-selector>\n}\n<!---->\n\n<!-- Toolbar Template -->\n<ng-template #toolbarTpl>\n <div class=\"custom-toolbar\">\n <!-- List Title -->\n <span class=\"list-title\">\n @if (listConfig?.title; as listTitle) {\n @if (query.keyword || query.categoryId) {\n <!-- Title (with link for reset filter) -->\n <a\n href=\"javascript:void(0)\"\n (click)=\"query.keyword = ''; query.categoryId = undefined; this.fetchList()\"\n matTooltip=\"Reset filters\"\n >{{ listTitle }}</a\n >\n <!---->\n } @else {\n <!-- Title -->\n {{ listTitle }}\n <!---->\n }\n } @else {\n <!-- Default Title -->\n List\n <!---->\n }\n\n @if (query.categoryId && selectedCategory?.[0]; as categoryLabel) {\n <span class=\"material-icons list-title-separator\"> keyboard_arrow_right </span>\n <!-- Category -->\n {{ categoryLabel.label }}\n <!---->\n }\n\n @if (query.keyword) {\n <span class=\"material-icons list-title-separator\"> keyboard_arrow_right </span>\n <!-- Keyword -->\n \"{{ query.keyword }}\"\n <!---->\n }\n </span>\n <!---->\n\n @if (query.keyword || query.categoryId) {\n <span style=\"width: 8px\"></span>\n <!-- Clear Filters Button -->\n <button\n mat-icon-button\n class=\"clear-filters-button\"\n matTooltip=\"Clear filters\"\n (click)=\"clearFilters()\"\n >\n <mat-icon>filter_alt_off</mat-icon>\n </button>\n <!---->\n }\n\n <span style=\"flex: 1\"></span>\n\n <!-- Create Button -->\n @if (listConfig?.createButton; as createBtn) {\n @if ($any(createBtn).routerLink; as routerLinkUrl) {\n <!-- Create Button with Router Link -->\n <a\n [routerLink]=\"routerLinkUrl\"\n mat-flat-button\n class=\"primary-button\"\n style=\"color: white; transform: translateY(-1px)\"\n >\n {{ createBtn.label }}\n <mat-icon style=\"margin-bottom: 1px\">add</mat-icon>\n </a>\n } @else if ($any(createBtn).link; as linkUrl) {\n <!-- Create Button with Link -->\n <a\n [href]=\"linkUrl\"\n mat-flat-button\n class=\"primary-button\"\n style=\"color: white; transform: translateY(-1px)\"\n >\n {{ createBtn.label }}\n <mat-icon style=\"margin-bottom: 1px\">add</mat-icon>\n </a>\n } @else if ($any(createBtn).click; as handler) {\n <!-- Create Button with Handler -->\n <button\n mat-flat-button\n class=\"primary-button\"\n style=\"color: white; transform: translateY(-1px)\"\n (click)=\"handler()\"\n >\n {{ createBtn?.label }}\n <mat-icon style=\"margin-bottom: 1px\">add</mat-icon>\n </button>\n }\n }\n <!---->\n\n <span style=\"width: 30px\"></span>\n\n <!-- Search Field -->\n <mat-form-field class=\"search-keyword xs-input\" appearance=\"outline\">\n <mat-icon matPrefix>search</mat-icon>\n <input\n matInput\n [placeholder]=\"\n selectedCategory?.[0]?.label\n ? 'Search in ' + selectedCategory?.[0]?.label + '...'\n : 'Search...'\n \"\n [(ngModel)]=\"query.keyword\"\n (keyup)=\"onKeywordInput()\"\n style=\"padding-top: 3px\"\n />\n\n <!-- Search clear button -->\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear search keyword\"\n matTooltip=\"Clear search keyword\"\n (click)=\"query.keyword = ''; this.fetchList()\"\n [style.visibility]=\"query.keyword ? 'visible' : 'hidden'\"\n >\n <mat-icon>highlight_off</mat-icon>\n </button>\n <!---->\n </mat-form-field>\n <!---->\n\n <span style=\"width: 8px\"></span>\n\n <!-- Refresh Button -->\n <button mat-icon-button matTooltip=\"Refresh\" (click)=\"fetchList()\">\n <mat-icon>refresh</mat-icon>\n </button>\n <!---->\n\n <span style=\"width: 5px\"></span>\n\n <!-- CSV Export Button -->\n <button mat-icon-button [matMenuTriggerFor]=\"csvExportMenu\" matTooltip=\"Export as CSV\">\n <mat-icon>download</mat-icon>\n </button>\n <!---->\n\n <!-- CSV Export Menu -->\n <mat-menu #csvExportMenu=\"matMenu\">\n @for (encoder of csvExportService.getAvailableEncoders(); track encoder.key) {\n <!-- Export with specific charset -->\n <p style=\"color: #333; font-size: 0.8rem; margin: 1rem 0.75rem 1rem 0.7rem\">\n CSV ({{ encoder.label }})\n </p>\n <button mat-menu-item (click)=\"exportAsCsv(encoder.key)\">\n <mat-icon>download</mat-icon>\n <span>Export {{ totalCount }} items</span>\n </button>\n <!---->\n }\n </mat-menu>\n <!---->\n </div>\n</ng-template>\n<!---->\n\n<!-- List (Grid) -->\n<mtx-grid\n [data]=\"gridData\"\n [columns]=\"gridColumns\"\n [showToolbar]=\"true\"\n [toolbarTemplate]=\"toolbarTpl\"\n [length]=\"totalCount\"\n [loading]=\"isLoading\"\n [pageOnFront]=\"false\"\n [pageIndex]=\"query.page\"\n [pageSize]=\"query.perPage\"\n [pageSizeOptions]=\"listConfig?.pageSizes ?? [10, 25, 50, 100]\"\n columnMenuButtonType=\"icon\"\n columnMenuButtonClass=\"column-menu-button\"\n columnMenuButtonIcon=\"view_column\"\n (page)=\"onPageChange($event)\"\n></mtx-grid>\n<!---->\n\n<!-- Cell Template with Link -->\n<ng-template #cellTplWithLink let-row let-index=\"index\" let-col=\"colDef\">\n @if (col.link) {\n <a [href]=\"col.link(row)\">\n {{ row[col.field] }}\n </a>\n } @else if (col.routerLink) {\n <a [routerLink]=\"col.routerLink(row)\">\n {{ row[col.field] }}\n </a>\n } @else {\n {{ row[col.field] }}\n }\n</ng-template>\n<!---->\n", styles: [":host{display:flex;gap:1rem;padding:1rem;box-sizing:border-box}lib-list-category-selector{display:block;width:200px;background-color:#fcfcfc;border-radius:9px;padding:.5rem;box-sizing:border-box}lib-list-category-selector .category-list{max-height:300px;overflow-y:auto}::ng-deep .mtx-grid-toolbar{display:block;background-color:#fcfcfc;border-top-left-radius:9px;border-top-right-radius:9px;margin-bottom:1px}::ng-deep .mtx-grid-toolbar .custom-toolbar{display:flex;align-items:center;justify-content:space-between;height:37px}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title{font-size:1rem;line-height:37px}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title a{color:var(--mat-theme-primary)}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title a:hover{text-decoration:underline}::ng-deep .mtx-grid-toolbar .custom-toolbar .list-title .list-title-separator{color:#aaa;vertical-align:middle;margin:auto .2rem 4px}::ng-deep .mtx-grid-toolbar .custom-toolbar .clear-filters-button{color:var(--mat-sys-on-primary-container);min-width:32px;padding:0;vertical-align:middle}::ng-deep .mtx-grid-toolbar .custom-toolbar .clear-filters-button mat-icon{vertical-align:middle;margin-bottom:3px}::ng-deep .mtx-grid-toolbar .custom-toolbar .search-keyword{width:15rem;--mat-form-field-container-height: 18.5px;--mat-form-field-container-text-line-height: 18.5px;--mat-form-field-container-vertical-padding: 8.5px;--mat-form-field-outlined-container-shape: 28px;--mat-form-field-subscript-text-line-height: 0px}::ng-deep .mtx-grid-toolbar .custom-toolbar button{color:var(--mat-sys-on-primary-container)}::ng-deep .mtx-grid-toolbar ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}::ng-deep .mtx-grid-toolbar ::ng-deep mtx-grid-column-menu button[mat-icon-button]{color:var(--mat-sys-on-primary-container);margin-right:.5rem}::ng-deep .mtx-grid-main{background-color:#fcfcfc}table{width:100%}::ng-deep table thead,::ng-deep table tbody{background-color:#fcfcfc!important}::ng-deep .mtx-grid-footer,::ng-deep .mat-mdc-paginator{display:block;background-color:#fcfcfc!important;border-bottom-left-radius:9px!important;border-bottom-right-radius:9px!important}a,::ng-deep table a,::ng-deep table a:visited{color:var(--mat-sys-on-primary-container);text-decoration:none}a:hover,::ng-deep table a:hover,::ng-deep table a:visited:hover{text-decoration:underline}\n"] }]
603
+ }], propDecorators: { listConfig: [{
604
+ type: Input
605
+ }], categorySelectorConfig: [{
606
+ type: Input
607
+ }], listColumns: [{
608
+ type: Input
609
+ }], query: [{
610
+ type: Input
611
+ }], cellTplWithLink: [{
612
+ type: ViewChild,
613
+ args: ['cellTplWithLink', { static: true }]
614
+ }] } });
615
+
616
+ class FormlyMatPrefixAddonWrapper extends FieldWrapper {
617
+ matPrefix;
618
+ matSuffix;
619
+ ngAfterViewInit() {
620
+ if (this.matPrefix) {
621
+ this.props['prefix'] = this.matPrefix;
622
+ }
623
+ if (this.matSuffix) {
624
+ this.props['suffix'] = this.matSuffix;
625
+ }
626
+ }
627
+ addonRightClick($event) {
628
+ if (this.props['addonRight'].onClick) {
629
+ this.props['addonRight'].onClick(this.to, this, $event);
630
+ }
631
+ }
632
+ addonLeftClick($event) {
633
+ if (this.props['addonLeft'].onClick) {
634
+ this.props['addonLeft'].onClick(this.to, this, $event);
635
+ }
636
+ }
637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FormlyMatPrefixAddonWrapper, deps: null, target: i0.ɵɵFactoryTarget.Component });
638
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: FormlyMatPrefixAddonWrapper, isStandalone: true, selector: "lib-formly-mat-prefix-addon-wrapper", viewQueries: [{ propertyName: "matPrefix", first: true, predicate: ["matPrefix"], descendants: true, static: true }, { propertyName: "matSuffix", first: true, predicate: ["matSuffix"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: `
639
+ <ng-container #fieldComponent></ng-container>
640
+
641
+ <ng-template #matSuffix>
642
+ @if (props['addonRight']) {
643
+ @if (props['addonRight']['icon']) {
644
+ <button
645
+ mat-icon-button
646
+ matSuffix
647
+ style="margin-right: 1rem;"
648
+ [matTooltip]="props['addonRight']['tooltip'] ?? ''"
649
+ (click)="props['addonRight'].onClick ? addonRightClick($event) : null"
650
+ type="button"
651
+ >
652
+ <mat-icon>{{ props['addonRight']['icon'] }}</mat-icon>
653
+ </button>
654
+ }
655
+ @if (props['addonRight']['text']) {
656
+ <span>{{ props['addonRight']['text'] }}</span>
657
+ }
658
+ }
659
+ </ng-template>
660
+ `, isInline: true, dependencies: [{ kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
661
+ }
662
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FormlyMatPrefixAddonWrapper, decorators: [{
663
+ type: Component,
664
+ args: [{
665
+ selector: 'lib-formly-mat-prefix-addon-wrapper',
666
+ imports: [MatIconButton, MatIcon, MatSuffix, MatTooltip],
667
+ template: `
668
+ <ng-container #fieldComponent></ng-container>
669
+
670
+ <ng-template #matSuffix>
671
+ @if (props['addonRight']) {
672
+ @if (props['addonRight']['icon']) {
673
+ <button
674
+ mat-icon-button
675
+ matSuffix
676
+ style="margin-right: 1rem;"
677
+ [matTooltip]="props['addonRight']['tooltip'] ?? ''"
678
+ (click)="props['addonRight'].onClick ? addonRightClick($event) : null"
679
+ type="button"
680
+ >
681
+ <mat-icon>{{ props['addonRight']['icon'] }}</mat-icon>
682
+ </button>
683
+ }
684
+ @if (props['addonRight']['text']) {
685
+ <span>{{ props['addonRight']['text'] }}</span>
686
+ }
687
+ }
688
+ </ng-template>
689
+ `,
690
+ }]
691
+ }], propDecorators: { matPrefix: [{
692
+ type: ViewChild,
693
+ args: ['matPrefix', { static: true }]
694
+ }], matSuffix: [{
695
+ type: ViewChild,
696
+ args: ['matSuffix', { static: true }]
697
+ }] } });
698
+ function formlyAddonsExtension(field) {
699
+ if (!field.props || (field.wrappers && field.wrappers.indexOf('addons') !== -1)) {
700
+ return;
701
+ }
702
+ if (field.props['addonLeft'] || field.props['addonRight']) {
703
+ field.wrappers = [...(field.wrappers || []), 'addons'];
704
+ }
705
+ }
706
+
707
+ function getErrorMessage(error) {
708
+ if (typeof error === 'string') {
709
+ return error;
710
+ }
711
+ else if (error instanceof Error) {
712
+ return error.message;
713
+ }
714
+ else if (error?.error?.message) {
715
+ if (Array.isArray(error.error.message)) {
716
+ return error.error.message[0];
717
+ }
718
+ else {
719
+ return error.error.message;
720
+ }
721
+ }
722
+ else {
723
+ return 'An unknown error occurred';
724
+ }
725
+ }
726
+
727
+ class NgxThinAdminEditor {
728
+ /**
729
+ * Config for editor
730
+ */
731
+ editorConfig;
732
+ /**
733
+ * Fields for formly form
734
+ */
735
+ editorFields;
736
+ /**
737
+ * Id of the item being edited.
738
+ */
739
+ itemId;
740
+ /**
741
+ * Internal state for form
742
+ */
743
+ isSaving = false;
744
+ isLoading = false;
745
+ form = new FormGroup({});
746
+ data = {};
747
+ formData = {};
748
+ /**
749
+ * Services
750
+ */
751
+ snackbar = inject(MatSnackBar);
752
+ cdr = inject(ChangeDetectorRef);
753
+ ngOnChanges(changes) {
754
+ // Fetcher
755
+ if (changes.editorConfig &&
756
+ changes.editorConfig.currentValue?.fetcher !== changes.editorConfig.previousValue?.fetcher) {
757
+ this.fetchItem();
758
+ }
759
+ else if (changes.itemId && changes.itemId.currentValue !== changes.itemId.previousValue) {
760
+ this.fetchItem();
761
+ }
762
+ // Fields
763
+ if (changes.editorFields?.currentValue) {
764
+ this.editorFields = changes.editorFields.currentValue.map((col) => {
765
+ return this.overwriteFieldProps(col);
766
+ });
767
+ }
768
+ }
769
+ overwriteFieldProps(field) {
770
+ if (!field.props) {
771
+ field.props = {};
772
+ }
773
+ // requiredOnCreate
774
+ if (field.props.requiredOnCreate) {
775
+ if (this.itemId === undefined) {
776
+ field.props.required = true;
777
+ }
778
+ else {
779
+ field.props.required = false;
780
+ }
781
+ }
782
+ // disabledOnUpdate
783
+ if (field.props.disabledOnUpdate) {
784
+ if (this.itemId !== undefined) {
785
+ //formControl.disable();
786
+ field.props.disabled = true;
787
+ }
788
+ else {
789
+ //formControl.enable();
790
+ field.props.disabled = false;
791
+ }
792
+ }
793
+ return field;
794
+ }
795
+ async fetchItem() {
796
+ if (!this.editorConfig?.fetcher) {
797
+ return;
798
+ }
799
+ else if (!this.itemId) {
800
+ this.data = {};
801
+ return;
802
+ }
803
+ // Set loading state
804
+ this.isLoading = true;
805
+ this.cdr.markForCheck();
806
+ // Call the fetcher function to get the item data
807
+ try {
808
+ let res = await this.editorConfig.fetcher(this.itemId);
809
+ if (res instanceof Response) {
810
+ if (!res.ok) {
811
+ const data = await res.json();
812
+ throw new Error(data.error ?? data.message ?? `Failed to fetch data: ${res.status} ${res.statusText}`);
813
+ }
814
+ res = await res.json();
815
+ }
816
+ this.data = { ...res };
817
+ this.formData = { ...res };
818
+ }
819
+ catch (e) {
820
+ const errorMessage = getErrorMessage(e);
821
+ this.snackbar.open(`Error: ${errorMessage}`, undefined, {
822
+ duration: 3000,
823
+ });
824
+ // Reset loading state
825
+ this.isLoading = false;
826
+ this.cdr.markForCheck();
827
+ return;
828
+ }
829
+ this.isLoading = false;
830
+ this.cdr.markForCheck();
831
+ }
832
+ async save() {
833
+ if (this.form.status === 'INVALID') {
834
+ this.snackbar.open('Error: Invalid input', undefined, {
835
+ duration: 3000,
836
+ });
837
+ // Mark field to show validation errors
838
+ Object.keys(this.form.controls).forEach((key) => {
839
+ const control = this.form.get(key);
840
+ control?.markAsTouched();
841
+ });
842
+ return;
843
+ }
844
+ if (!this.editorConfig?.saver) {
845
+ this.snackbar.open('Error: No saver function provided', undefined, {
846
+ duration: 3000,
847
+ });
848
+ return;
849
+ }
850
+ // Disable the form and save button to prevent multiple submissions
851
+ this.form.disable();
852
+ this.isSaving = true;
853
+ // Call the saver function
854
+ let result;
855
+ try {
856
+ let res = await this.editorConfig?.saver?.(this.formData, this.itemId);
857
+ if (res instanceof Response) {
858
+ if (!res.ok) {
859
+ const data = await res.json();
860
+ throw new Error(data.error ?? data.message ?? `Failed to save data: ${res.status} ${res.statusText}`);
861
+ }
862
+ res = await res.json();
863
+ }
864
+ result = res;
865
+ }
866
+ catch (e) {
867
+ const errorMessage = getErrorMessage(e);
868
+ this.snackbar.open(`Error: ${errorMessage}`, undefined, {
869
+ duration: 3000,
870
+ });
871
+ // Re-enable the form and save button
872
+ this.form.enable();
873
+ this.isSaving = false;
874
+ return;
875
+ }
876
+ // Show success message
877
+ this.snackbar.open('Data saved successfully', undefined, {
878
+ duration: 3000,
879
+ });
880
+ // Update form data with the result from the saver (e.g., to get generated ID)
881
+ this.data = { ...result };
882
+ this.formData = { ...result };
883
+ // Re-enable the form and save button
884
+ this.form.enable();
885
+ for (const field of this.editorFields ?? []) {
886
+ this.overwriteFieldProps(field);
887
+ }
888
+ this.isSaving = false;
889
+ }
890
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
891
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: NgxThinAdminEditor, isStandalone: true, selector: "ngx-thin-admin-editor", inputs: { editorConfig: "editorConfig", editorFields: "editorFields", itemId: "itemId" }, providers: [
892
+ provideFormlyCore(withFormlyMaterial()),
893
+ provideFormlyCore({
894
+ wrappers: [
895
+ {
896
+ name: 'addons',
897
+ component: FormlyMatPrefixAddonWrapper,
898
+ },
899
+ ],
900
+ extensions: [{ name: 'addons', extension: { onPopulate: formlyAddonsExtension } }],
901
+ }),
902
+ {
903
+ // Apply default appearance and other settings to all Material form fields in this component
904
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
905
+ useValue: {
906
+ appearance: 'outline',
907
+ floatLabel: 'always',
908
+ subscriptSizing: 'dynamic',
909
+ },
910
+ },
911
+ ], usesOnChanges: true, ngImport: i0, template: "<mat-card>\n <mat-card-header>\n <h3>\n @if (itemId !== undefined && $any(data); as editItem) {\n @if (editorConfig?.labelFieldKey && editItem[editorConfig!.labelFieldKey!]) {\n <!-- e.g., \"Edit - Taro (taro) \" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n <span class=\"editor-item-label\">{{ editItem[editorConfig!.labelFieldKey!] }}</span>\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n } @else if (editItem.id) {\n <!-- e.g., \"Edit - ID: 123\" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n ID:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n Edit\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n }\n } @else {\n <!-- Create Account -->\n Create\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n <!---->\n }\n </h3>\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n } @else {\n <form [formGroup]=\"form\" (ngSubmit)=\"save()\">\n <formly-form\n [form]=\"form\"\n [fields]=\"$any(editorFields) ?? []\"\n [model]=\"formData\"\n ></formly-form>\n\n @if (editorConfig?.saver) {\n <!-- Submit button -->\n <button\n type=\"submit\"\n mat-flat-button\n style=\"color: white; transform: translateY(-0.3rem); float: right\"\n >\n @if (itemId !== undefined) {\n Save Changes\n } @else {\n Create\n }\n </button>\n <!---->\n }\n </form>\n }\n </mat-card-content>\n</mat-card>\n", styles: [":host{max-width:600px;display:block;margin:0 auto}mat-card{padding:2rem}mat-card-header h3{font-size:1.2rem;font-weight:400}mat-card-header .editor-item-label{margin-left:1rem}mat-card-header .editor-header-sub{color:#555;margin-left:.85rem;font-size:.9rem;font-style:italic}form{margin-top:2rem}form ::ng-deep .mat-mdc-form-field-subscript-wrapper{margin-bottom:14px!important}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: FormlyForm, selector: "formly-form", inputs: ["form", "model", "fields", "options"], outputs: ["modelChange"] }, { kind: "component", type: MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: MatCardContent, selector: "mat-card-content" }] });
912
+ }
913
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, decorators: [{
914
+ type: Component,
915
+ args: [{ selector: 'ngx-thin-admin-editor', imports: [
916
+ ReactiveFormsModule,
917
+ FormlyForm,
918
+ MatButton,
919
+ MatProgressBar,
920
+ MatCard,
921
+ MatCardHeader,
922
+ MatCardContent,
923
+ ], providers: [
924
+ provideFormlyCore(withFormlyMaterial()),
925
+ provideFormlyCore({
926
+ wrappers: [
927
+ {
928
+ name: 'addons',
929
+ component: FormlyMatPrefixAddonWrapper,
930
+ },
931
+ ],
932
+ extensions: [{ name: 'addons', extension: { onPopulate: formlyAddonsExtension } }],
933
+ }),
934
+ {
935
+ // Apply default appearance and other settings to all Material form fields in this component
936
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
937
+ useValue: {
938
+ appearance: 'outline',
939
+ floatLabel: 'always',
940
+ subscriptSizing: 'dynamic',
941
+ },
942
+ },
943
+ ], template: "<mat-card>\n <mat-card-header>\n <h3>\n @if (itemId !== undefined && $any(data); as editItem) {\n @if (editorConfig?.labelFieldKey && editItem[editorConfig!.labelFieldKey!]) {\n <!-- e.g., \"Edit - Taro (taro) \" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n <span class=\"editor-item-label\">{{ editItem[editorConfig!.labelFieldKey!] }}</span>\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n } @else if (editItem.id) {\n <!-- e.g., \"Edit - ID: 123\" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n ID:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n Edit\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n }\n } @else {\n <!-- Create Account -->\n Create\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n <!---->\n }\n </h3>\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n } @else {\n <form [formGroup]=\"form\" (ngSubmit)=\"save()\">\n <formly-form\n [form]=\"form\"\n [fields]=\"$any(editorFields) ?? []\"\n [model]=\"formData\"\n ></formly-form>\n\n @if (editorConfig?.saver) {\n <!-- Submit button -->\n <button\n type=\"submit\"\n mat-flat-button\n style=\"color: white; transform: translateY(-0.3rem); float: right\"\n >\n @if (itemId !== undefined) {\n Save Changes\n } @else {\n Create\n }\n </button>\n <!---->\n }\n </form>\n }\n </mat-card-content>\n</mat-card>\n", styles: [":host{max-width:600px;display:block;margin:0 auto}mat-card{padding:2rem}mat-card-header h3{font-size:1.2rem;font-weight:400}mat-card-header .editor-item-label{margin-left:1rem}mat-card-header .editor-header-sub{color:#555;margin-left:.85rem;font-size:.9rem;font-style:italic}form{margin-top:2rem}form ::ng-deep .mat-mdc-form-field-subscript-wrapper{margin-bottom:14px!important}\n"] }]
944
+ }], propDecorators: { editorConfig: [{
945
+ type: Input
946
+ }], editorFields: [{
947
+ type: Input
948
+ }], itemId: [{
949
+ type: Input
950
+ }] } });
951
+
952
+ /*
953
+ * Public API Surface of ngx-thin-admin
954
+ */
955
+
956
+ /**
957
+ * Generated bundle index. Do not edit.
958
+ */
959
+
960
+ export { NgxThinAdminEditor, NgxThinAdminList };
961
+ //# sourceMappingURL=ngx-thin-admin.mjs.map