ngx-thin-admin 0.0.0-alpha.1 → 0.0.0-alpha.11

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.
@@ -1,35 +1,190 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Component, EventEmitter, ChangeDetectorRef, Output, Input, Injectable, ViewChild } from '@angular/core';
2
+ import { InjectionToken, inject, Component, EventEmitter, ChangeDetectorRef, Output, Input, Injectable, ViewChild } from '@angular/core';
3
3
  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 { MatIcon } from '@angular/material/icon';
8
- import * as i2$1 from '@angular/material/input';
7
+ import { MatSnackBar } from '@angular/material/snack-bar';
8
+ import * as i6 from '@angular/material/icon';
9
+ import { MatIcon, MatIconModule } from '@angular/material/icon';
10
+ import * as i2 from '@angular/material/input';
9
11
  import { MatInputModule, MatInput, MatPrefix, MatSuffix } from '@angular/material/input';
10
- import { MatFormField } from '@angular/material/select';
12
+ import * as i2$1 from '@angular/material/select';
13
+ import { MatSelectModule, MatFormField } from '@angular/material/select';
11
14
  import { MatTooltip } from '@angular/material/tooltip';
12
15
  import * as i2$2 from '@ng-matero/extensions/grid';
13
16
  import { MtxGridModule } from '@ng-matero/extensions/grid';
14
- import * as i1 from '@angular/forms';
17
+ import * as i3 from '@angular/forms';
15
18
  import { FormsModule, ReactiveFormsModule, FormGroup } from '@angular/forms';
16
19
  import { MatSelectionList, MatListOption } from '@angular/material/list';
17
20
  import { MatMenu, MatMenuItem, MatMenuTrigger, MatMenuContent } from '@angular/material/menu';
18
- import { MatSnackBar } from '@angular/material/snack-bar';
19
21
  import { MatDialogRef, MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog } from '@angular/material/dialog';
20
- import * as i2 from '@angular/material/form-field';
22
+ import * as i1 from '@angular/material/form-field';
21
23
  import { MatFormFieldModule, MatFormField as MatFormField$1, MatLabel, MatHint, MatSuffix as MatSuffix$1, MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
22
24
  import { stringify } from 'csv-stringify/browser/esm/sync';
23
- import { RouterLink } from '@angular/router';
25
+ import { RouterLink, Router } from '@angular/router';
26
+ import * as i5 from '@angular/material/progress-spinner';
27
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
24
28
  import { MatCard, MatCardHeader, MatCardContent } from '@angular/material/card';
25
- import { FieldWrapper, FieldType, FormlyAttributes, FormlyForm, provideFormlyCore } from '@ngx-formly/core';
29
+ import { FieldWrapper, FieldType, FormlyAttributes, FormlyForm, FORMLY_CONFIG, provideFormlyCore } from '@ngx-formly/core';
26
30
  import { withFormlyMaterial } from '@ngx-formly/material';
27
31
  import { MatProgressBar } from '@angular/material/progress-bar';
32
+ import * as i2$3 from '@angular/common';
33
+ import { CommonModule } from '@angular/common';
34
+
35
+ const messagesEn = {
36
+ 'common.cancel': 'Cancel',
37
+ 'common.ok': 'OK',
38
+ 'common.save': 'Save',
39
+ 'common.create': 'Create',
40
+ 'common.edit': 'Edit',
41
+ 'common.delete': 'Delete',
42
+ 'common.back': 'Back',
43
+ 'list.defaultTitle': 'List',
44
+ 'list.clearFilters': 'Clear filters',
45
+ 'list.searchDefaultPlaceholder': 'Search...',
46
+ 'list.searchInCategoryPlaceholder': 'Search in {category}...',
47
+ 'list.clearSearchKeyword': 'Clear search keyword',
48
+ 'list.refresh': 'Refresh',
49
+ 'list.exportAsCsv': 'Export as CSV',
50
+ 'list.exportedFileName': 'exported',
51
+ 'list.exportItems': 'Export {count} items',
52
+ 'editor.titleForCreate': 'Create {itemType}',
53
+ 'editor.titleForEdit': 'Edit {itemType}',
54
+ 'editor.idLabel': 'ID',
55
+ 'editor.saveForCreate': 'Save',
56
+ 'editor.saveForEdit': 'Save Changes',
57
+ 'editor.errorInvalidInput': 'Invalid input',
58
+ 'editor.errorNoSaver': 'No saver function provided',
59
+ 'editor.successSaved': 'Data saved successfully',
60
+ 'editor.backToList': 'To List',
61
+ 'category.defaultSingular': 'Category',
62
+ 'category.defaultPlural': 'Categories',
63
+ 'category.all': 'All',
64
+ 'category.editorDialogTitleForCreate': 'Create {categoryType}',
65
+ 'category.editorDialogTitleForEdit': 'Edit {categoryType}',
66
+ 'category.deletionDialogTitle': 'Delete {categoryType}',
67
+ 'category.deletionDialogMessage': 'Are you sure you want to delete the {categoryType} "{label}"? This action cannot be undone.',
68
+ 'category.errorCouldNotCreate': 'Could not create {categoryType}',
69
+ 'category.errorFailedUpdate': 'Failed to update {categoryType} "{label}"',
70
+ 'category.errorFailedDelete': 'Failed to delete {categoryType} "{label}"',
71
+ 'category.errorCouldNotSave': 'Could not save {categoryType}',
72
+ 'category.successCreated': '{categoryType} "{label}" has been created successfully',
73
+ 'category.successUpdated': '{categoryType} "{label}" has been updated successfully',
74
+ 'category.successDeleted': '{categoryType} "{label}" has been deleted successfully',
75
+ 'item.defaultSingular': 'Item',
76
+ 'item.deletionDialogTitle': 'Delete {itemType}',
77
+ 'item.deletionDialogMessage': 'Are you sure you want to delete "{label}" ({id})? This action cannot be undone.',
78
+ 'item.errorFailedDelete': 'Failed to delete "{label}"',
79
+ 'item.successDeleted': '"{label}" has been deleted successfully',
80
+ 'item.changeCategory': 'Change {categoryType}',
81
+ 'item.changeCategoryDialogTitle': 'Change {categoryType}',
82
+ 'item.changeCategoryDialogMessage': 'Select a {categoryType} to move "{label}" to.',
83
+ 'item.successCategoryChanged': '{categoryType} changed successfully',
84
+ 'item.errorFailedCategoryChange': 'Failed to change {categoryType} for "{label}"',
85
+ 'category.unselected': 'Uncategorized',
86
+ 'csv.exporting': 'Exporting CSV...',
87
+ 'csv.exportCompleted': 'CSV export completed. Exported {count} items.',
88
+ 'csv.exportCompletedWithErrors': 'CSV exported up to {count} items, but errors occurred: {error}',
89
+ 'csv.errorGenerateFailed': 'Failed to generate CSV file.',
90
+ };
91
+ const messagesJa = {
92
+ 'common.cancel': 'キャンセル',
93
+ 'common.ok': 'OK',
94
+ 'common.save': '保存',
95
+ 'common.create': '作成',
96
+ 'common.edit': '編集',
97
+ 'common.delete': '削除',
98
+ 'common.back': '戻る',
99
+ 'list.defaultTitle': '一覧',
100
+ 'list.clearFilters': 'フィルターをクリア',
101
+ 'list.searchDefaultPlaceholder': '検索...',
102
+ 'list.searchInCategoryPlaceholder': '{category} 内を検索...',
103
+ 'list.clearSearchKeyword': '検索キーワードをクリア',
104
+ 'list.refresh': '再読み込み',
105
+ 'list.exportAsCsv': 'CSV でエクスポート',
106
+ 'list.exportedFileName': 'exported',
107
+ 'list.exportItems': '{count} 件をエクスポート',
108
+ 'editor.titleForCreate': '{itemType}の作成',
109
+ 'editor.titleForEdit': '{itemType}の編集',
110
+ 'editor.idLabel': 'ID',
111
+ 'editor.saveForCreate': '作成',
112
+ 'editor.saveForEdit': '変更を保存',
113
+ 'editor.errorInvalidInput': '入力内容が不正です',
114
+ 'editor.errorNoSaver': '保存処理が設定されていません',
115
+ 'editor.successSaved': '保存しました',
116
+ 'editor.backToList': '一覧へ',
117
+ 'category.defaultSingular': 'カテゴリ',
118
+ 'category.defaultPlural': 'カテゴリ',
119
+ 'category.all': 'すべて',
120
+ 'category.editorDialogTitleForCreate': '{categoryType}の作成',
121
+ 'category.editorDialogTitleForEdit': '{categoryType}の編集',
122
+ 'category.deletionDialogTitle': '{categoryType}の削除',
123
+ 'category.deletionDialogMessage': '{categoryType} "{label}" を削除しますか?この操作は元に戻せません。',
124
+ 'category.errorCouldNotCreate': '{categoryType} を作成できませんでした',
125
+ 'category.errorFailedUpdate': '{categoryType} "{label}" の更新に失敗しました',
126
+ 'category.errorFailedDelete': '{categoryType} "{label}" の削除に失敗しました',
127
+ 'category.errorCouldNotSave': '{categoryType} を保存できませんでした',
128
+ 'category.successCreated': '{categoryType} "{label}" を作成しました',
129
+ 'category.successUpdated': '{categoryType} "{label}" を更新しました',
130
+ 'category.successDeleted': '{categoryType} "{label}" を削除しました',
131
+ 'item.defaultSingular': '項目',
132
+ 'item.deletionDialogTitle': '{itemType}の削除',
133
+ 'item.deletionDialogMessage': '"{label}" ({id}) を削除しますか?この操作は元に戻せません。',
134
+ 'item.errorFailedDelete': '"{label}" の削除に失敗しました',
135
+ 'item.successDeleted': '"{label}" を削除しました',
136
+ 'item.changeCategory': '{categoryType}を変更',
137
+ 'item.changeCategoryDialogTitle': '{categoryType}の変更',
138
+ 'item.changeCategoryDialogMessage': '"{label}" の移動先を選択してください',
139
+ 'item.successCategoryChanged': '{categoryType}を変更しました',
140
+ 'item.errorFailedCategoryChange': '「{label}」の{categoryType}変更に失敗しました',
141
+ 'category.unselected': '未分類',
142
+ 'csv.exporting': 'CSV をエクスポート中...',
143
+ 'csv.exportCompleted': 'CSV エクスポートが完了しました。{count} 件をエクスポートしました。',
144
+ 'csv.exportCompletedWithErrors': '{count} 件までエクスポートしましたが、エラーが発生しました: {error}',
145
+ 'csv.errorGenerateFailed': 'CSV ファイルの生成に失敗しました。',
146
+ };
147
+ function interpolate(message, params) {
148
+ if (!params) {
149
+ return message;
150
+ }
151
+ return message.replace(/\{(\w+)\}/g, (_, key) => {
152
+ const value = params[key];
153
+ return value === undefined ? '' : String(value);
154
+ });
155
+ }
156
+ function createTranslatorFromMessages(baseMessages, overrides) {
157
+ const merged = { ...baseMessages, ...overrides };
158
+ return (key, params) => {
159
+ return interpolate(merged[key] ?? messagesEn[key], params);
160
+ };
161
+ }
162
+ const NGX_THIN_ADMIN_TRANSLATE = new InjectionToken('NGX_THIN_ADMIN_TRANSLATE', {
163
+ providedIn: 'root',
164
+ factory: () => createTranslatorFromMessages(messagesEn),
165
+ });
166
+ function provideNgxThinAdminI18n(config) {
167
+ return {
168
+ provide: NGX_THIN_ADMIN_TRANSLATE,
169
+ useFactory: () => {
170
+ if (config?.translate) {
171
+ return config.translate;
172
+ }
173
+ const locale = config?.locale ?? 'en';
174
+ const base = locale === 'ja' ? messagesJa : messagesEn;
175
+ return createTranslatorFromMessages(base, config?.messages);
176
+ },
177
+ };
178
+ }
28
179
 
29
180
  class CategoryEditorDialog {
30
181
  dialogRef = inject((MatDialogRef));
31
182
  data = inject(MAT_DIALOG_DATA);
183
+ translate = inject(NGX_THIN_ADMIN_TRANSLATE);
32
184
  categoryLabel;
185
+ t(key, params) {
186
+ return this.translate(key, params);
187
+ }
33
188
  save() {
34
189
  if (!this.categoryLabel) {
35
190
  return;
@@ -39,7 +194,7 @@ class CategoryEditorDialog {
39
194
  });
40
195
  }
41
196
  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: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2$1.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.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.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"] }] });
197
+ 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 {{\n t('category.editorDialogTitleForEdit', {\n categoryType: data.config.singularLabel ?? t('category.defaultSingular'),\n })\n }}\n } @else {\n {{\n t('category.editorDialogTitleForCreate', {\n categoryType: data.config.singularLabel ?? t('category.defaultSingular'),\n })\n }}\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 ?? t('common.cancel') }}\n </button>\n <button type=\"submit\" matButton>{{ data.positiveButtonText ?? t('common.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: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.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: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3.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
198
  }
