ngx-thin-admin 0.0.0-alpha.2 → 0.0.0-alpha.3

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.
@@ -4,6 +4,7 @@ import { lastValueFrom, Subject } from 'rxjs';
4
4
  import { debounceTime } from 'rxjs/operators';
5
5
  import * as i4 from '@angular/material/button';
6
6
  import { MatButtonModule, MatIconButton, MatButton } from '@angular/material/button';
7
+ import { MatSnackBar } from '@angular/material/snack-bar';
7
8
  import { MatIcon } from '@angular/material/icon';
8
9
  import * as i2$1 from '@angular/material/input';
9
10
  import { MatInputModule, MatInput, MatPrefix, MatSuffix } from '@angular/material/input';
@@ -15,12 +16,11 @@ import * as i1 from '@angular/forms';
15
16
  import { FormsModule, ReactiveFormsModule, FormGroup } from '@angular/forms';
16
17
  import { MatSelectionList, MatListOption } from '@angular/material/list';
17
18
  import { MatMenu, MatMenuItem, MatMenuTrigger, MatMenuContent } from '@angular/material/menu';
18
- import { MatSnackBar } from '@angular/material/snack-bar';
19
19
  import { MatDialogRef, MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog } from '@angular/material/dialog';
20
20
  import * as i2 from '@angular/material/form-field';
