ngx-thin-admin 0.0.0-alpha.0 → 0.0.0-alpha.10

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';
7
+ import { MatSnackBar } from '@angular/material/snack-bar';
8
+ import * as i6 from '@angular/material/icon';
9
+ import { MatIcon, MatIconModule } from '@angular/material/icon';
8
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
- import * as i2$1 from '@ng-matero/extensions/grid';
15
+ import * as i2$2 from '@ng-matero/extensions/grid';
13
16
  import { MtxGridModule } from '@ng-matero/extensions/grid';
14
- import * as i1$1 from '@angular/forms';
15
- import { FormsModule, FormGroup, ReactiveFormsModule } from '@angular/forms';
17
+ import * as i3 from '@angular/forms';
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
22
  import * as i1 from '@angular/material/form-field';
21
- import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
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, 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: i1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }] });
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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatSelectionList, selector: "mat-selection-list", inputs: ["color", "compareWith", "multiple", "hideSingleSelectionIndicator", "disabled"], outputs: ["selectionChange"], exportAs: ["matSelectionList"] }, { kind: "component", type: MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: MatListOption, selector: "mat-list-option", inputs: ["togglePosition", "checkboxPosition", "color", "value", "selected"], outputs: ["selectedChange"], exportAs: ["matListOption"] }, { kind: "directive", type: MatMenuContent, selector: "ng-template[matMenuContent]" }] });
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$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MtxGridModule }, { kind: "component", type: i2$1.MtxGrid, selector: "mtx-grid", inputs: ["displayedColumns", "columns", "data", "length", "loading", "trackBy", "columnResizable", "emptyValuePlaceholder", "pageOnFront", "showPaginator", "pageDisabled", "showFirstLastButtons", "pageIndex", "pageSize", "pageSizeOptions", "hidePageSize", "paginationTemplate", "sortOnFront", "sortActive", "sortDirection", "sortDisableClear", "sortDisabled", "sortStart", "rowHover", "rowStriped", "expandable", "expansionTemplate", "multiSelectable", "multiSelectionWithClick", "rowSelectable", "hideRowSelectionCheckbox", "disableRowClickSelection", "rowSelectionFormatter", "rowClassFormatter", "rowSelected", "cellSelectable", "showToolbar", "toolbarTitle", "toolbarTemplate", "columnHideable", "columnHideableChecked", "columnSortable", "columnPinnable", "columnPinOptions", "showColumnMenuButton", "columnMenuButtonText", "columnMenuButtonType", "columnMenuButtonColor", "columnMenuButtonClass", "columnMenuButtonIcon", "columnMenuButtonFontIcon", "columnMenuButtonSvgIcon", "showColumnMenuHeader", "columnMenuHeaderText", "columnMenuHeaderTemplate", "showColumnMenuFooter", "columnMenuFooterText", "columnMenuFooterTemplate", "noResultText", "noResultTemplate", "headerTemplate", "headerExtraTemplate", "cellTemplate", "useContentRowTemplate", "useContentHeaderRowTemplate", "useContentFooterRowTemplate", "showSummary", "summaryTemplate", "showSidebar", "sidebarTemplate", "showStatusbar", "statusbarTemplate"], outputs: ["page", "sortChange", "rowClick", "rowContextMenu", "expansionChange", "rowSelectedChange", "cellSelectedChange", "columnChange"], exportAs: ["mtxGrid"] }, { kind: "component", type: MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: ListCategorySelector, selector: "lib-list-category-selector", inputs: ["config", "selectedCategory"], outputs: ["selectedCategoryChange"] }, { kind: "component", type: MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] });
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,25 +1262,99 @@ 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';
1265
+ class FormlyFieldFile extends FieldType {
1266
+ /** 表示用のファイル名 */
1267
+ fileName = '';
1268
+ /** ファイルが選択されたときの処理 */
1269
+ onFileChange(event) {
1270
+ const input = event.target;
1271
+ const file = input.files?.[0] ?? null;
1272
+ // formControl File オブジェクトをセット
1273
+ this.formControl.setValue(file);
1274
+ this.formControl.markAsDirty();
1275
+ // 表示用のファイル名を更新
1276
+ this.fileName = file?.name ?? '';
724
1277
  }
1278
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FormlyFieldFile, deps: null, target: i0.ɵɵFactoryTarget.Component });
1279
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: FormlyFieldFile, isStandalone: true, selector: "formly-field-file", usesInheritance: true, ngImport: i0, template: `
1280
+ <mat-form-field appearance="outline" floatLabel="always" subscriptSizing="dynamic" style="width:100%">
1281
+ <!-- Field label -->
1282
+ @if (props.label) {
1283
+ <mat-label>{{ props.label }}{{ props.required ? ' *' : '' }}</mat-label>
1284
+ }
1285
+
1286
+ <!-- Choosed file name -->
1287
+ <input matInput type="text" readonly [value]="fileName" [formlyAttributes]="field" (click)="fileInput.click()" />
1288
+
1289
+ <!-- File choose button -->
1290
+ <button mat-icon-button matSuffix style="margin-right: 1rem;" type="button" (click)="fileInput.click()">
1291
+ <mat-icon>attach_file</mat-icon>
1292
+ </button>
1293
+
1294
+ <!-- Actual file input (hidden) -->
1295
+ <input
1296
+ type="file"
1297
+ #fileInput
1298
+ style="display: none"
1299
+ [attr.accept]="props['accept'] ?? null"
1300
+ (change)="onFileChange($event)"
1301
+ />
1302
+
1303
+ <!-- Field description -->
1304
+ @if (props.description) {
1305
+ <mat-hint>{{ props.description }}</mat-hint>
1306
+ }
1307
+ </mat-form-field>
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"] }] });
725
1309
  }
1310
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FormlyFieldFile, decorators: [{
1311
+ type: Component,
1312
+ args: [{
1313
+ selector: 'formly-field-file',
1314
+ template: `
1315
+ <mat-form-field appearance="outline" floatLabel="always" subscriptSizing="dynamic" style="width:100%">
1316
+ <!-- Field label -->
1317
+ @if (props.label) {
1318
+ <mat-label>{{ props.label }}{{ props.required ? ' *' : '' }}</mat-label>
1319
+ }
1320
+
1321
+ <!-- Choosed file name -->
1322
+ <input matInput type="text" readonly [value]="fileName" [formlyAttributes]="field" (click)="fileInput.click()" />
1323
+
1324
+ <!-- File choose button -->
1325
+ <button mat-icon-button matSuffix style="margin-right: 1rem;" type="button" (click)="fileInput.click()">
1326
+ <mat-icon>attach_file</mat-icon>
1327
+ </button>
1328
+
1329
+ <!-- Actual file input (hidden) -->
1330
+ <input
1331
+ type="file"
1332
+ #fileInput
1333
+ style="display: none"
1334
+ [attr.accept]="props['accept'] ?? null"
1335
+ (change)="onFileChange($event)"
1336
+ />
1337
+
1338
+ <!-- Field description -->
1339
+ @if (props.description) {
1340
+ <mat-hint>{{ props.description }}</mat-hint>
1341
+ }
1342
+ </mat-form-field>
1343
+ `,
1344
+ standalone: true,
1345
+ imports: [
1346
+ ReactiveFormsModule,
1347
+ FormlyAttributes,
1348
+ MatInputModule,
1349
+ MatIcon,
1350
+ MatIconButton,
1351
+ MatFormField$1,
1352
+ MatLabel,
1353
+ MatHint,
1354
+ MatSuffix$1,
1355
+ ],
1356
+ }]
1357
+ }] });
726
1358
 
727
1359
  class NgxThinAdminEditor {
728
1360
  /**
@@ -737,6 +1369,10 @@ class NgxThinAdminEditor {
737
1369
  * Id of the item being edited.
738
1370
  */
739
1371
  itemId;
1372
+ /**
1373
+ * Config for item deleter
1374
+ */
1375
+ itemDeleterConfig;
740
1376
  /**
741
1377
  * Internal state for form
742
1378
  */
@@ -745,11 +1381,24 @@ class NgxThinAdminEditor {
745
1381
  form = new FormGroup({});
746
1382
  data = {};
747
1383
  formData = {};
1384
+ /**
1385
+ * For history back
1386
+ */
1387
+ windowHistory = window.history;
748
1388
  /**
749
1389
  * Services
750
1390
  */
751
1391
  snackbar = inject(MatSnackBar);
752
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
+ }
753
1402
  ngOnChanges(changes) {
754
1403
  // Fetcher
755
1404
  if (changes.editorConfig &&
@@ -831,7 +1480,7 @@ class NgxThinAdminEditor {
831
1480
  }
832
1481
  async save() {
833
1482
  if (this.form.status === 'INVALID') {
834
- this.snackbar.open('Error: Invalid input', undefined, {
1483
+ this.snackbar.open(`Error: ${this.t('editor.errorInvalidInput')}`, undefined, {
835
1484
  duration: 3000,
836
1485
  });
837
1486
  // Mark field to show validation errors
@@ -842,7 +1491,7 @@ class NgxThinAdminEditor {
842
1491
  return;
843
1492
  }
844
1493
  if (!this.editorConfig?.saver) {
845
- this.snackbar.open('Error: No saver function provided', undefined, {
1494
+ this.snackbar.open(`Error: ${this.t('editor.errorNoSaver')}`, undefined, {
846
1495
  duration: 3000,
847
1496
  });
848
1497
  return;
@@ -852,8 +1501,9 @@ class NgxThinAdminEditor {
852
1501
  this.isSaving = true;
853
1502
  // Call the saver function
854
1503
  let result;
1504
+ let showSuccessMessage = true;
855
1505
  try {
856
- let res = await this.editorConfig?.saver?.(this.formData, this.itemId);
1506
+ let res = await this.editorConfig.saver(this.formData, this.itemId);
857
1507
  if (res instanceof Response) {
858
1508
  if (!res.ok) {
859
1509
  const data = await res.json();
@@ -861,6 +1511,9 @@ class NgxThinAdminEditor {
861
1511
  }
862
1512
  res = await res.json();
863
1513
  }
1514
+ else if (typeof res === 'undefined') {
1515
+ showSuccessMessage = false;
1516
+ }
864
1517
  result = res;
865
1518
  }
866
1519
  catch (e) {
@@ -874,9 +1527,11 @@ class NgxThinAdminEditor {
874
1527
  return;
875
1528
  }
876
1529
  // Show success message
877
- this.snackbar.open('Data saved successfully', undefined, {
878
- duration: 3000,
879
- });
1530
+ if (showSuccessMessage) {
1531
+ this.snackbar.open(this.t('editor.successSaved'), undefined, {
1532
+ duration: 3000,
1533
+ });
1534
+ }
880
1535
  // Update form data with the result from the saver (e.g., to get generated ID)
881
1536
  this.data = { ...result };
882
1537
  this.formData = { ...result };
@@ -887,8 +1542,60 @@ class NgxThinAdminEditor {
887
1542
  }
888
1543
  this.isSaving = false;
889
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
+ }
890
1597
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
891
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: NgxThinAdminEditor, isStandalone: true, selector: "ngx-thin-admin-editor", inputs: { editorConfig: "editorConfig", editorFields: "editorFields", itemId: "itemId" }, providers: [
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: [
892
1599
  provideFormlyCore(withFormlyMaterial()),
893
1600
  provideFormlyCore({
894
1601
  wrappers: [
@@ -898,7 +1605,31 @@ class NgxThinAdminEditor {
898
1605
  },
899
1606
  ],
900
1607
  extensions: [{ name: 'addons', extension: { onPopulate: formlyAddonsExtension } }],
1608
+ types: [
1609
+ {
1610
+ name: 'file',
1611
+ component: FormlyFieldFile,
1612
+ },
1613
+ ],
901
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
+ },
902
1633
  {
903
1634
  // Apply default appearance and other settings to all Material form fields in this component
904
1635
  provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -908,7 +1639,7 @@ class NgxThinAdminEditor {
908
1639
  subscriptSizing: 'dynamic',
909
1640
  },
910
1641
  },
911
- ], usesOnChanges: true, ngImport: i0, template: "<mat-card>\n <mat-card-header>\n <h3>\n @if (itemId !== undefined && $any(data); as editItem) {\n @if (editorConfig?.labelFieldKey && editItem[editorConfig!.labelFieldKey!]) {\n <!-- e.g., \"Edit - Taro (taro) \" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n <span class=\"editor-item-label\">{{ editItem[editorConfig!.labelFieldKey!] }}</span>\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n } @else if (editItem.id) {\n <!-- e.g., \"Edit - ID: 123\" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n ID:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n Edit\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n }\n } @else {\n <!-- Create Account -->\n Create\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n <!---->\n }\n </h3>\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n } @else {\n <form [formGroup]=\"form\" (ngSubmit)=\"save()\">\n <formly-form\n [form]=\"form\"\n [fields]=\"$any(editorFields) ?? []\"\n [model]=\"formData\"\n ></formly-form>\n\n @if (editorConfig?.saver) {\n <!-- Submit button -->\n <button\n type=\"submit\"\n mat-flat-button\n style=\"color: white; transform: translateY(-0.3rem); float: right\"\n >\n @if (itemId !== undefined) {\n Save Changes\n } @else {\n Create\n }\n </button>\n <!---->\n }\n </form>\n }\n </mat-card-content>\n</mat-card>\n", styles: [":host{max-width:600px;display:block;margin:0 auto}mat-card{padding:2rem}mat-card-header h3{font-size:1.2rem;font-weight:400}mat-card-header .editor-item-label{margin-left:1rem}mat-card-header .editor-header-sub{color:#555;margin-left:.85rem;font-size:.9rem;font-style:italic}form{margin-top:2rem}form ::ng-deep .mat-mdc-form-field-subscript-wrapper{margin-bottom:14px!important}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: FormlyForm, selector: "formly-form", inputs: ["form", "model", "fields", "options"], outputs: ["modelChange"] }, { kind: "component", type: MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: MatCardContent, selector: "mat-card-content" }] });
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{--mat-button-filled-container-color: var(--mat-sys-error);--mat-button-filled-label-text-color: var(--mat-sys-on-error)}.footer-buttons .save-button{--mat-button-filled-container-color: var(--mat-sys-primary);--mat-button-filled-label-text-color: var(--mat-sys-on-primary)}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"] }] });
912
1643
  }
913
1644
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: NgxThinAdminEditor, decorators: [{
914
1645
  type: Component,
@@ -916,10 +1647,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
916
1647
  ReactiveFormsModule,
917
1648
  FormlyForm,
918
1649
  MatButton,
1650
+ MatIcon,
1651
+ RouterLink,
919
1652
  MatProgressBar,
920
1653
  MatCard,
921
1654
  MatCardHeader,
922
1655
  MatCardContent,
1656
+ CommonModule,
923
1657
  ], providers: [
924
1658
  provideFormlyCore(withFormlyMaterial()),
925
1659
  provideFormlyCore({
@@ -930,7 +1664,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
930
1664
  },
931
1665
  ],
932
1666
  extensions: [{ name: 'addons', extension: { onPopulate: formlyAddonsExtension } }],
1667
+ types: [
1668
+ {
1669
+ name: 'file',
1670
+ component: FormlyFieldFile,
1671
+ },
1672
+ ],
933
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
+ },
934
1692
  {
935
1693
  // Apply default appearance and other settings to all Material form fields in this component
936
1694
  provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -940,13 +1698,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
940
1698
  subscriptSizing: 'dynamic',
941
1699
  },
942
1700
  },
943
- ], template: "<mat-card>\n <mat-card-header>\n <h3>\n @if (itemId !== undefined && $any(data); as editItem) {\n @if (editorConfig?.labelFieldKey && editItem[editorConfig!.labelFieldKey!]) {\n <!-- e.g., \"Edit - Taro (taro) \" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n <span class=\"editor-item-label\">{{ editItem[editorConfig!.labelFieldKey!] }}</span>\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n } @else if (editItem.id) {\n <!-- e.g., \"Edit - ID: 123\" -->\n Edit &nbsp; <span style=\"color: #aaaaaa\">-</span>\n ID:\n <span class=\"editor-item-label\">{{ editItem.id }}</span>\n <!---->\n } @else {\n <!-- e.g., \"Edit Account (123)\" -->\n Edit\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n @if (editorConfig?.idFieldKey && editItem[editorConfig!.idFieldKey!]) {\n <small class=\"editor-header-sub\">{{ editItem[editorConfig!.idFieldKey!] }}</small>\n }\n <!---->\n }\n } @else {\n <!-- Create Account -->\n Create\n @if (editorConfig?.singularLabel) {\n {{ editorConfig?.singularLabel }}\n }\n <!---->\n }\n </h3>\n </mat-card-header>\n\n <mat-card-content>\n @if (isLoading) {\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n } @else {\n <form [formGroup]=\"form\" (ngSubmit)=\"save()\">\n <formly-form\n [form]=\"form\"\n [fields]=\"$any(editorFields) ?? []\"\n [model]=\"formData\"\n ></formly-form>\n\n @if (editorConfig?.saver) {\n <!-- Submit button -->\n <button\n type=\"submit\"\n mat-flat-button\n style=\"color: white; transform: translateY(-0.3rem); float: right\"\n >\n @if (itemId !== undefined) {\n Save Changes\n } @else {\n Create\n }\n </button>\n <!---->\n }\n </form>\n }\n </mat-card-content>\n</mat-card>\n", styles: [":host{max-width:600px;display:block;margin:0 auto}mat-card{padding:2rem}mat-card-header h3{font-size:1.2rem;font-weight:400}mat-card-header .editor-item-label{margin-left:1rem}mat-card-header .editor-header-sub{color:#555;margin-left:.85rem;font-size:.9rem;font-style:italic}form{margin-top:2rem}form ::ng-deep .mat-mdc-form-field-subscript-wrapper{margin-bottom:14px!important}\n"] }]
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{--mat-button-filled-container-color: var(--mat-sys-error);--mat-button-filled-label-text-color: var(--mat-sys-on-error)}.footer-buttons .save-button{--mat-button-filled-container-color: var(--mat-sys-primary);--mat-button-filled-label-text-color: var(--mat-sys-on-primary)}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"] }]
944
1702
  }], propDecorators: { editorConfig: [{
945
1703
  type: Input
946
1704
  }], editorFields: [{
947
1705
  type: Input
948
1706
  }], itemId: [{
949
1707
  type: Input
1708
+ }], itemDeleterConfig: [{
1709
+ type: Input
950
1710
  }] } });
951
1711
 
952
1712
  /*
@@ -957,5 +1717,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
957
1717
  * Generated bundle index. Do not edit.
958
1718
  */
959
1719
 
960
- export { NgxThinAdminEditor, NgxThinAdminList };
1720
+ export { NGX_THIN_ADMIN_EXTRA_FORMLY_CONFIG, NGX_THIN_ADMIN_TRANSLATE, NgxThinAdminEditor, NgxThinAdminList, provideNgxThinAdminFormlyConfig, provideNgxThinAdminI18n };
961
1721
  //# sourceMappingURL=ngx-thin-admin.mjs.map