44
199
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CategoryEditorDialog, decorators: [{
45
200
  type: Component,
@@ -52,14 +207,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
52
207
  MatDialogContent,
53
208
  MatDialogActions,
54
209
  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"] }]
210
+ ], template: "<h2 mat-dialog-title>\n @if (data.editCategory) {\n {{\n t('category.editorDialogTitleForEdit', {\n categoryType: data.config.singularLabel ?? t('category.defaultSingular'),\n })\n }}\n } @else {\n {{\n t('category.editorDialogTitleForCreate', {\n categoryType: data.config.singularLabel ?? t('category.defaultSingular'),\n })\n }}\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 ?? t('common.cancel') }}\n </button>\n <button type=\"submit\" matButton>{{ data.positiveButtonText ?? t('common.save') }}</button>\n </mat-dialog-actions>\n</form>\n", styles: [".cancel-btn{--mat-button-text-label-text-color: #333}\n"] }]
56
211
  }] });
57
212
 
58
213
  class ConfirmDialog {
59
214
  dialogRef = inject((MatDialogRef));
60
215
  data = inject(MAT_DIALOG_DATA);
216
+ translate = inject(NGX_THIN_ADMIN_TRANSLATE);
217
+ t(key, params) {
218
+ return this.translate(key, params);
219
+ }
61
220
  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"] }] });
221
+ 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 || t('common.cancel') }}\n </button>\n <button matButton [mat-dialog-close]=\"true\" cdkFocusInitial>\n {{ data.positiveButtonText || t('common.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
222
  }
64
223
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ConfirmDialog, decorators: [{
65
224
  type: Component,
@@ -70,9 +229,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
70
229
  MatDialogContent,
71
230
  MatDialogActions,
72
231
  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"] }]
232
+ ], 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 || t('common.cancel') }}\n </button>\n <button matButton [mat-dialog-close]=\"true\" cdkFocusInitial>\n {{ data.positiveButtonText || t('common.ok') }}\n </button>\n</mat-dialog-actions>\n", styles: [".cancel-btn{--mat-button-text-label-text-color: #333}\n"] }]
74
233
  }] });
75
234
 