21
21
  import { MatFormFieldModule, MatFormField as MatFormField$1, MatLabel, MatHint, MatSuffix as MatSuffix$1, MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
22
22
  import { stringify } from 'csv-stringify/browser/esm/sync';
23
- import { RouterLink } from '@angular/router';
23
+ import { RouterLink, Router } from '@angular/router';
24
24
  import { MatCard, MatCardHeader, MatCardContent } from '@angular/material/card';
25
25
  import { FieldWrapper, FieldType, FormlyAttributes, FormlyForm, provideFormlyCore } from '@ngx-formly/core';
26
26
  import { withFormlyMaterial } from '@ngx-formly/material';
@@ -42,6 +42,8 @@ const messagesEn = {
42
42
  'list.exportAsCsv': 'Export as CSV',
43
43
  'list.exportedFileName': 'exported',
44
44
  'list.exportItems': 'Export {count} items',
45
+ 'editor.titleForCreate': 'Create {itemType}',
46
+ 'editor.titleForEdit': 'Edit {itemType}',
45
47
  'editor.idLabel': 'ID',
46
48
  'editor.saveChanges': 'Save Changes',
47
49
  'editor.errorInvalidInput': 'Invalid input',
@@ -54,10 +56,18 @@ const messagesEn = {
54
56
  'category.editorDialogTitleForEdit': 'Edit {categoryType}',
55
57
  'category.deletionDialogTitle': 'Delete {categoryType}',
56
58
  'category.deletionDialogMessage': 'Are you sure you want to delete the {categoryType} "{label}"? This action cannot be undone.',
57
- 'category.errorCouldNotCreate': 'Could not create category',
58
- 'category.errorFailedUpdate': 'Failed to update category "{label}"',
59
- 'category.errorFailedDelete': 'Failed to delete category "{label}"',
60
- 'category.errorCouldNotSave': 'Could not save category',
59
+ 'category.errorCouldNotCreate': 'Could not create {categoryType}',
60
+ 'category.errorFailedUpdate': 'Failed to update {categoryType} "{label}"',
61
+ 'category.errorFailedDelete': 'Failed to delete {categoryType} "{label}"',
62
+ 'category.errorCouldNotSave': 'Could not save {categoryType}',
63
+ 'category.successCreated': '{categoryType} "{label}" has been created successfully',
64
+ 'category.successUpdated': '{categoryType} "{label}" has been updated successfully',
65
+ 'category.successDeleted': '{categoryType} "{label}" has been deleted successfully',
66
+ 'item.defaultSingular': 'Item',
67
+ 'item.deletionDialogTitle': 'Delete {itemType}',
68
+ 'item.deletionDialogMessage': 'Are you sure you want to delete "{label}" ({id})? This action cannot be undone.',
69
+ 'item.errorFailedDelete': 'Failed to delete "{label}"',
70
+ 'item.successDeleted': '"{label}" has been deleted successfully',
61
71
  'csv.exporting': 'Exporting CSV...',
62
72
  'csv.exportCompleted': 'CSV export completed. Exported {count} items.',
63
73
  'csv.exportCompletedWithErrors': 'CSV exported up to {count} items, but errors occurred: {error}',
@@ -79,6 +89,8 @@ const messagesJa = {
79
89
  'list.exportAsCsv': 'CSV でエクスポート',
80
90
  'list.exportedFileName': 'exported',
81
91
  'list.exportItems': '{count} 件をエクスポート',
92
+ 'editor.titleForCreate': '{itemType} の作成',
93
+ 'editor.titleForEdit': '{itemType} の編集',
82
94
  'editor.idLabel': 'ID',
83
95
  'editor.saveChanges': '変更を保存',
84
96
  'editor.errorInvalidInput': '入力内容が不正です',
@@ -91,10 +103,18 @@ const messagesJa = {
91
103
  'category.editorDialogTitleForEdit': '{categoryType} の編集',
92
104
  'category.deletionDialogTitle': '{categoryType} の削除',
93
105
  'category.deletionDialogMessage': '{categoryType} "{label}" を削除しますか?この操作は元に戻せません。',
94
- 'category.errorCouldNotCreate': 'カテゴリを作成できませんでした',
95
- 'category.errorFailedUpdate': 'カテゴリ "{label}" の更新に失敗しました',
96
- 'category.errorFailedDelete': 'カテゴリ "{label}" の削除に失敗しました',
97
- 'category.errorCouldNotSave': 'カテゴリを保存できませんでした',
106
+ 'category.errorCouldNotCreate': '{categoryType} を作成できませんでした',
107
+ 'category.errorFailedUpdate': '{categoryType} "{label}" の更新に失敗しました',
108
+ 'category.errorFailedDelete': '{categoryType} "{label}" の削除に失敗しました',
109
+ 'category.errorCouldNotSave': '{categoryType} を保存できませんでした',
110
+ 'category.successCreated': '{categoryType} "{label}" を作成しました',
111
+ 'category.successUpdated': '{categoryType} "{label}" を更新しました',
112
+ 'category.successDeleted': '{categoryType} "{label}" を削除しました',
113
+ 'item.defaultSingular': '項目',
114
+ 'item.deletionDialogTitle': '{itemType} の削除',
115
+ 'item.deletionDialogMessage': '"{label}" ({id}) を削除しますか?この操作は元に戻せません。',
116
+ 'item.errorFailedDelete': '"{label}" の削除に失敗しました',
117
+ 'item.successDeleted': '"{label}" を削除しました',
98
118
  'csv.exporting': 'CSV をエクスポート中...',
99
119
  'csv.exportCompleted': 'CSV エクスポートが完了しました。{count} 件をエクスポートしました。',
100
120
  'csv.exportCompletedWithErrors': '{count} 件までエクスポートしましたが、エラーが発生しました: {error}',
@@ -249,10 +269,18 @@ class ListCategorySelector {
249
269
  await this.config.creator(categoryLabel);
250
270
  }
251
271
  catch (e) {
252
- this.snackbar.open(`Error: ${e?.message ?? this.t('category.errorCouldNotCreate')}`, undefined, {
272
+ this.snackbar.open(`Error: ${e?.message ?? this.t('category.errorCouldNotCreate', { categoryType: this.config.singularLabel ?? this.t('category.defaultSingular') })}`, undefined, {
253
273
  duration: 3000,
254
274
  });
275
+ return;
255
276
  }
277
+ // Show success message
278
+ this.snackbar.open(this.t('category.successCreated', {
279
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
280
+ label: categoryLabel,
281
+ }), undefined, {
282
+ duration: 3000,
283
+ });
256
284
  // Refresh category list
257
285
  this.fetchCategories();
258
286
  }
@@ -265,10 +293,22 @@ class ListCategorySelector {
265
293
  await this.config.updater(category);
266
294
  }
267
295
  catch (e) {
268
- this.snackbar.open(`Error: ${e?.message ?? this.t('category.errorFailedUpdate', { label: category.label })}`, undefined, {
296
+ this.snackbar.open(`Error: ${e?.message ??
297
+ this.t('category.errorFailedUpdate', {
298
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
299
+ label: category.label,
300
+ })}`, undefined, {
269
301
  duration: 3000,
270
302
  });
303
+ return;
271
304
  }
305
+ // Show success message
306
+ this.snackbar.open(this.t('category.successUpdated', {
307
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
308
+ label: category.label,
309
+ }), undefined, {
310
+ duration: 3000,
311
+ });
272
312
  // Refresh category list
273
313
  this.fetchCategories();
274
314
  }
@@ -281,10 +321,22 @@ class ListCategorySelector {
281
321
  await this.config.deleter(category);
282
322
  }
283
323
  catch (e) {
284
- this.snackbar.open(`Error: ${e?.message ?? this.t('category.errorFailedDelete', { label: category.label })}`, undefined, {
324
+ this.snackbar.open(`Error: ${e?.message ??
325
+ this.t('category.errorFailedDelete', {
326
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
327
+ label: category.label,
328
+ })}`, undefined, {
285
329
  duration: 3000,
286
330
  });
331
+ return;
287
332
  }
333
+ // Show success message
334
+ this.snackbar.open(this.t('category.successDeleted', {
335
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
336
+ label: category.label,
337
+ }), undefined, {
338
+ duration: 3000,
339
+ });
288
340
  // Refresh category list
289
341
  this.fetchCategories();
290
342
  }
@@ -313,7 +365,10 @@ class ListCategorySelector {
313
365
  }
314
366
  }
315
367
  catch (e) {
316
- this.snackbar.open(`Error: ${e?.message ?? this.t('category.errorCouldNotSave')}`, undefined, {
368
+ this.snackbar.open(`Error: ${e?.message ??
369
+ this.t('category.errorCouldNotSave', {
370
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
371
+ })}`, undefined, {
317
372
  duration: 3000,
318
373
  });
319
374
  }
@@ -345,7 +400,11 @@ class ListCategorySelector {
345
400
  await this.deleteCategory(category);
346
401
  }
347
402
  catch (e) {
348
- this.snackbar.open(`Error: ${e?.message ?? this.t('category.errorFailedDelete', { label: category.label })}`, undefined, {
403
+ this.snackbar.open(`Error: ${e?.message ??
404
+ this.t('category.errorFailedDelete', {
405
+ categoryType: this.config?.singularLabel ?? this.t('category.defaultSingular'),
406
+ label: category.label,
407
+ })}`, undefined, {
349
408
  duration: 3000,
350
409
  });
351
410
  }
@@ -570,6 +629,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
570
629
  }]
571
630
  }], ctorParameters: () => [] });
572
631
 
632
+ function getErrorMessage(error) {
633
+ if (typeof error === 'string') {
634
+ return error;
635
+ }
636
+ else if (error instanceof Error) {
637
+ return error.message;
638
+ }
639
+ else if (error?.error?.message) {
640
+ if (Array.isArray(error.error.message)) {
641
+ return error.error.message[0];
642
+ }
643
+ else {
644
+ return error.error.message;
645
+ }
646
+ }
647
+ else {
648
+ return 'An unknown error occurred';
649
+ }
650
+ }
651
+
573
652
  class NgxThinAdminList {
574
653
  /**
575
654
  * Config for list
@@ -579,6 +658,11 @@ class NgxThinAdminList {
579
658
  * Config for category selector. If provided, a category selector will be displayed in the UI.
580
659
  */
581
660
  categorySelectorConfig;
661
+ /**
662
+ * Config for item deleter (If provided, item deletion feature is enabled)
663
+ * It works by calling openItemDeletionDialog(row) from the click handler of the column definition.
664
+ */
665
+ itemDeleterConfig;
582
666
  /**
583
667
  * Column schema (extended type of MtxGridColumn)
584
668
  * @see https://ng-matero.github.io/extensions/components/grid/api
@@ -628,7 +712,9 @@ class NgxThinAdminList {
628
712
  */
629
713
  cdr = inject(ChangeDetectorRef);
630
714
  translate = inject(NGX_THIN_ADMIN_TRANSLATE);
715
+ snackbar = inject(MatSnackBar);
631
716
  csvExportService = inject(CsvExportService);
717
+ dialog = inject(MatDialog);
632
718
  t(key, params) {
633
719
  return this.translate(key, params);
634
720
  }
@@ -712,8 +798,54 @@ class NgxThinAdminList {
712
798
  this.query.categoryId = categoryId;
713
799
  this.fetchList();
714
800
  }
801
+ async openItemDeletionDialog(item) {
802
+ if (!this.itemDeleterConfig?.deleter) {
803
+ return;
804
+ }
805
+ // Get the label and ID to query in the confirmation dialog
806
+ const idKey = this.listConfig?.idFieldKey;
807
+ const labelKey = this.listConfig?.labelFieldKey;
808
+ const id = idKey ? item[idKey] : (item.id ?? JSON.stringify(item));
809
+ const label = labelKey ? item[labelKey] : id;
810
+ const itemType = this.listConfig?.singularLabel || this.t('item.defaultSingular');
811
+ // Open the confirmation dialog
812
+ const dialogRef = this.dialog.open(ConfirmDialog, {
813
+ width: '400px',
814
+ data: {
815
+ title: this.t('item.deletionDialogTitle', { itemType }),
816
+ message: this.t('item.deletionDialogMessage', { label, id }),
817
+ positiveButtonText: this.t('common.delete'),
818
+ cancelButtonText: this.t('common.cancel'),
819
+ },
820
+ });
821
+ const result = await lastValueFrom(dialogRef.afterClosed());
822
+ if (!result) {
823
+ return;
824
+ }
825
+ // Execute delete process
826
+ await this.deleteItem(id, label);
827
+ }
828
+ async deleteItem(id, label) {
829
+ if (!this.itemDeleterConfig?.deleter) {
830
+ return;
831
+ }
832
+ try {
833
+ await this.itemDeleterConfig.deleter(id);
834
+ }
835
+ catch (e) {
836
+ const errorMessage = getErrorMessage(e);
837
+ this.snackbar.open(`Error: ${errorMessage || this.t('item.errorFailedDelete', { label })}`, undefined, { duration: 3000 });
838
+ return;
839
+ }
840
+ // Show success message
841
+ this.snackbar.open(this.t('item.successDeleted', { label }), undefined, {
842
+ duration: 3000,
843
+ });
844
+ // Refresh the list after deletion
845
+ this.fetchList();
846
+ }
715
847
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminList, deps: [], target: i0.ɵɵFactoryTarget.Component });
716
- 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]=\"t('list.clearFilters')\"\n >{{ listTitle }}</a\n >\n <!---->\n } @else {\n <!-- Title -->\n {{ listTitle }}\n <!---->\n }\n } @else {\n <!-- Default Title -->\n {{ t('list.defaultTitle') }}\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]=\"t('list.clearFilters')\"\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 ? t('list.searchInCategoryPlaceholder', { category: selectedCategory?.[0]?.label })\n : t('list.searchDefaultPlaceholder')\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 [attr.aria-label]=\"t('list.clearSearchKeyword')\"\n [matTooltip]=\"t('list.clearSearchKeyword')\"\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]=\"t('list.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\n mat-icon-button\n [matMenuTriggerFor]=\"csvExportMenu\"\n [matTooltip]=\"t('list.exportAsCsv')\"\n >\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>{{ t('list.exportItems', { count: totalCount }) }}</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.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MtxGridModule }, { kind: "component", type: i2$2.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"] }] });
848
+ 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", itemDeleterConfig: "itemDeleterConfig", 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]=\"t('list.clearFilters')\"\n >{{ listTitle }}</a\n >\n <!---->\n } @else {\n <!-- Title -->\n {{ listTitle }}\n <!---->\n }\n } @else {\n <!-- Default Title -->\n {{ t('list.defaultTitle') }}\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]=\"t('list.clearFilters')\"\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=\"create-item-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=\"create-item-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=\"create-item-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 ? t('list.searchInCategoryPlaceholder', { category: selectedCategory?.[0]?.label })\n : t('list.searchDefaultPlaceholder')\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 [attr.aria-label]=\"t('list.clearSearchKeyword')\"\n [matTooltip]=\"t('list.clearSearchKeyword')\"\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]=\"t('list.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\n mat-icon-button\n [matMenuTriggerFor]=\"csvExportMenu\"\n [matTooltip]=\"t('list.exportAsCsv')\"\n >\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>{{ t('list.exportItems', { count: totalCount }) }}</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 .create-item-button{text-decoration:none!important}::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.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MtxGridModule }, { kind: "component", type: i2$2.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"] }] });
717
849
  }