235
+ function getErrorMessage(error) {
236
+ if (typeof error === 'string') {
237
+ return error;
238
+ }
239
+ else if (error instanceof Error) {
240
+ return error.message;
241
+ }
242
+ else if (error?.error?.message) {
243
+ if (Array.isArray(error.error.message)) {
244
+ return error.error.message[0];
245
+ }
246
+ else {
247
+ return error.error.message;
248
+ }
249
+ }
250
+ else {
251
+ return 'An unknown error occurred';
252
+ }
253
+ }
254
+
76
255
  class ListCategorySelector {
77
256
  /**
78
257
  * Config for the category selector
@@ -96,6 +275,10 @@ class ListCategorySelector {
96
275
  snackbar = inject(MatSnackBar);
97
276
  dialog = inject(MatDialog);
98
277
  cdr = inject(ChangeDetectorRef);
278
+ translate = inject(NGX_THIN_ADMIN_TRANSLATE);
279
+ t(key, params) {
280
+ return this.translate(key, params);
281
+ }
99
282
  ngOnChanges(changes) {
100
283
  if (changes.config &&
101
284
  changes.config.currentValue?.fetcher !== changes.config.previousValue?.fetcher) {
@@ -127,13 +310,25 @@ class ListCategorySelector {
127
310
  }
128
311
  // Call the category creator function
129
312
  try {
130
- await this.config.creator(categoryLabel);
313
+ const res = await this.config.creator(categoryLabel);
314
+ if (res && 'ok' in res && !res.ok) {
315
+ const errorMessage = getErrorMessage(res);
316
+ throw new Error(errorMessage);
317
+ }
131
318
  }
132
319
  catch (e) {
133
- this.snackbar.open(`Error: ${e?.message ?? 'Could not create category'}`, undefined, {
320
+ this.snackbar.open(`Error: ${e?.message ?? this.t('category.errorCouldNotCreate', { categoryType: this.config.singularLabel ?? this.t('category.defaultSingular') })}`, undefined, {
134
321
  duration: 3000,
135
322
  });
323
+ return;
136
324
  }
325
+ // Show success message
326
+ this.snackbar.open(this.t('category.successCreated', {
327
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
328
+ label: categoryLabel,
329
+ }), undefined, {
330
+ duration: 3000,
331
+ });
137
332
  // Refresh category list
138
333
  this.fetchCategories();
139
334
  }
@@ -143,13 +338,29 @@ class ListCategorySelector {
143
338
  }
144
339
  // Call the category updater function
145
340
  try {
146
- await this.config.updater(category);
341
+ const res = await this.config.updater(category);
342
+ if (res && 'ok' in res && !res.ok) {
343
+ const errorMessage = getErrorMessage(res);
344
+ throw new Error(errorMessage);
345
+ }
147
346
  }
148
347
  catch (e) {
149
- this.snackbar.open(`Error: ${e?.message ?? `Failed to update category "${category.label}"`}`, undefined, {
348
+ this.snackbar.open(`Error: ${e?.message ??
349
+ this.t('category.errorFailedUpdate', {
350
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
351
+ label: category.label,
352
+ })}`, undefined, {
150
353
  duration: 3000,
151
354
  });
355
+ return;
152
356
  }
357
+ // Show success message
358
+ this.snackbar.open(this.t('category.successUpdated', {
359
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
360
+ label: category.label,
361
+ }), undefined, {
362
+ duration: 3000,
363
+ });
153
364
  // Refresh category list
154
365
  this.fetchCategories();
155
366
  }
@@ -159,13 +370,29 @@ class ListCategorySelector {
159
370
  }
160
371
  // Call the category deleter function
161
372
  try {
162
- await this.config.deleter(category);
373
+ const res = await this.config.deleter(category);
374
+ if (res && 'ok' in res && !res.ok) {
375
+ const errorMessage = getErrorMessage(res);
376
+ throw new Error(errorMessage);
377
+ }
163
378
  }
164
379
  catch (e) {
165
- this.snackbar.open(`Error: ${e?.message ?? `Failed to delete category: "${category.label}"`}`, undefined, {
380
+ this.snackbar.open(`Error: ${e?.message ??
381
+ this.t('category.errorFailedDelete', {
382
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
383
+ label: category.label,
384
+ })}`, undefined, {
166
385
  duration: 3000,
167
386
  });
387
+ return;
168
388
  }
389
+ // Show success message
390
+ this.snackbar.open(this.t('category.successDeleted', {
391
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
392
+ label: category.label,
393
+ }), undefined, {
394
+ duration: 3000,
395
+ });
169
396
  // Refresh category list
170
397
  this.fetchCategories();
171
398
  }
@@ -176,8 +403,8 @@ class ListCategorySelector {
176
403
  data: {
177
404
  config: this.config,
178
405
  editCategory: editCategory,
179
- positiveButtonText: editCategory ? 'Save' : 'Create',
180
- cancelButtonText: 'Cancel',
406
+ positiveButtonText: editCategory ? this.t('common.save') : this.t('common.create'),
407
+ cancelButtonText: this.t('common.cancel'),
181
408
  },
182
409
  });
183
410
  const result = await lastValueFrom(dialogRef.afterClosed());
@@ -194,7 +421,10 @@ class ListCategorySelector {
194
421
  }
195
422
  }
196
423
  catch (e) {
197
- this.snackbar.open(`Error: ${e?.message ?? 'Could not save category'}`, undefined, {
424
+ this.snackbar.open(`Error: ${e?.message ??
425
+ this.t('category.errorCouldNotSave', {
426
+ categoryType: this.config.singularLabel ?? this.t('category.defaultSingular'),
427
+ })}`, undefined, {
198
428
  duration: 3000,
199
429
  });
200
430
  }
@@ -206,10 +436,15 @@ class ListCategorySelector {
206
436
  const dialogRef = this.dialog.open(ConfirmDialog, {
207
437
  width: '400px',
208
438
  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',
439
+ title: this.t('category.deletionDialogTitle', {
440
+ categoryType: this.config?.singularLabel ?? this.t('category.defaultSingular'),
441
+ }),
442
+ message: this.t('category.deletionDialogMessage', {
443
+ categoryType: this.config?.singularLabel ?? this.t('category.defaultSingular'),
444
+ label: category.label,
445
+ }),
446
+ positiveButtonText: this.t('common.delete'),
447
+ cancelButtonText: this.t('common.cancel'),
213
448
  },
214
449
  });
215
450
  const result = await lastValueFrom(dialogRef.afterClosed());
@@ -221,7 +456,11 @@ class ListCategorySelector {
221
456
  await this.deleteCategory(category);
222
457
  }
223
458
  catch (e) {
224
- this.snackbar.open(`Error: ${e?.message ?? `Failed to delete category "${category.label}"`}`, undefined, {
459
+ this.snackbar.open(`Error: ${e?.message ??
460
+ this.t('category.errorFailedDelete', {
461
+ categoryType: this.config?.singularLabel ?? this.t('category.defaultSingular'),
462
+ label: category.label,
463
+ })}`, undefined, {
225
464
  duration: 3000,
226
465
  });
227
466
  }
@@ -229,7 +468,7 @@ class ListCategorySelector {
229
468
  this.fetchCategories();
230
469
  }
231
470
  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.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: "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]" }] });
471
+ 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\">\n {{ config?.title ?? t('category.defaultPlural') }}\n </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 {{ t('category.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)\">\n {{ t('common.edit') }}\n </button>\n }\n @if (config?.deleter) {\n <button mat-menu-item (click)=\"openCategoryDeletionDialog(category)\">\n {{ t('common.delete') }}\n </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: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.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
472
  }
234
473
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ListCategorySelector, decorators: [{
235
474
  type: Component,
@@ -243,7 +482,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
243
482
  MatMenuTrigger,
244
483
  MatListOption,
245
484
  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"] }]
485
+ ], template: "<header>\n <!-- Category selector title -->\n <span class=\"category-selector-title\">\n {{ config?.title ?? t('category.defaultPlural') }}\n </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 {{ t('category.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)\">\n {{ t('common.edit') }}\n </button>\n }\n @if (config?.deleter) {\n <button mat-menu-item (click)=\"openCategoryDeletionDialog(category)\">\n {{ t('common.delete') }}\n </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
486
  }], propDecorators: { config: [{
248
487
  type: Input
249
488
  }], selectedCategory: [{
@@ -254,6 +493,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
254
493
 
255
494
  class CsvExportService {
256
495
  snackbar = inject(MatSnackBar);
496
+ translate = inject(NGX_THIN_ADMIN_TRANSLATE);
257
497
  encoders = [
258
498
  {
259
499
  key: 'utf8',
@@ -310,8 +550,8 @@ class CsvExportService {
310
550
  // Initialize
311
551
  const items = [];
312
552
  let errorMessage = undefined;
313
- // Show processing message
314
- const mes = this.snackbar.open('Exporting CSV...');
553
+ // 処理中メッセージを表示
554
+ const mes = this.snackbar.open(this.translate('csv.exporting'));
315
555
  // Replace characters in the file name prefix that cannot be used in file names
316
556
  fileNamePrefix = fileNamePrefix.replace(/[/¥\\*?:"<>|@\s;^.]/g, '_');
317
557
  // Truncate the file name prefix if it is too long
@@ -405,7 +645,7 @@ class CsvExportService {
405
645
  if (!blob) {
406
646
  console.error(`exportListAsCsv - Blob is not created`);
407
647
  mes.dismiss();
408
- this.snackbar.open(`Failed to generate CSV file.`, undefined, {
648
+ this.snackbar.open(this.translate('csv.errorGenerateFailed'), undefined, {
409
649
  duration: 5000,
410
650
  });
411
651
  return;
@@ -420,14 +660,17 @@ class CsvExportService {
420
660
  URL.revokeObjectURL(objectUrl);
421
661
  // Dissmiss processing message
422
662
  mes.dismiss();
423
- // Show result message
663
+ // 結果メッセージを表示
424
664
  if (errorMessage) {
425
- this.snackbar.open(`CSV exported up to ${numOfExportedItems} items. but errors occurred: ${errorMessage}`, undefined, {
665
+ this.snackbar.open(this.translate('csv.exportCompletedWithErrors', {
666
+ count: numOfExportedItems,
667
+ error: errorMessage,
668
+ }), undefined, {
426
669
  duration: 5000,
427
670
  });
428
671
  }
429
672
  else {
430
- this.snackbar.open(`CSV export completed. Exported ${numOfExportedItems} items.`, undefined, {
673
+ this.snackbar.open(this.translate('csv.exportCompleted', { count: numOfExportedItems }), undefined, {
431
674
  duration: 3000,
432
675
  });
433
676
  }
@@ -442,6 +685,100 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
442
685
  }]
443
686
  }], ctorParameters: () => [] });
444
687
 
688
+ class CategoryChangeDialog {
689
+ dialogRef = inject((MatDialogRef));
690
+ data = inject(MAT_DIALOG_DATA);
691
+ translate = inject(NGX_THIN_ADMIN_TRANSLATE);
692
+ cdr = inject(ChangeDetectorRef);
693
+ dialog = inject(MatDialog);
694
+ snackbar = inject(MatSnackBar);
695
+ categories = [];
696
+ selectedCategoryId;
697
+ isLoading = false;
698
+ t(key, params) {
699
+ return this.translate(key, params);
700
+ }
701
+ async ngOnInit() {
702
+ this.selectedCategoryId = this.data.currentCategoryId;
703
+ this.isLoading = true;
704
+ this.cdr.markForCheck();
705
+ try {
706
+ const result = await Promise.resolve(this.data.categorySelectorConfig.fetcher());
707
+ this.categories = result.categories;
708
+ }
709
+ catch (e) {
710
+ console.error('Failed to fetch categories:', e);
711
+ }
712
+ finally {
713
+ this.isLoading = false;
714
+ this.cdr.markForCheck();
715
+ }
716
+ }
717
+ save() {
718
+ this.dialogRef.close({
719
+ categoryId: this.selectedCategoryId,
720
+ });
721
+ }
722
+ async createNewCategory() {
723
+ if (!this.data.categorySelectorConfig.creator)
724
+ return;
725
+ const dialogRef = this.dialog.open(CategoryEditorDialog, {
726
+ width: '400px',
727
+ data: {
728
+ config: this.data.categorySelectorConfig,
729
+ editCategory: undefined,
730
+ positiveButtonText: this.t('common.create'),
731
+ cancelButtonText: this.t('common.cancel'),
732
+ },
733
+ });
734
+ const result = await lastValueFrom(dialogRef.afterClosed());
735
+ if (!result || !result.label) {
736
+ return;
737
+ }
738
+ this.isLoading = true;
739
+ this.cdr.markForCheck();
740
+ try {
741
+ const newCategory = await Promise.resolve(this.data.categorySelectorConfig.creator(result.label));
742
+ this.snackbar.open(this.t('category.successCreated', {
743
+ categoryType: this.data.categorySelectorConfig.singularLabel ?? this.t('category.defaultSingular'),
744
+ label: result.label,
745
+ }), undefined, { duration: 3000 });
746
+ // Re-fetch categories
747
+ const fetchResult = await Promise.resolve(this.data.categorySelectorConfig.fetcher());
748
+ this.categories = fetchResult.categories;
749
+ // Auto-select the newly created category
750
+ this.selectedCategoryId = newCategory.id;
751
+ }
752
+ catch (e) {
753
+ this.snackbar.open(`Error: ${e?.message ??
754
+ this.t('category.errorCouldNotSave', {
755
+ categoryType: this.data.categorySelectorConfig.singularLabel ?? this.t('category.defaultSingular'),
756
+ })}`, undefined, { duration: 3000 });
757
+ }
758
+ finally {
759
+ this.isLoading = false;
760
+ this.cdr.markForCheck();
761
+ }
762
+ }
763
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CategoryChangeDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
764
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: CategoryChangeDialog, isStandalone: true, selector: "lib-category-change-dialog", ngImport: i0, template: "<h2 mat-dialog-title>\n {{\n t('item.changeCategoryDialogTitle', {\n categoryType: data.categorySelectorConfig.singularLabel || t('category.defaultSingular'),\n })\n }}\n</h2>\n\n<mat-dialog-content>\n @if (data.itemLabel) {\n <p style=\"margin-top: 5px; margin-bottom: 1rem\">\n {{\n t('item.changeCategoryDialogMessage', {\n label: data.itemLabel,\n categoryType: data.categorySelectorConfig.singularLabel || t('category.defaultSingular'),\n })\n }}\n </p>\n }\n\n <mat-form-field appearance=\"outline\" style=\"width: 100%; margin-top: 10px\">\n <mat-label>{{\n data.categorySelectorConfig.singularLabel || t('category.defaultSingular')\n }}</mat-label>\n <mat-select [(ngModel)]=\"selectedCategoryId\" [disabled]=\"isLoading\">\n <mat-option [value]=\"undefined\">{{ t('category.unselected') }}</mat-option>\n @for (category of categories; track category.id) {\n <mat-option [value]=\"category.id\">{{ category.label }}</mat-option>\n }\n </mat-select>\n @if (isLoading) {\n <mat-spinner matSuffix diameter=\"20\" style=\"margin-right: 12px\"></mat-spinner>\n }\n </mat-form-field>\n</mat-dialog-content>\n\n<mat-dialog-actions>\n @if (data.categorySelectorConfig.creator) {\n <button mat-button (click)=\"createNewCategory()\" [disabled]=\"isLoading\">\n <mat-icon style=\"margin-right: 4px; font-size: 1.2rem; width: 1.2rem; height: 1.2rem\"\n >add</mat-icon\n >\n <span>{{ t('common.create') }}</span>\n </button>\n }\n\n <span style=\"flex: 1\"></span>\n\n <button mat-button mat-dialog-close>\n {{ data.cancelButtonText || t('common.cancel') }}\n </button>\n <button mat-flat-button color=\"primary\" (click)=\"save()\" [disabled]=\"isLoading\">\n {{ data.positiveButtonText || t('common.save') }}\n </button>\n</mat-dialog-actions>\n", styles: [""], 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: "directive", type: i1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i2$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i2$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i5.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i6.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] });
765
+ }
766
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CategoryChangeDialog, decorators: [{
767
+ type: Component,
768
+ args: [{ selector: 'lib-category-change-dialog', imports: [
769
+ MatFormFieldModule,
770
+ MatSelectModule,
771
+ FormsModule,
772
+ MatButtonModule,
773
+ MatDialogTitle,
774
+ MatDialogContent,
775
+ MatDialogActions,
776
+ MatDialogClose,
777
+ MatProgressSpinnerModule,
778
+ MatIconModule,
779
+ ], template: "<h2 mat-dialog-title>\n {{\n t('item.changeCategoryDialogTitle', {\n categoryType: data.categorySelectorConfig.singularLabel || t('category.defaultSingular'),\n })\n }}\n</h2>\n\n<mat-dialog-content>\n @if (data.itemLabel) {\n <p style=\"margin-top: 5px; margin-bottom: 1rem\">\n {{\n t('item.changeCategoryDialogMessage', {\n label: data.itemLabel,\n categoryType: data.categorySelectorConfig.singularLabel || t('category.defaultSingular'),\n })\n }}\n </p>\n }\n\n <mat-form-field appearance=\"outline\" style=\"width: 100%; margin-top: 10px\">\n <mat-label>{{\n data.categorySelectorConfig.singularLabel || t('category.defaultSingular')\n }}</mat-label>\n <mat-select [(ngModel)]=\"selectedCategoryId\" [disabled]=\"isLoading\">\n <mat-option [value]=\"undefined\">{{ t('category.unselected') }}</mat-option>\n @for (category of categories; track category.id) {\n <mat-option [value]=\"category.id\">{{ category.label }}</mat-option>\n }\n </mat-select>\n @if (isLoading) {\n <mat-spinner matSuffix diameter=\"20\" style=\"margin-right: 12px\"></mat-spinner>\n }\n </mat-form-field>\n</mat-dialog-content>\n\n<mat-dialog-actions>\n @if (data.categorySelectorConfig.creator) {\n <button mat-button (click)=\"createNewCategory()\" [disabled]=\"isLoading\">\n <mat-icon style=\"margin-right: 4px; font-size: 1.2rem; width: 1.2rem; height: 1.2rem\"\n >add</mat-icon\n >\n <span>{{ t('common.create') }}</span>\n </button>\n }\n\n <span style=\"flex: 1\"></span>\n\n <button mat-button mat-dialog-close>\n {{ data.cancelButtonText || t('common.cancel') }}\n </button>\n <button mat-flat-button color=\"primary\" (click)=\"save()\" [disabled]=\"isLoading\">\n {{ data.positiveButtonText || t('common.save') }}\n </button>\n</mat-dialog-actions>\n" }]
780
+ }] });
781
+
445
782
  class NgxThinAdminList {
446
783
  /**
447
784
  * Config for list
@@ -451,6 +788,11 @@ class NgxThinAdminList {
451
788
  * Config for category selector. If provided, a category selector will be displayed in the UI.
452
789
  */
453
790
  categorySelectorConfig;
791
+ /**
792
+ * Config for item deleter (If provided, item deletion feature is enabled)
793
+ * It works by calling openItemDeletionDialog(row) from the click handler of the column definition.
794
+ */
795
+ itemDeleterConfig;
454
796
  /**
455
797
  * Column schema (extended type of MtxGridColumn)
456
798
  * @see https://ng-matero.github.io/extensions/components/grid/api
@@ -465,6 +807,10 @@ class NgxThinAdminList {
465
807
  perPage: 10,
466
808
  categoryId: undefined,
467
809
  };
810
+ /**
811
+ * Output query
812
+ */
813
+ queryChange = new EventEmitter();
468
814
  /**
469
815
  * Fetcher function to retrieve list data.
470
816
  * This should be provided as part of listConfig.
@@ -488,6 +834,7 @@ class NgxThinAdminList {
488
834
  keywordChanged$ = new Subject();
489
835
  ngOnInit() {
490
836
  this.keywordChanged$.pipe(debounceTime(500)).subscribe(() => {
837
+ this.queryChange.emit(this.query);
491
838
  this.fetchList();
492
839
  });
493
840
  }
@@ -495,20 +842,30 @@ class NgxThinAdminList {
495
842
  * Template for cells
496
843
  */
497
844
  cellTplWithLink;
845
+ /**
846
+ * Category Selector
847
+ */
848
+ categorySelector;
498
849
  /**
499
850
  * Services
500
851
  */
501
852
  cdr = inject(ChangeDetectorRef);
853
+ translate = inject(NGX_THIN_ADMIN_TRANSLATE);
854
+ snackbar = inject(MatSnackBar);
502
855
  csvExportService = inject(CsvExportService);
856
+ dialog = inject(MatDialog);
857
+ t(key, params) {
858
+ return this.translate(key, params);
859
+ }
503
860
  ngOnChanges(changes) {
504
861
  // 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
- });
862
+ if (changes.listColumns?.currentValue || changes.listConfig?.currentValue) {
863
+ this.buildGridColumns();
864
+ }
865
+ // Default page size
866
+ if (changes.listConfig?.currentValue?.pageSize !== undefined &&
867
+ changes.listConfig.currentValue.pageSize !== changes.listConfig.previousValue?.pageSize) {
868
+ this.query.perPage = changes.listConfig.currentValue.pageSize;
512
869
  }
513
870
  // List - trigger strictly when listFetcher is newly provided or explicitly changed
514
871
  if (changes.listConfig?.currentValue?.fetcher !== changes.listConfig?.previousValue?.fetcher) {
@@ -516,6 +873,48 @@ class NgxThinAdminList {
516
873
  this.fetchList();
517
874
  }
518
875
  }
876
+ buildGridColumns() {
877
+ if (!this.listColumns) {
878
+ this.gridColumns = [];
879
+ return;
880
+ }
881
+ let columns = this.listColumns.map((col) => {
882
+ if (col.link || col.routerLink) {
883
+ return { ...col, cellTemplate: this.cellTplWithLink };
884
+ }
885
+ return col;
886
+ });
887
+ if (this.listConfig?.categoryChanger && this.categorySelectorConfig) {
888
+ const actionsColIndex = columns.findIndex((col) => col.field === 'actions' && col.type === 'button');
889
+ const categoryType = this.categorySelectorConfig.singularLabel || this.t('category.defaultSingular');
890
+ const changeCategoryBtn = {
891
+ type: 'icon',
892
+ icon: 'folder_shared',
893
+ tooltip: this.t('item.changeCategory', { categoryType }),
894
+ click: (record) => this.openItemCategoryChangeDialog(record),
895
+ };
896
+ if (actionsColIndex >= 0) {
897
+ const col = { ...columns[actionsColIndex] };
898
+ const originalButtons = col.buttons;
899
+ if (typeof originalButtons === 'function') {
900
+ col.buttons = (record) => [...originalButtons(record), changeCategoryBtn];
901
+ }
902
+ else {
903
+ col.buttons = [...(originalButtons || []), changeCategoryBtn];
904
+ }
905
+ columns[actionsColIndex] = col;
906
+ }
907
+ else {
908
+ columns.push({
909
+ header: 'Actions',
910
+ field: 'actions',
911
+ type: 'button',
912
+ buttons: [changeCategoryBtn],
913
+ });
914
+ }
915
+ }
916
+ this.gridColumns = columns;
917
+ }
519
918
  async fetchList() {
520
919
  if (!this.listFetcher) {
521
920
  return;
@@ -541,7 +940,11 @@ class NgxThinAdminList {
541
940
  clearFilters() {
542
941
  this.query.keyword = '';
543
942
  this.query.categoryId = undefined;
943
+ this.query.filterColumn = undefined;
944
+ this.query.filterValue = undefined;
945
+ this.query.filterLabel = undefined;
544
946
  this.selectedCategory = [];
947
+ this.queryChange.emit(this.query);
545
948
  this.fetchList();
546
949
  }
547
950
  onKeywordInput() {
@@ -552,7 +955,9 @@ class NgxThinAdminList {
552
955
  return;
553
956
  }
554
957
  // Determine file name prefix
555
- let fileNamePrefix = this.listConfig?.title ? this.listConfig.title : 'exported';
958
+ let fileNamePrefix = this.listConfig?.title
959
+ ? this.listConfig.title
960
+ : this.t('list.exportedFileName');
556
961
  if (this.selectedCategory && this.selectedCategory[0]?.id) {
557
962
  // If a category is selected, include the category label in the file name prefix
558
963
  fileNamePrefix += `_${this.selectedCategory[0]?.label}`;
@@ -567,19 +972,144 @@ class NgxThinAdminList {
567
972
  keyword: this.query.keyword,
568
973
  }, this.listColumns);
569
974
  }
975
+ getColumnLabel(columnName) {
976
+ const column = this.listColumns?.find((col) => col.field === columnName);
977
+ if (column?.header) {
978
+ return column.header;
979
+ }
980
+ return columnName;
981
+ }
982
+ getColumnValueLabel(columnName, value) {
983
+ const column = this.listColumns?.find((col) => col.field === columnName);
984
+ if (!column) {
985
+ return value;
986
+ }
987
+ if (this.gridData.length == 0) {
988
+ return value;
989
+ }
990
+ const item = this.gridData.find((item) => String(item[columnName]) === String(value));
991
+ if (item) {
992
+ if (column.formatter) {
993
+ return column.formatter(item, column);
994
+ }
995
+ return item[columnName];
996
+ }
997
+ return value;
998
+ }
570
999
  onPageChange(event) {
571
1000
  this.query.page = event.pageIndex;
572
1001
  this.query.perPage = event.pageSize;
1002
+ this.queryChange.emit(this.query);
573
1003
  this.fetchList();
574
1004
  }
575
1005
  onSelectedCategoryChange($event) {
576
1006
  console.log('Selected category changed:', $event);
577
1007
  const categoryId = $event && $event.length >= 1 && $event[0] ? $event[0].id : undefined;
578
1008
  this.query.categoryId = categoryId;
1009
+ this.queryChange.emit(this.query);
1010
+ this.fetchList();
1011
+ }
1012
+ async openItemDeletionDialog(item) {
1013
+ if (!this.itemDeleterConfig?.deleter) {
1014
+ return;
1015
+ }
1016
+ // Get the label and ID to query in the confirmation dialog
1017
+ const idKey = this.listConfig?.idFieldKey;
1018
+ const labelKey = this.listConfig?.labelFieldKey;
1019
+ const id = idKey ? item[idKey] : (item.id ?? JSON.stringify(item));
1020
+ const label = labelKey ? item[labelKey] : id;
1021
+ const itemType = this.listConfig?.singularLabel || this.t('item.defaultSingular');
1022
+ // Open the confirmation dialog
1023
+ const dialogRef = this.dialog.open(ConfirmDialog, {
1024
+ width: '400px',
1025
+ data: {
1026
+ title: this.t('item.deletionDialogTitle', { itemType }),
1027
+ message: this.t('item.deletionDialogMessage', { label, id }),
1028
+ positiveButtonText: this.t('common.delete'),
1029
+ cancelButtonText: this.t('common.cancel'),
1030
+ },
1031
+ });
1032
+ const result = await lastValueFrom(dialogRef.afterClosed());
1033
+ if (!result) {
1034
+ return;
1035
+ }
1036
+ // Execute delete process
1037
+ await this.deleteItem(id, label);
1038
+ }
1039
+ async deleteItem(id, label) {
1040
+ if (!this.itemDeleterConfig?.deleter) {
1041
+ return;
1042
+ }
1043
+ try {
1044
+ await this.itemDeleterConfig.deleter(id);
1045
+ }
1046
+ catch (e) {
1047
+ const errorMessage = getErrorMessage(e);
1048
+ this.snackbar.open(`Error: ${errorMessage || this.t('item.errorFailedDelete', { label })}`, undefined, { duration: 3000 });
1049
+ return;
1050
+ }
1051
+ // Show success message
1052
+ this.snackbar.open(this.t('item.successDeleted', { label }), undefined, {
1053
+ duration: 3000,
1054
+ });
1055
+ // Refresh the list after deletion
1056
+ this.fetchList();
1057
+ }
1058
+ async openItemCategoryChangeDialog(item) {
1059
+ if (!this.listConfig?.categoryChanger || !this.categorySelectorConfig) {
1060
+ return;
1061
+ }
1062
+ const idKey = this.listConfig?.idFieldKey;
1063
+ const labelKey = this.listConfig?.labelFieldKey;
1064
+ const id = idKey ? item[idKey] : (item.id ?? JSON.stringify(item));
1065
+ const label = labelKey ? item[labelKey] : id;
1066
+ let currentCategoryId;
1067
+ if (typeof this.listConfig.categoryFieldKey === 'function') {
1068
+ currentCategoryId = this.listConfig.categoryFieldKey(item);
1069
+ }
1070
+ else if (typeof this.listConfig.categoryFieldKey === 'string') {
1071
+ currentCategoryId = item[this.listConfig.categoryFieldKey];
1072
+ }
1073
+ else {
1074
+ currentCategoryId = item.categoryId ?? item.category?.id;
1075
+ }
1076
+ const dialogRef = this.dialog.open(CategoryChangeDialog, {
1077
+ width: '400px',
1078
+ data: {
1079
+ itemLabel: label,
1080
+ categorySelectorConfig: this.categorySelectorConfig,
1081
+ currentCategoryId: currentCategoryId,
1082
+ positiveButtonText: this.t('common.save'),
1083
+ cancelButtonText: this.t('common.cancel'),
1084
+ },
1085
+ });
1086
+ const result = await lastValueFrom(dialogRef.afterClosed());
1087
+ if (!result) {
1088
+ return;
1089
+ }
1090
+ const newCategoryId = result.categoryId;
1091
+ const categoryType = this.categorySelectorConfig.singularLabel || this.t('category.defaultSingular');
1092
+ try {
1093
+ const res = await this.listConfig.categoryChanger(id, newCategoryId);
1094
+ if (res && 'ok' in res && !res.ok) {
1095
+ const errorMessage = getErrorMessage(res);
1096
+ throw new Error(errorMessage);
1097
+ }
1098
+ }
1099
+ catch (e) {
1100
+ const errorMessage = getErrorMessage(e);
1101
+ this.snackbar.open(`Error: ${errorMessage || this.t('item.errorFailedCategoryChange', { label, categoryType })}`, undefined, { duration: 3000 });
1102
+ return;
1103
+ }
1104
+ this.snackbar.open(this.t('item.successCategoryChanged', { label, categoryType }), undefined, {
1105
+ duration: 3000,
1106
+ });
1107
+ // Refresh the list and category selector
579
1108
  this.fetchList();
1109
+ this.categorySelector?.fetchCategories();
580
1110
  }
581
1111
  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.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"] }] });
1112
+ 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" }, outputs: { queryChange: "queryChange" }, viewQueries: [{ propertyName: "cellTplWithLink", first: true, predicate: ["cellTplWithLink"], descendants: true, static: true }, { propertyName: "categorySelector", first: true, predicate: ListCategorySelector, descendants: 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 || (query.filterColumn && query.filterValue)) {\n <!-- Title (with link for reset filter) -->\n <a\n href=\"javascript:void(0)\"\n (click)=\"clearFilters()\"\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.filterColumn && query.filterValue) {\n <span class=\"material-icons list-title-separator\"> keyboard_arrow_right </span>\n <!-- Custom Filter -->\n {{ query.filterLabel ?? getColumnLabel(query.filterColumn) }}:\n {{ getColumnValueLabel(query.filterColumn, query.filterValue) }}\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 || (query.filterColumn && query.filterValue)) {\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 ?? t('common.create') }}\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 ?? t('common.create') }}\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 ?? t('common.create') }}\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 @if (col.formatter) {\n {{ col.formatter(row) }}\n } @else {\n {{ row[col.field] }}\n }\n </a>\n } @else if (col.routerLink) {\n @let link = col.routerLink(row);\n @let fragment = link.fragment ? link.fragment : link;\n <a [routerLink]=\"fragment\">\n @if (col.formatter) {\n {{ col.formatter(row) }}\n } @else {\n {{ row[col.field] }}\n }\n </a>\n } @else {\n @if (col.formatter) {\n {{ col.formatter(row) }}\n } @else {\n {{ row[col.field] }}\n }\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: i3.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: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.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"] }] });
583
1113
  }