718
850
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminList, decorators: [{
719
851
  type: Component,
@@ -733,11 +865,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
733
865
  MatMenuItem,
734
866
  MatMenuTrigger,
735
867
  RouterLink,
736
- ], 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]=\"t('list.clearFilters')\"\n >{{ listTitle }}</a\n >\n <!---->\n } @else {\n <!-- Title -->\n {{ listTitle }}\n <!---->\n }\n } @else {\n <!-- Default Title -->\n {{ t('list.defaultTitle') }}\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]=\"t('list.clearFilters')\"\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 ? t('list.searchInCategoryPlaceholder', { category: selectedCategory?.[0]?.label })\n : t('list.searchDefaultPlaceholder')\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 [attr.aria-label]=\"t('list.clearSearchKeyword')\"\n [matTooltip]=\"t('list.clearSearchKeyword')\"\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]=\"t('list.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\n mat-icon-button\n [matMenuTriggerFor]=\"csvExportMenu\"\n [matTooltip]=\"t('list.exportAsCsv')\"\n >\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>{{ t('list.exportItems', { count: totalCount }) }}</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"] }]
868
+ ], 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]=\"t('list.clearFilters')\"\n >{{ listTitle }}</a\n >\n <!---->\n } @else {\n <!-- Title -->\n {{ listTitle }}\n <!---->\n }\n } @else {\n <!-- Default Title -->\n {{ t('list.defaultTitle') }}\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]=\"t('list.clearFilters')\"\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=\"create-item-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=\"create-item-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=\"create-item-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 ? t('list.searchInCategoryPlaceholder', { category: selectedCategory?.[0]?.label })\n : t('list.searchDefaultPlaceholder')\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 [attr.aria-label]=\"t('list.clearSearchKeyword')\"\n [matTooltip]=\"t('list.clearSearchKeyword')\"\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]=\"t('list.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\n mat-icon-button\n [matMenuTriggerFor]=\"csvExportMenu\"\n [matTooltip]=\"t('list.exportAsCsv')\"\n >\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>{{ t('list.exportItems', { count: totalCount }) }}</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 .create-item-button{text-decoration:none!important}::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"] }]
737
869
  }], propDecorators: { listConfig: [{
738
870
  type: Input
739
871
  }], categorySelectorConfig: [{
740
872
  type: Input
873
+ }], itemDeleterConfig: [{
874
+ type: Input
741
875
  }], listColumns: [{
742
876
  type: Input
743
877
  }], query: [{
@@ -838,26 +972,6 @@ function formlyAddonsExtension(field) {
838
972
  }
839
973
  }
840
974
 
841
- function getErrorMessage(error) {
842
- if (typeof error === 'string') {
843
- return error;
844
- }
845
- else if (error instanceof Error) {
846
- return error.message;
847
- }
848
- else if (error?.error?.message) {
849
- if (Array.isArray(error.error.message)) {
850
- return error.error.message[0];
851
- }
852
- else {
853
- return error.error.message;
854
- }
855
- }
856
- else {
857
- return 'An unknown error occurred';
858
- }
859
- }
860
-
861
975
  class FormlyFieldFile extends FieldType {
862
976
  /** 表示用のファイル名 */
863
977
  fileName = '';
@@ -965,6 +1079,10 @@ class NgxThinAdminEditor {
965
1079
  * Id of the item being edited.
966
1080
  */
967
1081
  itemId;
1082
+ /**
1083
+ * Config for item deleter
1084
+ */
1085
+ itemDeleterConfig;
968
1086
  /**
969
1087
  * Internal state for form
970
1088
  */
@@ -979,9 +1097,14 @@ class NgxThinAdminEditor {
979
1097
  snackbar = inject(MatSnackBar);
980
1098
  cdr = inject(ChangeDetectorRef);
981
1099
  translate = inject(NGX_THIN_ADMIN_TRANSLATE);
1100
+ dialog = inject(MatDialog);
1101
+ router = inject(Router);
982
1102
  t(key, params) {
983
1103
  return this.translate(key, params);
984
1104
  }
1105
+ get editorItemType() {
1106
+ return this.editorConfig?.singularLabel || this.t('item.defaultSingular');
1107
+ }
985
1108
  ngOnChanges(changes) {
986
1109
  // Fetcher
987
1110
  if (changes.editorConfig &&
@@ -1119,8 +1242,54 @@ class NgxThinAdminEditor {
1119
1242
  }
1120
1243
  this.isSaving = false;
1121
1244
  }
1245
+ async openDeletionDialog() {
1246
+ if (!this.itemDeleterConfig?.deleter || this.itemId === undefined) {
1247
+ return;
1248
+ }
1249
+ // Get the label and ID to query in the confirmation dialog
1250
+ const id = this.itemId;
1251
+ const label = this.editorConfig?.labelFieldKey ? this.data[this.editorConfig.labelFieldKey] : id;
1252
+ const itemType = this.editorItemType;
1253
+ // Open the confirmation dialog
1254
+ const dialogRef = this.dialog.open(ConfirmDialog, {
1255
+ width: '400px',
1256
+ data: {
1257
+ title: this.t('item.deletionDialogTitle', { itemType }),
1258
+ message: this.t('item.deletionDialogMessage', { label, id }),
1259
+ positiveButtonText: this.t('common.delete'),
1260
+ cancelButtonText: this.t('common.cancel'),
1261
+ },
1262
+ });
1263
+ const result = await lastValueFrom(dialogRef.afterClosed());
1264
+ if (!result) {
1265
+ return;
1266
+ }
1267
+ // Execute delete process
1268
+ await this.deleteItem(id, label);
1269
+ }
1270
+ async deleteItem(id, label) {
1271
+ if (!this.itemDeleterConfig?.deleter) {
1272
+ return;
1273
+ }
1274
+ try {
1275
+ await this.itemDeleterConfig.deleter(id);
1276
+ }
1277
+ catch (e) {
1278
+ const errorMessage = getErrorMessage(e);
1279
+ this.snackbar.open(`Error: ${errorMessage || this.t('item.errorFailedDelete', { label })}`, undefined, { duration: 3000 });
1280
+ return;
1281
+ }
1282
+ // Show success message
1283
+ this.snackbar.open(this.t('item.successDeleted', { label }), undefined, {
1284
+ duration: 3000,
1285
+ });
1286
+ // Subsequence routing
1287
+ if (this.itemDeleterConfig.afterDeleteNavigateTo) {
1288
+ this.router.navigate([this.itemDeleterConfig.afterDeleteNavigateTo]);
1289
+ }
1290
+ }
1122
1291
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
1123
- 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: [
1292
+ 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", itemDeleterConfig: "itemDeleterConfig" }, providers: [
1124
1293
  provideFormlyCore(withFormlyMaterial()),
1125
1294
  provideFormlyCore({
1126
1295
  wrappers: [
@@ -1146,7 +1315,7 @@ class NgxThinAdminEditor {
1146
1315
  subscriptSizing: 'dynamic',
1147
1316
  },
1148
1317
  },
1149
- ], 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 {{ t('common.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 {{ t('common.edit') }} &nbsp; <span style=\"color: #aaaaaa\">-</span>\n {{ t('editor.idLabel') }}:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n {{ t('common.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 {{ t('common.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 || isSaving) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n }\n\n @if (!isLoading) {\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 {{ t('editor.saveChanges') }}\n } @else {\n {{ t('common.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.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.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" }] });
1318
+ ], 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 {{ t('common.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 {{ t('common.edit') }} &nbsp; <span style=\"color: #aaaaaa\">-</span>\n {{ t('editor.idLabel') }}:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n {{ t('editor.titleForEdit', { itemType: editorItemType }) }}\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 {{ t('editor.titleForCreate', { itemType: editorItemType }) }}\n <!---->\n }\n </h3>\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading || isSaving) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n }\n\n @if (!isLoading) {\n <form [formGroup]=\"form\" (ngSubmit)=\"save()\">\n <formly-form\n [form]=\"form\"\n [fields]=\"$any(editorFields) ?? []\"\n [model]=\"formData\"\n ></formly-form>\n\n <!-- Button Area -->\n <div style=\"display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem\">\n <!-- Delete button (visible only in edit mode and if itemDeleterConfig is provided) -->\n @if (itemDeleterConfig?.deleter && itemId !== undefined) {\n <button\n type=\"button\"\n mat-stroked-button\n color=\"warn\"\n (click)=\"openDeletionDialog()\"\n [disabled]=\"isLoading || isSaving\"\n >\n {{ t('common.delete') }}\n </button>\n } @else {\n <span></span>\n }\n <!---->\n\n <!-- Submit button (visible only if saver is provided) -->\n @if (editorConfig?.saver) {\n <button\n type=\"submit\"\n mat-flat-button\n style=\"color: white; transform: translateY(-0.3rem)\"\n >\n @if (itemId !== undefined) {\n {{ t('editor.saveChanges') }}\n } @else {\n {{ t('common.create') }}\n }\n </button>\n }\n <!---->\n </div>\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.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.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" }] });
1150
1319
  }
1151
1320
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, decorators: [{
1152
1321
  type: Component,
@@ -1184,13 +1353,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1184
1353
  subscriptSizing: 'dynamic',
1185
1354
  },
1186
1355
  },
1187
- ], 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 {{ t('common.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 {{ t('common.edit') }} &nbsp; <span style=\"color: #aaaaaa\">-</span>\n {{ t('editor.idLabel') }}:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n {{ t('common.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 {{ t('common.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 || isSaving) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n }\n\n @if (!isLoading) {\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 {{ t('editor.saveChanges') }}\n } @else {\n {{ t('common.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"] }]
1356
+ ], 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 {{ t('common.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 {{ t('common.edit') }} &nbsp; <span style=\"color: #aaaaaa\">-</span>\n {{ t('editor.idLabel') }}:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n {{ t('editor.titleForEdit', { itemType: editorItemType }) }}\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 {{ t('editor.titleForCreate', { itemType: editorItemType }) }}\n <!---->\n }\n </h3>\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading || isSaving) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n }\n\n @if (!isLoading) {\n <form [formGroup]=\"form\" (ngSubmit)=\"save()\">\n <formly-form\n [form]=\"form\"\n [fields]=\"$any(editorFields) ?? []\"\n [model]=\"formData\"\n ></formly-form>\n\n <!-- Button Area -->\n <div style=\"display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem\">\n <!-- Delete button (visible only in edit mode and if itemDeleterConfig is provided) -->\n @if (itemDeleterConfig?.deleter && itemId !== undefined) {\n <button\n type=\"button\"\n mat-stroked-button\n color=\"warn\"\n (click)=\"openDeletionDialog()\"\n [disabled]=\"isLoading || isSaving\"\n >\n {{ t('common.delete') }}\n </button>\n } @else {\n <span></span>\n }\n <!---->\n\n <!-- Submit button (visible only if saver is provided) -->\n @if (editorConfig?.saver) {\n <button\n type=\"submit\"\n mat-flat-button\n style=\"color: white; transform: translateY(-0.3rem)\"\n >\n @if (itemId !== undefined) {\n {{ t('editor.saveChanges') }}\n } @else {\n {{ t('common.create') }}\n }\n </button>\n }\n <!---->\n </div>\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"] }]
1188
1357
  }], propDecorators: { editorConfig: [{
1189
1358
  type: Input
1190
1359
  }], editorFields: [{
1191
1360
  type: Input
1192
1361
  }], itemId: [{
1193
1362
  type: Input
1363
+ }], itemDeleterConfig: [{
1364
+ type: Input
1194
1365
  }] } });
1195
1366
 
1196
1367
  /*