584
1114
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminList, decorators: [{
585
1115
  type: Component,
@@ -599,20 +1129,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
599
1129
  MatMenuItem,
600
1130
  MatMenuTrigger,
601
1131
  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"] }]
1132
+ ], 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 || (query.filterColumn && query.filterValue)) {\n <!-- Title (with link for reset filter) -->\n <a\n href=\"javascript:void(0)\"\n (click)=\"clearFilters()\"\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.filterColumn && query.filterValue) {\n <span class=\"material-icons list-title-separator\"> keyboard_arrow_right </span>\n <!-- Custom Filter -->\n {{ query.filterLabel ?? getColumnLabel(query.filterColumn) }}:\n {{ getColumnValueLabel(query.filterColumn, query.filterValue) }}\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 || (query.filterColumn && query.filterValue)) {\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 ?? t('common.create') }}\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 ?? t('common.create') }}\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 ?? t('common.create') }}\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 @if (col.formatter) {\n {{ col.formatter(row) }}\n } @else {\n {{ row[col.field] }}\n }\n </a>\n } @else if (col.routerLink) {\n @let link = col.routerLink(row);\n @let fragment = link.fragment ? link.fragment : link;\n <a [routerLink]=\"fragment\">\n @if (col.formatter) {\n {{ col.formatter(row) }}\n } @else {\n {{ row[col.field] }}\n }\n </a>\n } @else {\n @if (col.formatter) {\n {{ col.formatter(row) }}\n } @else {\n {{ row[col.field] }}\n }\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"] }]
603
1133
  }], propDecorators: { listConfig: [{
604
1134
  type: Input
605
1135
  }], categorySelectorConfig: [{
606
1136
  type: Input
1137
+ }], itemDeleterConfig: [{
1138
+ type: Input
607
1139
  }], listColumns: [{
608
1140
  type: Input
609
1141
  }], query: [{
610
1142
  type: Input
1143
+ }], queryChange: [{
1144
+ type: Output
611
1145
  }], cellTplWithLink: [{
612
1146
  type: ViewChild,
613
1147
  args: ['cellTplWithLink', { static: true }]
1148
+ }], categorySelector: [{
1149
+ type: ViewChild,
1150
+ args: [ListCategorySelector]
614
1151
  }] } });
615
1152
 
1153
+ /**
1154
+ * Injection token for providing extra formly config to NgxThinAdminEditor.
1155
+ * Use provideNgxThinAdminFormlyConfig() to register from the consumer app.
1156
+ */
1157
+ const NGX_THIN_ADMIN_EXTRA_FORMLY_CONFIG = new InjectionToken('NGX_THIN_ADMIN_EXTRA_FORMLY_CONFIG');
1158
+ /**
1159
+ * Returns a provider that adds custom formly types, wrappers, etc. to NgxThinAdminEditor.
1160
+ *
1161
+ * @example
1162
+ * provideNgxThinAdminFormlyConfig({
1163
+ * types: [{ name: 'qrcode', component: QrcodeFieldComponent }],
1164
+ * })
1165
+ */
1166
+ function provideNgxThinAdminFormlyConfig(config) {
1167
+ return {
1168
+ provide: NGX_THIN_ADMIN_EXTRA_FORMLY_CONFIG,
1169
+ useValue: config,
1170
+ multi: true,
1171
+ };
1172
+ }
1173
+
616
1174
  class FormlyMatPrefixAddonWrapper extends FieldWrapper {
617
1175
  matPrefix;
618
1176
  matSuffix;
@@ -704,26 +1262,6 @@ function formlyAddonsExtension(field) {
704
1262
  }
705
1263
  }
706
1264
 
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
1265
  class FormlyFieldFile extends FieldType {
728
1266
  /** 表示用のファイル名 */
729
1267
  fileName = '';
@@ -767,7 +1305,7 @@ class FormlyFieldFile extends FieldType {
767
1305
  <mat-hint>{{ props.description }}</mat-hint>
768
1306
  }
769
1307
  </mat-form-field>
770
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: FormlyAttributes, selector: "[formlyAttributes]", inputs: ["formlyAttributes", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2$1.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: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { 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"] }] });
1308
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: FormlyAttributes, selector: "[formlyAttributes]", inputs: ["formlyAttributes", "id"] }, { 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: "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: "directive", type: i1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { 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"] }] });
771
1309
  }
772
1310
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FormlyFieldFile, decorators: [{
773
1311
  type: Component,
@@ -831,6 +1369,10 @@ class NgxThinAdminEditor {
831
1369
  * Id of the item being edited.
832
1370
  */
833
1371
  itemId;
1372
+ /**
1373
+ * Config for item deleter
1374
+ */
1375
+ itemDeleterConfig;
834
1376
  /**
835
1377
  * Internal state for form
836
1378
  */
@@ -839,11 +1381,24 @@ class NgxThinAdminEditor {
839
1381
  form = new FormGroup({});
840
1382
  data = {};
841
1383
  formData = {};
1384
+ /**
1385
+ * For history back
1386
+ */
1387
+ windowHistory = window.history;
842
1388
  /**
843
1389
  * Services
844
1390
  */
845
1391
  snackbar = inject(MatSnackBar);
846
1392
  cdr = inject(ChangeDetectorRef);
1393
+ translate = inject(NGX_THIN_ADMIN_TRANSLATE);
1394
+ dialog = inject(MatDialog);
1395
+ router = inject(Router);
1396
+ t(key, params) {
1397
+ return this.translate(key, params);
1398
+ }
1399
+ get editorItemType() {
1400
+ return this.editorConfig?.singularLabel || this.t('item.defaultSingular');
1401
+ }
847
1402
  ngOnChanges(changes) {
848
1403
  // Fetcher
849
1404
  if (changes.editorConfig &&
@@ -925,7 +1480,7 @@ class NgxThinAdminEditor {
925
1480
  }
926
1481
  async save() {
927
1482
  if (this.form.status === 'INVALID') {
928
- this.snackbar.open('Error: Invalid input', undefined, {
1483
+ this.snackbar.open(`Error: ${this.t('editor.errorInvalidInput')}`, undefined, {
929
1484
  duration: 3000,
930
1485
  });
931
1486
  // Mark field to show validation errors
@@ -936,7 +1491,7 @@ class NgxThinAdminEditor {
936
1491
  return;
937
1492
  }
938
1493
  if (!this.editorConfig?.saver) {
939
- this.snackbar.open('Error: No saver function provided', undefined, {
1494
+ this.snackbar.open(`Error: ${this.t('editor.errorNoSaver')}`, undefined, {
940
1495
  duration: 3000,
941
1496
  });
942
1497
  return;
@@ -946,8 +1501,9 @@ class NgxThinAdminEditor {
946
1501
  this.isSaving = true;
947
1502
  // Call the saver function
948
1503
  let result;
1504
+ let showSuccessMessage = true;
949
1505
  try {
950
- let res = await this.editorConfig?.saver?.(this.formData, this.itemId);
1506
+ let res = await this.editorConfig.saver(this.formData, this.itemId);
951
1507
  if (res instanceof Response) {
952
1508
  if (!res.ok) {
953
1509
  const data = await res.json();
@@ -955,6 +1511,9 @@ class NgxThinAdminEditor {
955
1511
  }
956
1512
  res = await res.json();
957
1513
  }
1514
+ else if (typeof res === 'undefined') {
1515
+ showSuccessMessage = false;
1516
+ }
958
1517
  result = res;
959
1518
  }
960
1519
  catch (e) {
@@ -968,9 +1527,11 @@ class NgxThinAdminEditor {
968
1527
  return;
969
1528
  }
970
1529
  // Show success message
971
- this.snackbar.open('Data saved successfully', undefined, {
972
- duration: 3000,
973
- });
1530
+ if (showSuccessMessage) {
1531
+ this.snackbar.open(this.t('editor.successSaved'), undefined, {
1532
+ duration: 3000,
1533
+ });
1534
+ }
974
1535
  // Update form data with the result from the saver (e.g., to get generated ID)
975
1536
  this.data = { ...result };
976
1537
  this.formData = { ...result };
@@ -981,8 +1542,60 @@ class NgxThinAdminEditor {
981
1542
  }
982
1543
  this.isSaving = false;
983
1544
  }
1545
+ async openDeletionDialog() {
1546
+ if (!this.itemDeleterConfig?.deleter || this.itemId === undefined) {
1547
+ return;
1548
+ }
1549
+ // Get the label and ID to query in the confirmation dialog
1550
+ const id = this.itemId;
1551
+ const label = this.editorConfig?.labelFieldKey
1552
+ ? this.data[this.editorConfig.labelFieldKey]
1553
+ : id;
1554
+ const itemType = this.editorItemType;
1555
+ // Open the confirmation dialog
1556
+ const dialogRef = this.dialog.open(ConfirmDialog, {
1557
+ width: '400px',
1558
+ data: {
1559
+ title: this.t('item.deletionDialogTitle', { itemType }),
1560
+ message: this.t('item.deletionDialogMessage', { label, id }),
1561
+ positiveButtonText: this.t('common.delete'),
1562
+ cancelButtonText: this.t('common.cancel'),
1563
+ },
1564
+ });
1565
+ const result = await lastValueFrom(dialogRef.afterClosed());
1566
+ if (!result) {
1567
+ return;
1568
+ }
1569
+ // Execute delete process
1570
+ await this.deleteItem(id, label);
1571
+ }
1572
+ async deleteItem(id, label) {
1573
+ if (!this.itemDeleterConfig?.deleter) {
1574
+ return;
1575
+ }
1576
+ try {
1577
+ const res = await this.itemDeleterConfig.deleter(id);
1578
+ if (res && 'ok' in res && !res.ok) {
1579
+ const errorMessage = getErrorMessage(res);
1580
+ throw new Error(errorMessage);
1581
+ }
1582
+ }
1583
+ catch (e) {
1584
+ const errorMessage = getErrorMessage(e);
1585
+ this.snackbar.open(`Error: ${errorMessage || this.t('item.errorFailedDelete', { label })}`, undefined, { duration: 3000 });
1586
+ return;
1587
+ }
1588
+ // Show success message
1589
+ this.snackbar.open(this.t('item.successDeleted', { label }), undefined, {
1590
+ duration: 3000,
1591
+ });
1592
+ // Subsequence routing
1593
+ if (this.itemDeleterConfig.afterDeleteNavigateTo) {
1594
+ this.router.navigate([this.itemDeleterConfig.afterDeleteNavigateTo]);
1595
+ }
1596
+ }
984
1597
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
985
- 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: [
1598
+ 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: [
986
1599
  provideFormlyCore(withFormlyMaterial()),
987
1600
  provideFormlyCore({
988
1601
  wrappers: [
@@ -999,6 +1612,24 @@ class NgxThinAdminEditor {
999
1612
  },
1000
1613
  ],
1001
1614
  }),
1615
+ {
1616
+ provide: FORMLY_CONFIG,
1617
+ multi: true,
1618
+ useFactory: () => {
1619
+ const configs = inject(NGX_THIN_ADMIN_EXTRA_FORMLY_CONFIG, {
1620
+ optional: true,
1621
+ skipSelf: true,
1622
+ });
1623
+ if (!configs || configs.length === 0)
1624
+ return {};
1625
+ return configs.reduce((acc, c) => ({
1626
+ types: [...(acc.types ?? []), ...(c.types ?? [])],
1627
+ wrappers: [...(acc.wrappers ?? []), ...(c.wrappers ?? [])],
1628
+ validators: [...(acc.validators ?? []), ...(c.validators ?? [])],
1629
+ extensions: [...(acc.extensions ?? []), ...(c.extensions ?? [])],
1630
+ }), {});
1631
+ },
1632
+ },
1002
1633
  {
1003
1634
  // Apply default appearance and other settings to all Material form fields in this component
1004
1635
  provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -1008,7 +1639,7 @@ class NgxThinAdminEditor {
1008
1639
  subscriptSizing: 'dynamic',
1009
1640
  },
1010
1641
  },
1011
- ], 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.ɵ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" }] });
1642
+ ], usesOnChanges: true, ngImport: i0, template: "<mat-card>\n <mat-card-header style=\"position: relative\">\n <!-- Back button -->\n @if (editorConfig?.backButton) {\n @if ($any(editorConfig?.backButton).historyBack && windowHistory.length > 1) {\n <button mat-button (click)=\"windowHistory.back()\" class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </button>\n } @else if ($any(editorConfig?.backButton).routerLink; as backButtonLink) {\n <a [routerLink]=\"backButtonLink\" mat-button class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </a>\n } @else if ($any(editorConfig?.backButton).link; as backButtonLink) {\n <a [href]=\"backButtonLink\" mat-button class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </a>\n } @else if ($any(editorConfig?.backButton).click; as handler) {\n <button mat-button (click)=\"handler()\" class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </button>\n }\n }\n <!---->\n\n <!-- Editor title -->\n <h3 style=\"margin: 0\" [style.margin-top]=\"editorConfig?.backButton ? '1rem' : '0'\">\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 <!---->\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading || isSaving) {\n <mat-progress-bar mode=\"indeterminate\" style=\"margin-top: 1rem\"></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\n class=\"footer-buttons\"\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 0.5rem;\n gap: 1rem;\n \"\n >\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 class=\"delete-button\"\n matButton=\"filled\"\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 <!-- Custom footer buttons -->\n @if (editorConfig?.footerButtons) {\n @for (buttonConfig of editorConfig?.footerButtons; track buttonConfig.label) {\n @if (buttonConfig.show ? buttonConfig.show($any(formData), itemId) : true) {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"buttonConfig.click($any(formData), itemId)\"\n [disabled]=\"isLoading || isSaving\"\n [ngClass]=\"buttonConfig.className\"\n >\n {{ buttonConfig.label }}\n </button>\n }\n }\n }\n <!---->\n\n <span style=\"flex: 1\"></span>\n\n <!-- Submit button (visible only if saver is provided) -->\n @if (editorConfig?.saver) {\n <button\n type=\"submit\"\n class=\"save-button\"\n matButton=\"filled\"\n style=\"color: white; transform: translateY(-0.3rem)\"\n >\n @if (itemId !== undefined) {\n {{ t('editor.saveForEdit') }}\n } @else {\n {{ t('editor.saveForCreate') }}\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}.back-link{display:flex;position:absolute;top:-1.5rem;left:-1rem;align-items:center;gap:.3rem;font-size:.8rem}.back-link mat-icon{height:.8rem;width:.8rem;font-size:.8rem}.footer-buttons .delete-button{background-color:#c1312e}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: ReactiveFormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.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: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { 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" }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
1012
1643
  }
1013
1644
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, decorators: [{
1014
1645
  type: Component,
@@ -1016,10 +1647,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1016
1647
  ReactiveFormsModule,
1017
1648
  FormlyForm,
1018
1649
  MatButton,
1650
+ MatIcon,
1651
+ RouterLink,
1019
1652
  MatProgressBar,
1020
1653
  MatCard,
1021
1654
  MatCardHeader,
1022
1655
  MatCardContent,
1656
+ CommonModule,
1023
1657
  ], providers: [
1024
1658
  provideFormlyCore(withFormlyMaterial()),
1025
1659
  provideFormlyCore({
@@ -1037,6 +1671,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1037
1671
  },
1038
1672
  ],
1039
1673
  }),
1674
+ {
1675
+ provide: FORMLY_CONFIG,
1676
+ multi: true,
1677
+ useFactory: () => {
1678
+ const configs = inject(NGX_THIN_ADMIN_EXTRA_FORMLY_CONFIG, {
1679
+ optional: true,
1680
+ skipSelf: true,
1681
+ });
1682
+ if (!configs || configs.length === 0)
1683
+ return {};
1684
+ return configs.reduce((acc, c) => ({
1685
+ types: [...(acc.types ?? []), ...(c.types ?? [])],
1686
+ wrappers: [...(acc.wrappers ?? []), ...(c.wrappers ?? [])],
1687
+ validators: [...(acc.validators ?? []), ...(c.validators ?? [])],
1688
+ extensions: [...(acc.extensions ?? []), ...(c.extensions ?? [])],
1689
+ }), {});
1690
+ },
1691
+ },
1040
1692
  {
1041
1693
  // Apply default appearance and other settings to all Material form fields in this component
1042
1694
  provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -1046,13 +1698,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1046
1698
  subscriptSizing: 'dynamic',
1047
1699
  },
1048
1700
  },
1049
- ], 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"] }]
1701
+ ], template: "<mat-card>\n <mat-card-header style=\"position: relative\">\n <!-- Back button -->\n @if (editorConfig?.backButton) {\n @if ($any(editorConfig?.backButton).historyBack && windowHistory.length > 1) {\n <button mat-button (click)=\"windowHistory.back()\" class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </button>\n } @else if ($any(editorConfig?.backButton).routerLink; as backButtonLink) {\n <a [routerLink]=\"backButtonLink\" mat-button class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </a>\n } @else if ($any(editorConfig?.backButton).link; as backButtonLink) {\n <a [href]=\"backButtonLink\" mat-button class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </a>\n } @else if ($any(editorConfig?.backButton).click; as handler) {\n <button mat-button (click)=\"handler()\" class=\"back-link\">\n <mat-icon>arrow_back_ios_new</mat-icon>\n {{ editorConfig?.backButton?.label ?? t('editor.backToList') }}\n </button>\n }\n }\n <!---->\n\n <!-- Editor title -->\n <h3 style=\"margin: 0\" [style.margin-top]=\"editorConfig?.backButton ? '1rem' : '0'\">\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 <!---->\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading || isSaving) {\n <mat-progress-bar mode=\"indeterminate\" style=\"margin-top: 1rem\"></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\n class=\"footer-buttons\"\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 0.5rem;\n gap: 1rem;\n \"\n >\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 class=\"delete-button\"\n matButton=\"filled\"\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 <!-- Custom footer buttons -->\n @if (editorConfig?.footerButtons) {\n @for (buttonConfig of editorConfig?.footerButtons; track buttonConfig.label) {\n @if (buttonConfig.show ? buttonConfig.show($any(formData), itemId) : true) {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"buttonConfig.click($any(formData), itemId)\"\n [disabled]=\"isLoading || isSaving\"\n [ngClass]=\"buttonConfig.className\"\n >\n {{ buttonConfig.label }}\n </button>\n }\n }\n }\n <!---->\n\n <span style=\"flex: 1\"></span>\n\n <!-- Submit button (visible only if saver is provided) -->\n @if (editorConfig?.saver) {\n <button\n type=\"submit\"\n class=\"save-button\"\n matButton=\"filled\"\n style=\"color: white; transform: translateY(-0.3rem)\"\n >\n @if (itemId !== undefined) {\n {{ t('editor.saveForEdit') }}\n } @else {\n {{ t('editor.saveForCreate') }}\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}.back-link{display:flex;position:absolute;top:-1.5rem;left:-1rem;align-items:center;gap:.3rem;font-size:.8rem}.back-link mat-icon{height:.8rem;width:.8rem;font-size:.8rem}.footer-buttons .delete-button{background-color:#c1312e}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"] }]
1050
1702
  }], propDecorators: { editorConfig: [{
1051
1703
  type: Input
1052
1704
  }], editorFields: [{
1053
1705
  type: Input
1054
1706
  }], itemId: [{
1055
1707
  type: Input
1708
+ }], itemDeleterConfig: [{
1709
+ type: Input
1056
1710
  }] } });
1057
1711
 
1058
1712
  /*
@@ -1063,5 +1717,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1063
1717
  * Generated bundle index. Do not edit.
1064
1718
  */
1065
1719
 
1066
- export { NgxThinAdminEditor, NgxThinAdminList };
1720
+ export { NGX_THIN_ADMIN_EXTRA_FORMLY_CONFIG, NGX_THIN_ADMIN_TRANSLATE, NgxThinAdminEditor, NgxThinAdminList, provideNgxThinAdminFormlyConfig, provideNgxThinAdminI18n };
1067
1721
  //# sourceMappingURL=ngx-thin-admin.mjs.map