@flusys/ng-shared 0.1.0-alpha.1
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.
- package/fesm2022/flusys-ng-shared.mjs +1543 -0
- package/fesm2022/flusys-ng-shared.mjs.map +1 -0
- package/package.json +26 -0
- package/types/flusys-ng-shared.d.ts +1107 -0
|
@@ -0,0 +1,1543 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output, HostListener, NgModule, resource, Component, Injector, model, runInInjectionContext, untracked, forwardRef, viewChild, ChangeDetectionStrategy, InjectionToken } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { isPlatformServer, CommonModule, NgOptimizedImage, NgComponentOutlet, DatePipe } from '@angular/common';
|
|
5
|
+
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
6
|
+
import { APP_CONFIG, getServiceUrl, ApiLoaderService } from '@flusys/ng-core';
|
|
7
|
+
import { of, firstValueFrom, skip, debounceTime, distinctUntilChanged, tap as tap$1 } from 'rxjs';
|
|
8
|
+
import { tap, catchError, map } from 'rxjs/operators';
|
|
9
|
+
import * as i4 from '@angular/forms';
|
|
10
|
+
import { NgControl, ReactiveFormsModule, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
|
+
import { RouterOutlet, RouterLink, Router } from '@angular/router';
|
|
12
|
+
import { AutoCompleteModule } from 'primeng/autocomplete';
|
|
13
|
+
import { ButtonModule } from 'primeng/button';
|
|
14
|
+
import { CardModule } from 'primeng/card';
|
|
15
|
+
import * as i2 from 'primeng/checkbox';
|
|
16
|
+
import { CheckboxModule } from 'primeng/checkbox';
|
|
17
|
+
import { DatePickerModule } from 'primeng/datepicker';
|
|
18
|
+
import { DialogModule } from 'primeng/dialog';
|
|
19
|
+
import { DividerModule } from 'primeng/divider';
|
|
20
|
+
import { FileUploadModule } from 'primeng/fileupload';
|
|
21
|
+
import { IconFieldModule } from 'primeng/iconfield';
|
|
22
|
+
import { ImageModule } from 'primeng/image';
|
|
23
|
+
import { InputIconModule } from 'primeng/inputicon';
|
|
24
|
+
import { InputNumberModule } from 'primeng/inputnumber';
|
|
25
|
+
import * as i1$1 from 'primeng/inputtext';
|
|
26
|
+
import { InputTextModule } from 'primeng/inputtext';
|
|
27
|
+
import { ListboxModule } from 'primeng/listbox';
|
|
28
|
+
import { MultiSelectModule } from 'primeng/multiselect';
|
|
29
|
+
import { PaginatorModule } from 'primeng/paginator';
|
|
30
|
+
import { PanelModule } from 'primeng/panel';
|
|
31
|
+
import { PasswordModule } from 'primeng/password';
|
|
32
|
+
import { PopoverModule } from 'primeng/popover';
|
|
33
|
+
import { ProgressBarModule } from 'primeng/progressbar';
|
|
34
|
+
import { RadioButtonModule } from 'primeng/radiobutton';
|
|
35
|
+
import { RippleModule } from 'primeng/ripple';
|
|
36
|
+
import * as i3 from 'primeng/select';
|
|
37
|
+
import { SelectModule } from 'primeng/select';
|
|
38
|
+
import { SelectButtonModule } from 'primeng/selectbutton';
|
|
39
|
+
import { SplitButtonModule } from 'primeng/splitbutton';
|
|
40
|
+
import { StepsModule } from 'primeng/steps';
|
|
41
|
+
import { TableModule } from 'primeng/table';
|
|
42
|
+
import { TabsModule } from 'primeng/tabs';
|
|
43
|
+
import { TagModule } from 'primeng/tag';
|
|
44
|
+
import { TextareaModule } from 'primeng/textarea';
|
|
45
|
+
import { ToggleSwitchModule } from 'primeng/toggleswitch';
|
|
46
|
+
import { TooltipModule } from 'primeng/tooltip';
|
|
47
|
+
import { TreeTableModule } from 'primeng/treetable';
|
|
48
|
+
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
|
|
49
|
+
|
|
50
|
+
;
|
|
51
|
+
;
|
|
52
|
+
|
|
53
|
+
var ContactTypeEnum;
|
|
54
|
+
(function (ContactTypeEnum) {
|
|
55
|
+
ContactTypeEnum[ContactTypeEnum["PHONE"] = 1] = "PHONE";
|
|
56
|
+
ContactTypeEnum[ContactTypeEnum["EMAIL"] = 2] = "EMAIL";
|
|
57
|
+
})(ContactTypeEnum || (ContactTypeEnum = {}));
|
|
58
|
+
|
|
59
|
+
var IconTypeEnum;
|
|
60
|
+
(function (IconTypeEnum) {
|
|
61
|
+
IconTypeEnum[IconTypeEnum["PRIMENG_ICON"] = 1] = "PRIMENG_ICON";
|
|
62
|
+
IconTypeEnum[IconTypeEnum["IMAGE_FILE_LINK"] = 2] = "IMAGE_FILE_LINK";
|
|
63
|
+
IconTypeEnum[IconTypeEnum["DIRECT_TAG_SVG"] = 3] = "DIRECT_TAG_SVG";
|
|
64
|
+
})(IconTypeEnum || (IconTypeEnum = {}));
|
|
65
|
+
|
|
66
|
+
class PlatformService {
|
|
67
|
+
platformId = inject(PLATFORM_ID);
|
|
68
|
+
get isServer() {
|
|
69
|
+
return isPlatformServer(this.platformId);
|
|
70
|
+
}
|
|
71
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PlatformService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
72
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PlatformService, providedIn: 'root' });
|
|
73
|
+
}
|
|
74
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PlatformService, decorators: [{
|
|
75
|
+
type: Injectable,
|
|
76
|
+
args: [{
|
|
77
|
+
providedIn: 'root'
|
|
78
|
+
}]
|
|
79
|
+
}] });
|
|
80
|
+
|
|
81
|
+
class CookieService {
|
|
82
|
+
doc = inject(DOCUMENT, { optional: true });
|
|
83
|
+
platformService = inject(PlatformService);
|
|
84
|
+
request = inject(REQUEST, { optional: true });
|
|
85
|
+
get() {
|
|
86
|
+
return !this.platformService.isServer ? this.doc.cookie : this.request?.headers.get('cookie') ?? "";
|
|
87
|
+
}
|
|
88
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CookieService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
89
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CookieService, providedIn: 'root' });
|
|
90
|
+
}
|
|
91
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CookieService, decorators: [{
|
|
92
|
+
type: Injectable,
|
|
93
|
+
args: [{
|
|
94
|
+
providedIn: 'root',
|
|
95
|
+
}]
|
|
96
|
+
}] });
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Service to fetch file URLs from the backend.
|
|
100
|
+
* Uses POST /file-manager/get-files endpoint which:
|
|
101
|
+
* - Handles presigned URLs for cloud storage (AWS S3, Azure)
|
|
102
|
+
* - Auto-refreshes expired URLs
|
|
103
|
+
* - Validates file access permissions
|
|
104
|
+
* - Works with all storage providers (Local, S3, Azure, SFTP)
|
|
105
|
+
*/
|
|
106
|
+
class FileUrlService {
|
|
107
|
+
http = inject(HttpClient);
|
|
108
|
+
appConfig = inject(APP_CONFIG);
|
|
109
|
+
/** Cache of file URLs by file ID */
|
|
110
|
+
urlCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "urlCache" }] : []));
|
|
111
|
+
/**
|
|
112
|
+
* Get file URL by ID from cache (reactive signal)
|
|
113
|
+
*/
|
|
114
|
+
getFileUrl(fileId) {
|
|
115
|
+
if (!fileId)
|
|
116
|
+
return null;
|
|
117
|
+
return this.urlCache().get(fileId)?.url ?? null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get file URL signal (computed from cache)
|
|
121
|
+
*/
|
|
122
|
+
fileUrlSignal(fileId) {
|
|
123
|
+
return computed(() => this.getFileUrl(fileId));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetch file URLs from backend and update cache.
|
|
127
|
+
* Returns Observable of fetched files.
|
|
128
|
+
*/
|
|
129
|
+
fetchFileUrls(fileIds) {
|
|
130
|
+
if (!fileIds.length)
|
|
131
|
+
return of([]);
|
|
132
|
+
const requestDto = fileIds.map((id) => ({ id }));
|
|
133
|
+
return this.http.post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto).pipe(tap((files) => {
|
|
134
|
+
// Update cache
|
|
135
|
+
const cache = new Map(this.urlCache());
|
|
136
|
+
files.forEach((file) => cache.set(file.id, file));
|
|
137
|
+
this.urlCache.set(cache);
|
|
138
|
+
}), catchError((error) => {
|
|
139
|
+
console.error('Failed to fetch file URLs:', error);
|
|
140
|
+
return of([]);
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Fetch a single file URL.
|
|
145
|
+
* Returns Observable of file info or null if not found.
|
|
146
|
+
*/
|
|
147
|
+
fetchSingleFileUrl(fileId) {
|
|
148
|
+
return this.fetchFileUrls([fileId]).pipe(map((files) => files[0] ?? null));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Clear the URL cache.
|
|
152
|
+
* Useful on logout or when switching contexts.
|
|
153
|
+
*/
|
|
154
|
+
clearCache() {
|
|
155
|
+
this.urlCache.set(new Map());
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Remove specific file from cache.
|
|
159
|
+
*/
|
|
160
|
+
removeFromCache(fileId) {
|
|
161
|
+
const cache = new Map(this.urlCache());
|
|
162
|
+
cache.delete(fileId);
|
|
163
|
+
this.urlCache.set(cache);
|
|
164
|
+
}
|
|
165
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
166
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FileUrlService, providedIn: 'root' });
|
|
167
|
+
}
|
|
168
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: FileUrlService, decorators: [{
|
|
169
|
+
type: Injectable,
|
|
170
|
+
args: [{ providedIn: 'root' }]
|
|
171
|
+
}] });
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Permission Validator Service
|
|
175
|
+
*
|
|
176
|
+
* Centralized service for permission validation across all packages.
|
|
177
|
+
* Provides signal-based state management and reactive permission checking.
|
|
178
|
+
*
|
|
179
|
+
* Features:
|
|
180
|
+
* - Signal-based permission storage
|
|
181
|
+
* - Individual permission checks
|
|
182
|
+
* - Permission change detection
|
|
183
|
+
* - Reactive permission state updates
|
|
184
|
+
*
|
|
185
|
+
* Usage:
|
|
186
|
+
* ```typescript
|
|
187
|
+
* // In component
|
|
188
|
+
* readonly permissionValidator = inject(PermissionValidatorService);
|
|
189
|
+
*
|
|
190
|
+
* // Set permissions (typically from auth/IAM)
|
|
191
|
+
* this.permissionValidator.setPermissions(['user.view', 'user.create']);
|
|
192
|
+
*
|
|
193
|
+
* // Check individual permission
|
|
194
|
+
* if (this.permissionValidator.hasPermission('user.view')) {
|
|
195
|
+
* // Show UI element
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* // Access current permissions reactively
|
|
199
|
+
* const permissions = this.permissionValidator.permissions();
|
|
200
|
+
* ```
|
|
201
|
+
*
|
|
202
|
+
* @packageDocumentation
|
|
203
|
+
*/
|
|
204
|
+
class PermissionValidatorService {
|
|
205
|
+
// ==================== SIGNALS ====================
|
|
206
|
+
/**
|
|
207
|
+
* Private writable signal for permissions
|
|
208
|
+
*/
|
|
209
|
+
_permissions = signal([], ...(ngDevMode ? [{ debugName: "_permissions" }] : []));
|
|
210
|
+
/**
|
|
211
|
+
* Readonly permission signal for external consumers
|
|
212
|
+
*/
|
|
213
|
+
permissions = this._permissions.asReadonly();
|
|
214
|
+
/**
|
|
215
|
+
* Private writable signal for loaded state
|
|
216
|
+
*/
|
|
217
|
+
_isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "_isLoaded" }] : []));
|
|
218
|
+
/**
|
|
219
|
+
* Set permissions (replaces existing permissions)
|
|
220
|
+
* @param permissions - Array of permission codes
|
|
221
|
+
*/
|
|
222
|
+
setPermissions(permissions) {
|
|
223
|
+
this._permissions.set(permissions);
|
|
224
|
+
this._isLoaded.set(true);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Clear all permissions
|
|
228
|
+
*/
|
|
229
|
+
clearPermissions() {
|
|
230
|
+
this._permissions.set([]);
|
|
231
|
+
this._isLoaded.set(false);
|
|
232
|
+
}
|
|
233
|
+
// ==================== PERMISSION CHECKING ====================
|
|
234
|
+
/**
|
|
235
|
+
* Check if user has a specific permission
|
|
236
|
+
* @param permissionCode - Required permission code
|
|
237
|
+
* @returns True if user has permission
|
|
238
|
+
*/
|
|
239
|
+
hasPermission(permissionCode) {
|
|
240
|
+
return this.permissions().includes(permissionCode);
|
|
241
|
+
}
|
|
242
|
+
// ==================== UTILITY METHODS ====================
|
|
243
|
+
/**
|
|
244
|
+
* Check if permissions are loaded
|
|
245
|
+
* @returns True if permissions have been loaded
|
|
246
|
+
*/
|
|
247
|
+
isPermissionsLoaded() {
|
|
248
|
+
return this._isLoaded();
|
|
249
|
+
}
|
|
250
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionValidatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
251
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionValidatorService, providedIn: 'root' });
|
|
252
|
+
}
|
|
253
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PermissionValidatorService, decorators: [{
|
|
254
|
+
type: Injectable,
|
|
255
|
+
args: [{
|
|
256
|
+
providedIn: 'root',
|
|
257
|
+
}]
|
|
258
|
+
}] });
|
|
259
|
+
|
|
260
|
+
class EditModeElementChangerDirective {
|
|
261
|
+
el = inject(ElementRef);
|
|
262
|
+
ngControl = inject(NgControl, { optional: true });
|
|
263
|
+
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
264
|
+
constructor() {
|
|
265
|
+
effect(() => {
|
|
266
|
+
const editMode = this.isEditMode();
|
|
267
|
+
this.updateElement(editMode);
|
|
268
|
+
this.updateControl(editMode);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
ngAfterViewInit() {
|
|
272
|
+
const editMode = this.isEditMode();
|
|
273
|
+
this.updateElement(editMode);
|
|
274
|
+
this.updateControl(editMode);
|
|
275
|
+
}
|
|
276
|
+
updateControl(editMode) {
|
|
277
|
+
if (!editMode)
|
|
278
|
+
this.ngControl?.control?.disable();
|
|
279
|
+
else
|
|
280
|
+
this.ngControl?.control?.enable();
|
|
281
|
+
}
|
|
282
|
+
updateElement(editMode) {
|
|
283
|
+
const inputElement = this.el.nativeElement;
|
|
284
|
+
if (inputElement instanceof HTMLInputElement) {
|
|
285
|
+
if (!editMode) {
|
|
286
|
+
inputElement.setAttribute('readonly', 'true');
|
|
287
|
+
inputElement?.classList.add('edit-mode-element-css');
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
inputElement.removeAttribute('readonly');
|
|
291
|
+
inputElement?.classList.remove('edit-mode-element-css');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (inputElement.tagName == 'P-SELECT') {
|
|
295
|
+
if (!editMode) {
|
|
296
|
+
inputElement.classList.add('edit-mode-element-css');
|
|
297
|
+
inputElement.classList.add('overflow-hidden');
|
|
298
|
+
inputElement.querySelector('.p-select-dropdown').style.display = 'none';
|
|
299
|
+
inputElement.querySelector('.p-select-label').classList.add('edit-mode-element-css');
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
inputElement.classList.remove("edit-mode-element-css");
|
|
303
|
+
inputElement.querySelector('.p-select-dropdown').style.display = 'flex';
|
|
304
|
+
inputElement.querySelector('.p-select-label').classList.remove("edit-mode-element-css");
|
|
305
|
+
inputElement.classList.remove('overflow-hidden');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (inputElement.tagName == 'P-CALENDAR') {
|
|
309
|
+
if (!editMode) {
|
|
310
|
+
inputElement.firstElementChild.children[0]?.classList.add('edit-mode-element-css');
|
|
311
|
+
inputElement.firstElementChild.children[0]?.classList.add('cursor-auto');
|
|
312
|
+
inputElement.firstElementChild.children[1]?.classList.add('hidden');
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
inputElement.firstElementChild.children[0]?.classList.remove('edit-mode-element-css');
|
|
316
|
+
inputElement.firstElementChild.children[0]?.classList.remove('cursor-auto');
|
|
317
|
+
inputElement.firstElementChild.children[1]?.classList.remove('hidden');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EditModeElementChangerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
322
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: EditModeElementChangerDirective, isStandalone: true, selector: "[appEditModeElementChanger]", inputs: { isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
323
|
+
}
|
|
324
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EditModeElementChangerDirective, decorators: [{
|
|
325
|
+
type: Directive,
|
|
326
|
+
args: [{
|
|
327
|
+
selector: '[appEditModeElementChanger]',
|
|
328
|
+
standalone: true,
|
|
329
|
+
}]
|
|
330
|
+
}], ctorParameters: () => [], propDecorators: { isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }] } });
|
|
331
|
+
|
|
332
|
+
/** Evaluate permission logic (string or ILogicNode) against user permissions */
|
|
333
|
+
function evaluatePermission(logic, permissions) {
|
|
334
|
+
if (!logic)
|
|
335
|
+
return false;
|
|
336
|
+
if (typeof logic === 'string')
|
|
337
|
+
return permissions.includes(logic);
|
|
338
|
+
return evaluateLogicNode(logic, permissions);
|
|
339
|
+
}
|
|
340
|
+
/** Recursively evaluate an ILogicNode tree */
|
|
341
|
+
function evaluateLogicNode(node, permissions) {
|
|
342
|
+
switch (node.type) {
|
|
343
|
+
case 'action':
|
|
344
|
+
return node.actionId ? permissions.includes(node.actionId) : false;
|
|
345
|
+
case 'group':
|
|
346
|
+
if (!node.children || node.children.length === 0)
|
|
347
|
+
return false;
|
|
348
|
+
return node.operator === 'AND'
|
|
349
|
+
? node.children.every((child) => evaluateLogicNode(child, permissions))
|
|
350
|
+
: node.children.some((child) => evaluateLogicNode(child, permissions));
|
|
351
|
+
default:
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** Check if user has ANY of the specified permissions (OR logic) */
|
|
356
|
+
function hasAnyPermission(permissionCodes, permissions) {
|
|
357
|
+
if (!permissionCodes?.length)
|
|
358
|
+
return false;
|
|
359
|
+
return permissionCodes.some((code) => permissions.includes(code));
|
|
360
|
+
}
|
|
361
|
+
/** Check if user has ALL of the specified permissions (AND logic) */
|
|
362
|
+
function hasAllPermissions(permissionCodes, permissions) {
|
|
363
|
+
if (!permissionCodes?.length)
|
|
364
|
+
return false;
|
|
365
|
+
return permissionCodes.every((code) => permissions.includes(code));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* HasPermission Directive
|
|
370
|
+
*
|
|
371
|
+
* Structural directive for permission-based rendering.
|
|
372
|
+
* Shows/hides elements based on permission logic evaluation.
|
|
373
|
+
*
|
|
374
|
+
* Supports:
|
|
375
|
+
* - Simple permission check (string)
|
|
376
|
+
* - Complex permission logic (ILogicNode with AND/OR operators)
|
|
377
|
+
*
|
|
378
|
+
* Usage:
|
|
379
|
+
* ```html
|
|
380
|
+
* <!-- Simple permission check -->
|
|
381
|
+
* <button *hasPermission="'user.create'">Create User</button>
|
|
382
|
+
*
|
|
383
|
+
* <!-- Complex permission logic -->
|
|
384
|
+
* <button *hasPermission="userManageLogic">Manage Users</button>
|
|
385
|
+
*
|
|
386
|
+
* <!-- Where userManageLogic is: -->
|
|
387
|
+
* <!-- (user.view OR user.create) AND department.manage -->
|
|
388
|
+
* ```
|
|
389
|
+
*
|
|
390
|
+
* The directive automatically reacts to permission changes through signals.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* // In component
|
|
394
|
+
* readonly userManageLogic: ILogicNode = {
|
|
395
|
+
* id: 'root',
|
|
396
|
+
* type: 'group',
|
|
397
|
+
* operator: 'AND',
|
|
398
|
+
* children: [
|
|
399
|
+
* {
|
|
400
|
+
* id: '1',
|
|
401
|
+
* type: 'group',
|
|
402
|
+
* operator: 'OR',
|
|
403
|
+
* children: [
|
|
404
|
+
* { id: '2', type: 'action', actionId: 'user.view' },
|
|
405
|
+
* { id: '3', type: 'action', actionId: 'user.create' }
|
|
406
|
+
* ]
|
|
407
|
+
* },
|
|
408
|
+
* { id: '4', type: 'action', actionId: 'department.manage' }
|
|
409
|
+
* ]
|
|
410
|
+
* };
|
|
411
|
+
*
|
|
412
|
+
* // In template
|
|
413
|
+
* <div *hasPermission="userManageLogic">
|
|
414
|
+
* User management content
|
|
415
|
+
* </div>
|
|
416
|
+
*/
|
|
417
|
+
class HasPermissionDirective {
|
|
418
|
+
// ==================== DEPENDENCIES ====================
|
|
419
|
+
templateRef = inject((TemplateRef));
|
|
420
|
+
viewContainer = inject(ViewContainerRef);
|
|
421
|
+
permissionValidator = inject(PermissionValidatorService);
|
|
422
|
+
// ==================== SIGNALS ====================
|
|
423
|
+
/**
|
|
424
|
+
* Permission logic input signal
|
|
425
|
+
* Accepts either:
|
|
426
|
+
* - string: Single permission code to check
|
|
427
|
+
* - ILogicNode: Complex permission logic tree
|
|
428
|
+
*/
|
|
429
|
+
hasPermission = input(null, ...(ngDevMode ? [{ debugName: "hasPermission" }] : []));
|
|
430
|
+
/**
|
|
431
|
+
* View created state
|
|
432
|
+
*/
|
|
433
|
+
viewCreated = false;
|
|
434
|
+
// ==================== CONSTRUCTOR ====================
|
|
435
|
+
constructor() {
|
|
436
|
+
// Effect to reactively update view when permissions or logic changes
|
|
437
|
+
effect(() => {
|
|
438
|
+
// Track permission changes
|
|
439
|
+
this.permissionValidator.permissions();
|
|
440
|
+
// Track logic changes
|
|
441
|
+
const logic = this.hasPermission();
|
|
442
|
+
// Evaluate and update view
|
|
443
|
+
this.updateView(logic);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// ==================== VIEW MANAGEMENT ====================
|
|
447
|
+
/**
|
|
448
|
+
* Update view based on permission evaluation
|
|
449
|
+
*/
|
|
450
|
+
updateView(logic) {
|
|
451
|
+
// Fail closed: No logic = no access
|
|
452
|
+
if (!logic) {
|
|
453
|
+
this.clearView();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
// Fail closed: No permissions loaded = no access (unauthenticated state)
|
|
457
|
+
if (!this.permissionValidator.isPermissionsLoaded()) {
|
|
458
|
+
this.clearView();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
// Use shared evaluatePermission utility
|
|
462
|
+
const hasPermission = evaluatePermission(logic, this.permissionValidator.permissions());
|
|
463
|
+
if (hasPermission && !this.viewCreated) {
|
|
464
|
+
// Create view if not exists
|
|
465
|
+
this.viewContainer.createEmbeddedView(this.templateRef);
|
|
466
|
+
this.viewCreated = true;
|
|
467
|
+
}
|
|
468
|
+
else if (!hasPermission && this.viewCreated) {
|
|
469
|
+
this.clearView();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Clear the view container
|
|
474
|
+
*/
|
|
475
|
+
clearView() {
|
|
476
|
+
if (this.viewCreated) {
|
|
477
|
+
this.viewContainer.clear();
|
|
478
|
+
this.viewCreated = false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HasPermissionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
482
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: HasPermissionDirective, isStandalone: true, selector: "[hasPermission]", inputs: { hasPermission: { classPropertyName: "hasPermission", publicName: "hasPermission", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
483
|
+
}
|
|
484
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HasPermissionDirective, decorators: [{
|
|
485
|
+
type: Directive,
|
|
486
|
+
args: [{
|
|
487
|
+
selector: '[hasPermission]',
|
|
488
|
+
standalone: true,
|
|
489
|
+
}]
|
|
490
|
+
}], ctorParameters: () => [], propDecorators: { hasPermission: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPermission", required: false }] }] } });
|
|
491
|
+
|
|
492
|
+
class IsEmptyImageDirective {
|
|
493
|
+
src = input('', ...(ngDevMode ? [{ debugName: "src" }] : []));
|
|
494
|
+
hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
495
|
+
defaultImg = 'lib/assets/images/default/default-image.jpg';
|
|
496
|
+
imageSrc = computed(() => {
|
|
497
|
+
if (this.hasError()) {
|
|
498
|
+
return this.defaultImg;
|
|
499
|
+
}
|
|
500
|
+
const srcValue = this.src();
|
|
501
|
+
return srcValue ? srcValue : this.defaultImg;
|
|
502
|
+
}, ...(ngDevMode ? [{ debugName: "imageSrc" }] : []));
|
|
503
|
+
onError() {
|
|
504
|
+
this.hasError.set(true);
|
|
505
|
+
}
|
|
506
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IsEmptyImageDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
507
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: IsEmptyImageDirective, isStandalone: true, selector: "img", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "error": "onError()" }, properties: { "src": "imageSrc()" } }, ngImport: i0 });
|
|
508
|
+
}
|
|
509
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IsEmptyImageDirective, decorators: [{
|
|
510
|
+
type: Directive,
|
|
511
|
+
args: [{
|
|
512
|
+
selector: 'img',
|
|
513
|
+
host: {
|
|
514
|
+
'[src]': 'imageSrc()',
|
|
515
|
+
'(error)': 'onError()',
|
|
516
|
+
},
|
|
517
|
+
standalone: true,
|
|
518
|
+
}]
|
|
519
|
+
}], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }] } });
|
|
520
|
+
|
|
521
|
+
class PreventDefaultDirective {
|
|
522
|
+
eventType = input('click', ...(ngDevMode ? [{ debugName: "eventType" }] : []));
|
|
523
|
+
preventKey = input(undefined, ...(ngDevMode ? [{ debugName: "preventKey" }] : []));
|
|
524
|
+
action = output();
|
|
525
|
+
onClick(event) {
|
|
526
|
+
if (this.eventType() === 'click') {
|
|
527
|
+
this.processEvent(event);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
onKeydown(event) {
|
|
531
|
+
if (this.eventType() === 'keydown') {
|
|
532
|
+
if (!this.preventKey() || event.key === this.preventKey()) {
|
|
533
|
+
this.processEvent(event);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
onKeyup(event) {
|
|
538
|
+
if (this.eventType() === 'keyup') {
|
|
539
|
+
if (!this.preventKey() || event.key === this.preventKey()) {
|
|
540
|
+
this.processEvent(event);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
processEvent(event) {
|
|
545
|
+
event.preventDefault();
|
|
546
|
+
if (this.action)
|
|
547
|
+
this.action.emit(event);
|
|
548
|
+
}
|
|
549
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PreventDefaultDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
550
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: PreventDefaultDirective, isStandalone: true, selector: "[appPreventDefault]", inputs: { eventType: { classPropertyName: "eventType", publicName: "eventType", isSignal: true, isRequired: false, transformFunction: null }, preventKey: { classPropertyName: "preventKey", publicName: "preventKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "keyup": "onKeyup($event)" } }, ngImport: i0 });
|
|
551
|
+
}
|
|
552
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PreventDefaultDirective, decorators: [{
|
|
553
|
+
type: Directive,
|
|
554
|
+
args: [{
|
|
555
|
+
selector: '[appPreventDefault]',
|
|
556
|
+
standalone: true,
|
|
557
|
+
}]
|
|
558
|
+
}], propDecorators: { eventType: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventType", required: false }] }], preventKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "preventKey", required: false }] }], action: [{ type: i0.Output, args: ["action"] }], onClick: [{
|
|
559
|
+
type: HostListener,
|
|
560
|
+
args: ['click', ['$event']]
|
|
561
|
+
}], onKeydown: [{
|
|
562
|
+
type: HostListener,
|
|
563
|
+
args: ['keydown', ['$event']]
|
|
564
|
+
}], onKeyup: [{
|
|
565
|
+
type: HostListener,
|
|
566
|
+
args: ['keyup', ['$event']]
|
|
567
|
+
}] } });
|
|
568
|
+
|
|
569
|
+
class AngularModule {
|
|
570
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
571
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: AngularModule, imports: [CommonModule,
|
|
572
|
+
RouterOutlet,
|
|
573
|
+
RouterLink,
|
|
574
|
+
IsEmptyImageDirective,
|
|
575
|
+
NgOptimizedImage,
|
|
576
|
+
NgComponentOutlet,
|
|
577
|
+
PreventDefaultDirective], exports: [CommonModule,
|
|
578
|
+
ReactiveFormsModule,
|
|
579
|
+
FormsModule,
|
|
580
|
+
RouterOutlet,
|
|
581
|
+
RouterLink,
|
|
582
|
+
IsEmptyImageDirective,
|
|
583
|
+
NgOptimizedImage,
|
|
584
|
+
NgComponentOutlet,
|
|
585
|
+
PreventDefaultDirective] });
|
|
586
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AngularModule, providers: [
|
|
587
|
+
DatePipe,
|
|
588
|
+
], imports: [CommonModule, CommonModule,
|
|
589
|
+
ReactiveFormsModule,
|
|
590
|
+
FormsModule] });
|
|
591
|
+
}
|
|
592
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AngularModule, decorators: [{
|
|
593
|
+
type: NgModule,
|
|
594
|
+
args: [{
|
|
595
|
+
imports: [
|
|
596
|
+
CommonModule,
|
|
597
|
+
RouterOutlet,
|
|
598
|
+
RouterLink,
|
|
599
|
+
IsEmptyImageDirective,
|
|
600
|
+
NgOptimizedImage,
|
|
601
|
+
NgComponentOutlet,
|
|
602
|
+
PreventDefaultDirective
|
|
603
|
+
],
|
|
604
|
+
providers: [
|
|
605
|
+
DatePipe,
|
|
606
|
+
],
|
|
607
|
+
exports: [
|
|
608
|
+
CommonModule,
|
|
609
|
+
ReactiveFormsModule,
|
|
610
|
+
FormsModule,
|
|
611
|
+
RouterOutlet,
|
|
612
|
+
RouterLink,
|
|
613
|
+
IsEmptyImageDirective,
|
|
614
|
+
NgOptimizedImage,
|
|
615
|
+
NgComponentOutlet,
|
|
616
|
+
PreventDefaultDirective
|
|
617
|
+
],
|
|
618
|
+
}]
|
|
619
|
+
}] });
|
|
620
|
+
|
|
621
|
+
class PrimeModule {
|
|
622
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PrimeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
623
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: PrimeModule, exports: [InputTextModule,
|
|
624
|
+
TagModule,
|
|
625
|
+
SelectButtonModule,
|
|
626
|
+
PasswordModule,
|
|
627
|
+
ButtonModule,
|
|
628
|
+
TooltipModule,
|
|
629
|
+
CheckboxModule,
|
|
630
|
+
StepsModule,
|
|
631
|
+
RippleModule,
|
|
632
|
+
PanelModule,
|
|
633
|
+
PaginatorModule,
|
|
634
|
+
TableModule,
|
|
635
|
+
InputNumberModule,
|
|
636
|
+
TextareaModule,
|
|
637
|
+
ProgressBarModule,
|
|
638
|
+
FileUploadModule,
|
|
639
|
+
CardModule,
|
|
640
|
+
SelectModule,
|
|
641
|
+
InputIconModule,
|
|
642
|
+
IconFieldModule,
|
|
643
|
+
PopoverModule,
|
|
644
|
+
ListboxModule,
|
|
645
|
+
RadioButtonModule,
|
|
646
|
+
ToggleSwitchModule,
|
|
647
|
+
ImageModule,
|
|
648
|
+
DatePickerModule,
|
|
649
|
+
SplitButtonModule,
|
|
650
|
+
DividerModule,
|
|
651
|
+
MultiSelectModule,
|
|
652
|
+
AutoCompleteModule,
|
|
653
|
+
TabsModule,
|
|
654
|
+
DialogModule,
|
|
655
|
+
TreeTableModule] });
|
|
656
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PrimeModule, imports: [InputTextModule,
|
|
657
|
+
TagModule,
|
|
658
|
+
SelectButtonModule,
|
|
659
|
+
PasswordModule,
|
|
660
|
+
ButtonModule,
|
|
661
|
+
TooltipModule,
|
|
662
|
+
CheckboxModule,
|
|
663
|
+
StepsModule,
|
|
664
|
+
RippleModule,
|
|
665
|
+
PanelModule,
|
|
666
|
+
PaginatorModule,
|
|
667
|
+
TableModule,
|
|
668
|
+
InputNumberModule,
|
|
669
|
+
TextareaModule,
|
|
670
|
+
ProgressBarModule,
|
|
671
|
+
FileUploadModule,
|
|
672
|
+
CardModule,
|
|
673
|
+
SelectModule,
|
|
674
|
+
InputIconModule,
|
|
675
|
+
IconFieldModule,
|
|
676
|
+
PopoverModule,
|
|
677
|
+
ListboxModule,
|
|
678
|
+
RadioButtonModule,
|
|
679
|
+
ToggleSwitchModule,
|
|
680
|
+
ImageModule,
|
|
681
|
+
DatePickerModule,
|
|
682
|
+
SplitButtonModule,
|
|
683
|
+
DividerModule,
|
|
684
|
+
MultiSelectModule,
|
|
685
|
+
AutoCompleteModule,
|
|
686
|
+
TabsModule,
|
|
687
|
+
DialogModule,
|
|
688
|
+
TreeTableModule] });
|
|
689
|
+
}
|
|
690
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PrimeModule, decorators: [{
|
|
691
|
+
type: NgModule,
|
|
692
|
+
args: [{
|
|
693
|
+
exports: [
|
|
694
|
+
InputTextModule,
|
|
695
|
+
TagModule,
|
|
696
|
+
SelectButtonModule,
|
|
697
|
+
PasswordModule,
|
|
698
|
+
ButtonModule,
|
|
699
|
+
TooltipModule,
|
|
700
|
+
CheckboxModule,
|
|
701
|
+
StepsModule,
|
|
702
|
+
RippleModule,
|
|
703
|
+
PanelModule,
|
|
704
|
+
PaginatorModule,
|
|
705
|
+
TableModule,
|
|
706
|
+
InputNumberModule,
|
|
707
|
+
TextareaModule,
|
|
708
|
+
ProgressBarModule,
|
|
709
|
+
FileUploadModule,
|
|
710
|
+
CardModule,
|
|
711
|
+
SelectModule,
|
|
712
|
+
InputIconModule,
|
|
713
|
+
IconFieldModule,
|
|
714
|
+
PopoverModule,
|
|
715
|
+
ListboxModule,
|
|
716
|
+
RadioButtonModule,
|
|
717
|
+
ToggleSwitchModule,
|
|
718
|
+
ImageModule,
|
|
719
|
+
DatePickerModule,
|
|
720
|
+
SplitButtonModule,
|
|
721
|
+
DividerModule,
|
|
722
|
+
MultiSelectModule,
|
|
723
|
+
AutoCompleteModule,
|
|
724
|
+
TabsModule,
|
|
725
|
+
DialogModule,
|
|
726
|
+
TreeTableModule,
|
|
727
|
+
],
|
|
728
|
+
}]
|
|
729
|
+
}] });
|
|
730
|
+
|
|
731
|
+
// =============================================================================
|
|
732
|
+
// API Resource Service - Signal-based CRUD with Angular Resource API
|
|
733
|
+
// =============================================================================
|
|
734
|
+
/**
|
|
735
|
+
* Abstract base class for API services using Angular 21 resource() API.
|
|
736
|
+
* Provides signal-based reactive data fetching with automatic loading states.
|
|
737
|
+
* Response types match FLUSYS_NEST backend DTOs.
|
|
738
|
+
*
|
|
739
|
+
* ## Endpoint Mapping
|
|
740
|
+
*
|
|
741
|
+
* All endpoints use POST method (RPC-style API):
|
|
742
|
+
* - `POST /{resource}/insert` - Create single item
|
|
743
|
+
* - `POST /{resource}/insert-many` - Create multiple items
|
|
744
|
+
* - `POST /{resource}/get/:id` - Get single item by ID
|
|
745
|
+
* - `POST /{resource}/get-all?q=` - List with pagination/filter
|
|
746
|
+
* - `POST /{resource}/update` - Update single item
|
|
747
|
+
* - `POST /{resource}/update-many` - Update multiple items
|
|
748
|
+
* - `POST /{resource}/delete` - Delete/restore/permanent delete
|
|
749
|
+
*
|
|
750
|
+
* @example
|
|
751
|
+
* ```typescript
|
|
752
|
+
* // Define service
|
|
753
|
+
* @Injectable({ providedIn: 'root' })
|
|
754
|
+
* export class UserService extends ApiResourceService<UserDto, User> {
|
|
755
|
+
* constructor(http: HttpClient) {
|
|
756
|
+
* super('users', http);
|
|
757
|
+
* }
|
|
758
|
+
* }
|
|
759
|
+
*
|
|
760
|
+
* // In component - use signals
|
|
761
|
+
* userService = inject(UserService);
|
|
762
|
+
* users = this.userService.data; // Signal<User[]>
|
|
763
|
+
* isLoading = this.userService.isLoading; // Signal<boolean>
|
|
764
|
+
* total = this.userService.total; // Signal<number>
|
|
765
|
+
*
|
|
766
|
+
* // Trigger fetch
|
|
767
|
+
* this.userService.fetchList('search', { pagination: { currentPage: 0, pageSize: 10 } });
|
|
768
|
+
*
|
|
769
|
+
* // CRUD operations
|
|
770
|
+
* await this.userService.insertAsync({ name: 'John', email: 'john@example.com' });
|
|
771
|
+
* await this.userService.updateAsync({ id: '123', name: 'John Updated' });
|
|
772
|
+
* await this.userService.deleteAsync({ id: '123', type: 'delete' });
|
|
773
|
+
* ```
|
|
774
|
+
*/
|
|
775
|
+
class ApiResourceService {
|
|
776
|
+
baseUrl;
|
|
777
|
+
loaderService = inject(ApiLoaderService);
|
|
778
|
+
http;
|
|
779
|
+
moduleApiName;
|
|
780
|
+
// ==========================================================================
|
|
781
|
+
// State Signals for List Queries
|
|
782
|
+
// ==========================================================================
|
|
783
|
+
/** Current search term */
|
|
784
|
+
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
785
|
+
/** Current filter and pagination state */
|
|
786
|
+
filterData = signal({
|
|
787
|
+
pagination: { currentPage: 0, pageSize: 10 },
|
|
788
|
+
filter: {},
|
|
789
|
+
select: [],
|
|
790
|
+
sort: {},
|
|
791
|
+
}, ...(ngDevMode ? [{ debugName: "filterData" }] : []));
|
|
792
|
+
/** Resource for list data - uses IListResponse matching backend */
|
|
793
|
+
listResource;
|
|
794
|
+
// ==========================================================================
|
|
795
|
+
// Computed State Accessors
|
|
796
|
+
// ==========================================================================
|
|
797
|
+
/** Whether data is currently loading */
|
|
798
|
+
isLoading = computed(() => this.listResource.isLoading(), ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
799
|
+
/** List data array */
|
|
800
|
+
data = computed(() => this.listResource.value()?.data ?? [], ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
801
|
+
/** Total count of items */
|
|
802
|
+
total = computed(() => this.listResource.value()?.meta?.total ?? 0, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
803
|
+
/** Pagination metadata */
|
|
804
|
+
pageInfo = computed(() => this.listResource.value()?.meta, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
|
|
805
|
+
/** Whether there are more pages */
|
|
806
|
+
hasMore = computed(() => {
|
|
807
|
+
const meta = this.listResource.value()?.meta;
|
|
808
|
+
if (!meta)
|
|
809
|
+
return false;
|
|
810
|
+
return meta.hasMore ?? (meta.page + 1) * meta.pageSize < meta.total;
|
|
811
|
+
}, ...(ngDevMode ? [{ debugName: "hasMore" }] : []));
|
|
812
|
+
constructor(moduleApiName, http) {
|
|
813
|
+
this.moduleApiName = moduleApiName;
|
|
814
|
+
this.baseUrl = inject(APP_CONFIG).apiBaseUrl + '/' + moduleApiName;
|
|
815
|
+
this.http = http;
|
|
816
|
+
// Create resource that reacts to search and filter changes
|
|
817
|
+
this.listResource = resource({ ...(ngDevMode ? { debugName: "listResource" } : {}), params: () => ({
|
|
818
|
+
search: this.searchTerm(),
|
|
819
|
+
filter: this.filterData(),
|
|
820
|
+
}),
|
|
821
|
+
loader: async ({ params }) => {
|
|
822
|
+
const { search, filter } = params;
|
|
823
|
+
return this.fetchAllAsync(search, filter);
|
|
824
|
+
} });
|
|
825
|
+
}
|
|
826
|
+
getHttpOptions(endpoint, params) {
|
|
827
|
+
return {
|
|
828
|
+
headers: new HttpHeaders({
|
|
829
|
+
'x-loader-tag': `${this.moduleApiName}/${endpoint}`,
|
|
830
|
+
}),
|
|
831
|
+
...(params ? { params } : {}),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
// ==========================================================================
|
|
835
|
+
// List Management Methods
|
|
836
|
+
// ==========================================================================
|
|
837
|
+
/**
|
|
838
|
+
* Fetch list data (triggers resource reload)
|
|
839
|
+
*/
|
|
840
|
+
fetchList(search = '', filter) {
|
|
841
|
+
this.searchTerm.set(search);
|
|
842
|
+
if (filter) {
|
|
843
|
+
this.filterData.update((prev) => ({ ...prev, ...filter }));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Update pagination
|
|
848
|
+
*/
|
|
849
|
+
setPagination(pagination) {
|
|
850
|
+
this.filterData.update((prev) => ({ ...prev, pagination }));
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Go to next page
|
|
854
|
+
*/
|
|
855
|
+
nextPage() {
|
|
856
|
+
this.filterData.update((prev) => ({
|
|
857
|
+
...prev,
|
|
858
|
+
pagination: {
|
|
859
|
+
...prev.pagination,
|
|
860
|
+
currentPage: (prev.pagination?.currentPage ?? 0) + 1,
|
|
861
|
+
},
|
|
862
|
+
}));
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Reset to first page
|
|
866
|
+
*/
|
|
867
|
+
resetPagination() {
|
|
868
|
+
this.filterData.update((prev) => ({
|
|
869
|
+
...prev,
|
|
870
|
+
pagination: { currentPage: 0, pageSize: prev.pagination?.pageSize ?? 10 },
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Reload current data
|
|
875
|
+
*/
|
|
876
|
+
reload() {
|
|
877
|
+
this.listResource.reload();
|
|
878
|
+
}
|
|
879
|
+
// ==========================================================================
|
|
880
|
+
// Observable-based API Methods (IApiService interface)
|
|
881
|
+
// ==========================================================================
|
|
882
|
+
/**
|
|
883
|
+
* Insert single item (Observable)
|
|
884
|
+
* POST /{resource}/insert
|
|
885
|
+
*/
|
|
886
|
+
insert(dto) {
|
|
887
|
+
return this.http.post(`${this.baseUrl}/insert`, dto, this.getHttpOptions('insert'));
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Insert multiple items (Observable)
|
|
891
|
+
* POST /{resource}/insert-many
|
|
892
|
+
*/
|
|
893
|
+
insertMany(dtos) {
|
|
894
|
+
return this.http.post(`${this.baseUrl}/insert-many`, dtos, this.getHttpOptions('insert-many'));
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Find single item by ID (Observable)
|
|
898
|
+
* POST /{resource}/get/:id
|
|
899
|
+
*/
|
|
900
|
+
findById(id, select) {
|
|
901
|
+
return this.http.post(`${this.baseUrl}/get/${id}`, { select }, this.getHttpOptions(`get/${id}`));
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Get all items with pagination (Observable)
|
|
905
|
+
* POST /{resource}/get-all?q=search
|
|
906
|
+
*/
|
|
907
|
+
getAll(search, filter) {
|
|
908
|
+
let params = new HttpParams();
|
|
909
|
+
if (search) {
|
|
910
|
+
params = params.append('q', search);
|
|
911
|
+
}
|
|
912
|
+
return this.http.post(`${this.baseUrl}/get-all`, filter, this.getHttpOptions('get-all', params));
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Update single item (Observable)
|
|
916
|
+
* POST /{resource}/update
|
|
917
|
+
*/
|
|
918
|
+
update(dto) {
|
|
919
|
+
return this.http.post(`${this.baseUrl}/update`, dto, this.getHttpOptions('update'));
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Update multiple items (Observable)
|
|
923
|
+
* POST /{resource}/update-many
|
|
924
|
+
*/
|
|
925
|
+
updateMany(dtos) {
|
|
926
|
+
return this.http.post(`${this.baseUrl}/update-many`, dtos, this.getHttpOptions('update-many'));
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Delete items (Observable)
|
|
930
|
+
* POST /{resource}/delete
|
|
931
|
+
* @param deleteDto - { id: string | string[], type: 'delete' | 'restore' | 'permanent' }
|
|
932
|
+
*/
|
|
933
|
+
delete(deleteDto) {
|
|
934
|
+
return this.http.post(`${this.baseUrl}/delete`, deleteDto, this.getHttpOptions('delete'));
|
|
935
|
+
}
|
|
936
|
+
// ==========================================================================
|
|
937
|
+
// Promise-based API Methods (Async)
|
|
938
|
+
// ==========================================================================
|
|
939
|
+
/**
|
|
940
|
+
* Fetch paginated list (async)
|
|
941
|
+
*/
|
|
942
|
+
async fetchAllAsync(search, filter) {
|
|
943
|
+
return firstValueFrom(this.getAll(search, filter));
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Find single item by ID (async)
|
|
947
|
+
*/
|
|
948
|
+
async findByIdAsync(id, select) {
|
|
949
|
+
return firstValueFrom(this.findById(id, select));
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Insert single item (async)
|
|
953
|
+
*/
|
|
954
|
+
async insertAsync(dto) {
|
|
955
|
+
return firstValueFrom(this.insert(dto));
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Insert multiple items (async)
|
|
959
|
+
*/
|
|
960
|
+
async insertManyAsync(dtos) {
|
|
961
|
+
return firstValueFrom(this.insertMany(dtos));
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Update single item (async)
|
|
965
|
+
*/
|
|
966
|
+
async updateAsync(dto) {
|
|
967
|
+
return firstValueFrom(this.update(dto));
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Update multiple items (async)
|
|
971
|
+
*/
|
|
972
|
+
async updateManyAsync(dtos) {
|
|
973
|
+
return firstValueFrom(this.updateMany(dtos));
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Delete items (async)
|
|
977
|
+
*/
|
|
978
|
+
async deleteAsync(deleteDto) {
|
|
979
|
+
return firstValueFrom(this.delete(deleteDto));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
class IconComponent {
|
|
984
|
+
icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
985
|
+
iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
|
|
986
|
+
IconTypeEnum = IconTypeEnum; // Needed for template reference
|
|
987
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
988
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: IconComponent, isStandalone: true, selector: "lib-icon", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, iconType: { classPropertyName: "iconType", publicName: "iconType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
989
|
+
@if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
|
|
990
|
+
<i [ngClass]="icon()"></i>
|
|
991
|
+
}@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
|
|
992
|
+
<img [alt]="icon()" [src]="icon()" />
|
|
993
|
+
}@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
|
|
994
|
+
{{ icon() }}
|
|
995
|
+
}@else{ I } }
|
|
996
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }] });
|
|
997
|
+
}
|
|
998
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: IconComponent, decorators: [{
|
|
999
|
+
type: Component,
|
|
1000
|
+
args: [{
|
|
1001
|
+
selector: 'lib-icon',
|
|
1002
|
+
imports: [AngularModule],
|
|
1003
|
+
template: `
|
|
1004
|
+
@if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
|
|
1005
|
+
<i [ngClass]="icon()"></i>
|
|
1006
|
+
}@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
|
|
1007
|
+
<img [alt]="icon()" [src]="icon()" />
|
|
1008
|
+
}@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
|
|
1009
|
+
{{ icon() }}
|
|
1010
|
+
}@else{ I } }
|
|
1011
|
+
`,
|
|
1012
|
+
}]
|
|
1013
|
+
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Base class for form controls that support ALL Angular form patterns:
|
|
1017
|
+
*
|
|
1018
|
+
* 1. **Template-driven forms:** `[(value)]="mySignal"` or `[(ngModel)]="myValue"`
|
|
1019
|
+
* 2. **Reactive forms:** `[formControl]="ctrl"` or `formControlName="field"`
|
|
1020
|
+
* 3. **Signal forms (Angular 21+):** `[formField]="formTree.field"`
|
|
1021
|
+
*
|
|
1022
|
+
* Implements both `ControlValueAccessor` (reactive forms) and `FormValueControl` (signal forms).
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* ```typescript
|
|
1026
|
+
* @Component({
|
|
1027
|
+
* providers: [provideValueAccessor(MySelectComponent)]
|
|
1028
|
+
* })
|
|
1029
|
+
* export class MySelectComponent extends BaseFormControl<string | null> {
|
|
1030
|
+
* override readonly value = model<string | null>(null);
|
|
1031
|
+
*
|
|
1032
|
+
* constructor() {
|
|
1033
|
+
* super();
|
|
1034
|
+
* this.initializeFormControl();
|
|
1035
|
+
* }
|
|
1036
|
+
* }
|
|
1037
|
+
*
|
|
1038
|
+
* // Usage:
|
|
1039
|
+
* // Template-driven: <my-select [(value)]="selectedId" />
|
|
1040
|
+
* // Reactive forms: <my-select [formControl]="myControl" />
|
|
1041
|
+
* // Signal forms: <my-select [formField]="formTree.myField" />
|
|
1042
|
+
* ```
|
|
1043
|
+
*/
|
|
1044
|
+
class BaseFormControl {
|
|
1045
|
+
injector = inject(Injector);
|
|
1046
|
+
/**
|
|
1047
|
+
* Disabled state model - bound to form control disabled state.
|
|
1048
|
+
* Using model() to satisfy FormValueControl interface requirements.
|
|
1049
|
+
*/
|
|
1050
|
+
disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
1051
|
+
/**
|
|
1052
|
+
* Touched state model - for validation styling.
|
|
1053
|
+
* Using model() to satisfy FormValueControl interface requirements.
|
|
1054
|
+
*/
|
|
1055
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : []));
|
|
1056
|
+
onChange = () => { };
|
|
1057
|
+
onTouched = () => { };
|
|
1058
|
+
isWritingValue = false;
|
|
1059
|
+
/**
|
|
1060
|
+
* Initialize the form control synchronization for ControlValueAccessor.
|
|
1061
|
+
* Must be called in the subclass constructor after super().
|
|
1062
|
+
*
|
|
1063
|
+
* Note: For signal forms ([formField]), this is not needed as
|
|
1064
|
+
* the FormField directive binds directly to the value model.
|
|
1065
|
+
*/
|
|
1066
|
+
initializeFormControl() {
|
|
1067
|
+
runInInjectionContext(this.injector, () => {
|
|
1068
|
+
effect(() => {
|
|
1069
|
+
const currentValue = this.value();
|
|
1070
|
+
if (!this.isWritingValue) {
|
|
1071
|
+
untracked(() => this.onChange(currentValue));
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
// ============================================
|
|
1077
|
+
// ControlValueAccessor Implementation
|
|
1078
|
+
// (For reactive forms: formControl, formControlName)
|
|
1079
|
+
// ============================================
|
|
1080
|
+
/** ControlValueAccessor: Write value from form control to signal */
|
|
1081
|
+
writeValue(value) {
|
|
1082
|
+
this.isWritingValue = true;
|
|
1083
|
+
this.value.set(value);
|
|
1084
|
+
this.isWritingValue = false;
|
|
1085
|
+
}
|
|
1086
|
+
/** ControlValueAccessor: Register change callback */
|
|
1087
|
+
registerOnChange(fn) {
|
|
1088
|
+
this.onChange = fn;
|
|
1089
|
+
}
|
|
1090
|
+
/** ControlValueAccessor: Register touched callback */
|
|
1091
|
+
registerOnTouched(fn) {
|
|
1092
|
+
this.onTouched = fn;
|
|
1093
|
+
}
|
|
1094
|
+
/** ControlValueAccessor: Set disabled state from form control */
|
|
1095
|
+
setDisabledState(isDisabled) {
|
|
1096
|
+
this.disabled.set(isDisabled);
|
|
1097
|
+
}
|
|
1098
|
+
// ============================================
|
|
1099
|
+
// FormValueControl Implementation
|
|
1100
|
+
// (For signal forms: [formField])
|
|
1101
|
+
// ============================================
|
|
1102
|
+
// The FormValueControl interface only requires:
|
|
1103
|
+
// - value: ModelSignal<T> (already declared as abstract above)
|
|
1104
|
+
// The FormField directive automatically binds to the value model.
|
|
1105
|
+
/** Mark the control as touched (call on blur or panel close) */
|
|
1106
|
+
markAsTouched() {
|
|
1107
|
+
if (!this.touched()) {
|
|
1108
|
+
this.touched.set(true);
|
|
1109
|
+
this.onTouched();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BaseFormControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1113
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: BaseFormControl, isStandalone: true, inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", touched: "touchedChange" }, ngImport: i0 });
|
|
1114
|
+
}
|
|
1115
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BaseFormControl, decorators: [{
|
|
1116
|
+
type: Directive
|
|
1117
|
+
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }] } });
|
|
1118
|
+
/**
|
|
1119
|
+
* Factory function to provide NG_VALUE_ACCESSOR for a component.
|
|
1120
|
+
* Use in component providers array.
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* @Component({
|
|
1125
|
+
* providers: [provideValueAccessor(LazySelectComponent)]
|
|
1126
|
+
* })
|
|
1127
|
+
* export class LazySelectComponent extends BaseFormControl<string | null> {}
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
1130
|
+
function provideValueAccessor(component) {
|
|
1131
|
+
return {
|
|
1132
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1133
|
+
useExisting: forwardRef(() => component),
|
|
1134
|
+
multi: true
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Lazy-loading multi-select component with search, pagination, and select-all.
|
|
1140
|
+
*
|
|
1141
|
+
* Supports ALL Angular form patterns:
|
|
1142
|
+
* - Template-driven: `[(value)]="selectedIds"`
|
|
1143
|
+
* - Reactive forms: `[formControl]="ctrl"` or `formControlName="field"`
|
|
1144
|
+
* - Signal forms: `[formField]="formTree.field"`
|
|
1145
|
+
*/
|
|
1146
|
+
class LazyMultiSelectComponent extends BaseFormControl {
|
|
1147
|
+
// Inputs
|
|
1148
|
+
placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1149
|
+
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
1150
|
+
isLoading = input.required(...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
1151
|
+
total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1152
|
+
pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1153
|
+
selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
|
|
1154
|
+
/** Two-way bound value using model() for signal forms compatibility */
|
|
1155
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1156
|
+
// Outputs
|
|
1157
|
+
onSearch = output();
|
|
1158
|
+
onPagination = output();
|
|
1159
|
+
// UI signals
|
|
1160
|
+
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1161
|
+
/** Computed: Display text for selected values (replaces getSelectedValue method) */
|
|
1162
|
+
selectedValueDisplay = computed(() => {
|
|
1163
|
+
const selectedValues = this.value() ?? [];
|
|
1164
|
+
if (selectedValues.length === 0)
|
|
1165
|
+
return '';
|
|
1166
|
+
if (selectedValues.length > 3) {
|
|
1167
|
+
return `${selectedValues.length} Items Selected`;
|
|
1168
|
+
}
|
|
1169
|
+
return this.selectDataList()
|
|
1170
|
+
.filter(item => selectedValues.includes(item.value))
|
|
1171
|
+
.map(item => item.label)
|
|
1172
|
+
.join(', ');
|
|
1173
|
+
}, ...(ngDevMode ? [{ debugName: "selectedValueDisplay" }] : []));
|
|
1174
|
+
/** Computed: Whether all items are selected (replaces isSelectAll signal) */
|
|
1175
|
+
isSelectAll = computed(() => {
|
|
1176
|
+
const selectedValues = this.value() ?? [];
|
|
1177
|
+
const allValues = this.selectDataList().map(item => item.value);
|
|
1178
|
+
if (selectedValues.length === 0 || allValues.length === 0)
|
|
1179
|
+
return false;
|
|
1180
|
+
return allValues.every(val => selectedValues.includes(val));
|
|
1181
|
+
}, ...(ngDevMode ? [{ debugName: "isSelectAll" }] : []));
|
|
1182
|
+
constructor() {
|
|
1183
|
+
super();
|
|
1184
|
+
this.initializeFormControl();
|
|
1185
|
+
// Search debounce effect
|
|
1186
|
+
runInInjectionContext(this.injector, () => {
|
|
1187
|
+
toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1((value) => {
|
|
1188
|
+
this.onSearch.emit(value);
|
|
1189
|
+
})), { initialValue: this.searchTerm() });
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
// Signal to toggle panel
|
|
1193
|
+
scrollTargetEl = null;
|
|
1194
|
+
onScrollBound = this.onScroll.bind(this);
|
|
1195
|
+
multiScrollContainer = viewChild.required('multiScrollContainer');
|
|
1196
|
+
onScroll(event) {
|
|
1197
|
+
const el = event.target;
|
|
1198
|
+
const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
|
|
1199
|
+
if (nearBottom && !this.isLoading()) {
|
|
1200
|
+
const pagination = this.pagination();
|
|
1201
|
+
const nextPage = pagination.currentPage + 1;
|
|
1202
|
+
const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
|
|
1203
|
+
if (hasMore) {
|
|
1204
|
+
this.onPagination.emit({ ...pagination, currentPage: nextPage });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
pSelectRef = viewChild.required('pSelect');
|
|
1209
|
+
openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
|
|
1210
|
+
onSelectClick(event) {
|
|
1211
|
+
if (this.disabled())
|
|
1212
|
+
return;
|
|
1213
|
+
this.pSelectRef()?.nativeElement.classList.add('p-focus');
|
|
1214
|
+
this.openOptions.update((isOpen) => !isOpen);
|
|
1215
|
+
}
|
|
1216
|
+
onOverlayClick(event) {
|
|
1217
|
+
event.stopPropagation();
|
|
1218
|
+
}
|
|
1219
|
+
handleDocumentClick(event) {
|
|
1220
|
+
const clickedInside = this.pSelectRef()?.nativeElement.contains(event.target);
|
|
1221
|
+
if (!clickedInside) {
|
|
1222
|
+
this.openOptions.set(false);
|
|
1223
|
+
this.pSelectRef()?.nativeElement.classList.remove('p-focus');
|
|
1224
|
+
this.markAsTouched();
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
isSelected(data) {
|
|
1228
|
+
return this.value()?.includes(data.value);
|
|
1229
|
+
}
|
|
1230
|
+
key(option) {
|
|
1231
|
+
return option.value;
|
|
1232
|
+
}
|
|
1233
|
+
selectValue(event, option) {
|
|
1234
|
+
const previousValue = this.value() ?? [];
|
|
1235
|
+
if (event.checked) {
|
|
1236
|
+
if (!previousValue.includes(option.value)) {
|
|
1237
|
+
this.value.set([...previousValue, option.value]);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
const updated = previousValue.filter((v) => v !== option.value);
|
|
1242
|
+
this.value.set(updated);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
changeSelectAll(event) {
|
|
1246
|
+
if (event.checked) {
|
|
1247
|
+
this.value.set(this.selectDataList().map((item) => item.value));
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
this.value.set([]);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
clear(event) {
|
|
1254
|
+
event.stopPropagation();
|
|
1255
|
+
this.value.set([]);
|
|
1256
|
+
}
|
|
1257
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1258
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: LazyMultiSelectComponent, isStandalone: true, selector: "lib-lazy-multi-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, host: { listeners: { "document:click": "handleDocumentClick($event)" } }, providers: [provideValueAccessor(LazyMultiSelectComponent)], viewQueries: [{ propertyName: "multiScrollContainer", first: true, predicate: ["multiScrollContainer"], descendants: true, isSignal: true }, { propertyName: "pSelectRef", first: true, predicate: ["pSelect"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\"\n [class.p-disabled]=\"disabled()\">\n @if(selectedValueDisplay()){\n <span class=\"p-select-label\">{{selectedValueDisplay()}}</span>\n }@else {\n <span class=\"p-select-label p-placeholder\">{{placeHolder()}}</span>\n }\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\"><i class=\"pi pi-times\"></i></span>\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n data-p-icon=\"chevron-down\" class=\"p-multiselect-dropdown-icon p-icon ng-star-inserted\"\n data-pc-section=\"triggericon\" aria-hidden=\"true\" pc75=\"\">\n <path\n d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\"\n fill=\"currentColor\"></path>\n </svg>\n </span>\n </div>\n @if(openOptions()){\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox binary=\"true\" (onChange)=\"changeSelectAll($event)\" [ngModel]=\"isSelectAll()\" [disabled]=\"disabled()\"/>\n <input type=\"text\" pInputText class=\"w-full\" [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\" [ngModelOptions]=\"{ standalone: true }\"\n placeholder=\"Search...\" />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data); let i = $index) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox binary=\"true\" (onChange)=\"selectValue($event,data)\" [ngModel]=\"isSelected(data)\" [disabled]=\"disabled()\" />\n <span>{{data.label}}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{top:33px;z-index:1004;transform-origin:center top;margin-top:2px}.p-select-option:hover{background:var(--p-select-option-focus-background);color:var(--p-select-option-focus-color)}.p-select-list-container{max-height:200px}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background);color:var(--p-select-option-selected-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background);color:var(--p-select-option-selected-focus-color)}\n"], dependencies: [{ kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i4.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: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1259
|
+
}
|
|
1260
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
|
|
1261
|
+
type: Component,
|
|
1262
|
+
args: [{ selector: 'lib-lazy-multi-select', imports: [PrimeModule, AngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazyMultiSelectComponent)], template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\"\n [class.p-disabled]=\"disabled()\">\n @if(selectedValueDisplay()){\n <span class=\"p-select-label\">{{selectedValueDisplay()}}</span>\n }@else {\n <span class=\"p-select-label p-placeholder\">{{placeHolder()}}</span>\n }\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\"><i class=\"pi pi-times\"></i></span>\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n data-p-icon=\"chevron-down\" class=\"p-multiselect-dropdown-icon p-icon ng-star-inserted\"\n data-pc-section=\"triggericon\" aria-hidden=\"true\" pc75=\"\">\n <path\n d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\"\n fill=\"currentColor\"></path>\n </svg>\n </span>\n </div>\n @if(openOptions()){\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox binary=\"true\" (onChange)=\"changeSelectAll($event)\" [ngModel]=\"isSelectAll()\" [disabled]=\"disabled()\"/>\n <input type=\"text\" pInputText class=\"w-full\" [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\" [ngModelOptions]=\"{ standalone: true }\"\n placeholder=\"Search...\" />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data); let i = $index) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox binary=\"true\" (onChange)=\"selectValue($event,data)\" [ngModel]=\"isSelected(data)\" [disabled]=\"disabled()\" />\n <span>{{data.label}}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{top:33px;z-index:1004;transform-origin:center top;margin-top:2px}.p-select-option:hover{background:var(--p-select-option-focus-background);color:var(--p-select-option-focus-color)}.p-select-list-container{max-height:200px}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background);color:var(--p-select-option-selected-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background);color:var(--p-select-option-selected-focus-color)}\n"] }]
|
|
1263
|
+
}], ctorParameters: () => [], propDecorators: { placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }], multiScrollContainer: [{ type: i0.ViewChild, args: ['multiScrollContainer', { isSignal: true }] }], pSelectRef: [{ type: i0.ViewChild, args: ['pSelect', { isSignal: true }] }], handleDocumentClick: [{
|
|
1264
|
+
type: HostListener,
|
|
1265
|
+
args: ['document:click', ['$event']]
|
|
1266
|
+
}] } });
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Lazy-loading single select component with search and pagination.
|
|
1270
|
+
*
|
|
1271
|
+
* Supports ALL Angular form patterns:
|
|
1272
|
+
* - Template-driven: `[(value)]="selectedId"`
|
|
1273
|
+
* - Reactive forms: `[formControl]="ctrl"` or `formControlName="field"`
|
|
1274
|
+
* - Signal forms: `[formField]="formTree.field"`
|
|
1275
|
+
*/
|
|
1276
|
+
class LazySelectComponent extends BaseFormControl {
|
|
1277
|
+
// Inputs
|
|
1278
|
+
placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1279
|
+
optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
|
|
1280
|
+
optionValue = input.required(...(ngDevMode ? [{ debugName: "optionValue" }] : []));
|
|
1281
|
+
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
1282
|
+
isLoading = input.required(...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
1283
|
+
total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1284
|
+
pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1285
|
+
selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
|
|
1286
|
+
/** Two-way bound value using model() for signal forms compatibility */
|
|
1287
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1288
|
+
// Outputs
|
|
1289
|
+
onSearch = output();
|
|
1290
|
+
onPagination = output();
|
|
1291
|
+
// UI signals
|
|
1292
|
+
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1293
|
+
// Effect hooks
|
|
1294
|
+
constructor() {
|
|
1295
|
+
super();
|
|
1296
|
+
this.initializeFormControl();
|
|
1297
|
+
runInInjectionContext(this.injector, () => {
|
|
1298
|
+
toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1(value => {
|
|
1299
|
+
this.onSearch.emit(value);
|
|
1300
|
+
})), { initialValue: this.searchTerm() });
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
// Signal to toggle panel
|
|
1304
|
+
isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
|
|
1305
|
+
scrollTargetEl = null;
|
|
1306
|
+
onScrollBound = this.onScroll.bind(this);
|
|
1307
|
+
scrollContainer = viewChild.required('scrollContainer');
|
|
1308
|
+
onScroll(event) {
|
|
1309
|
+
const el = event.target;
|
|
1310
|
+
const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
|
|
1311
|
+
if (nearBottom && !this.isLoading()) {
|
|
1312
|
+
const pagination = this.pagination();
|
|
1313
|
+
const nextPage = pagination.currentPage + 1;
|
|
1314
|
+
const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
|
|
1315
|
+
if (hasMore) {
|
|
1316
|
+
this.onPagination.emit({ ...pagination, currentPage: nextPage });
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// Toggle panel and manage scroll event
|
|
1321
|
+
showPanel() {
|
|
1322
|
+
if (this.disabled())
|
|
1323
|
+
return;
|
|
1324
|
+
this.isPanelShow.update(prev => !prev);
|
|
1325
|
+
const isNowVisible = this.isPanelShow();
|
|
1326
|
+
if (isNowVisible) {
|
|
1327
|
+
setTimeout(() => {
|
|
1328
|
+
const containerEl = this.scrollContainer().nativeElement;
|
|
1329
|
+
const target = containerEl.querySelector('.p-select-list-container');
|
|
1330
|
+
if (target) {
|
|
1331
|
+
target.addEventListener('scroll', this.onScrollBound);
|
|
1332
|
+
this.scrollTargetEl = target;
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
console.warn('.p-select-list-container not found after panel show');
|
|
1336
|
+
}
|
|
1337
|
+
}, 0);
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
if (this.scrollTargetEl) {
|
|
1341
|
+
this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
|
|
1342
|
+
this.scrollTargetEl = null;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
onBlur() {
|
|
1347
|
+
this.markAsTouched();
|
|
1348
|
+
}
|
|
1349
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1350
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", type: LazySelectComponent, isStandalone: true, selector: "lib-lazy-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: true, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: true, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, providers: [provideValueAccessor(LazySelectComponent)], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n [options]=\"selectDataList()\"\n [(ngModel)]=\"value\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n class=\"w-full\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\">\n <ng-template let-filter #filter>\n <input\n pInputText\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\"\n [ngModelOptions]=\"{standalone:true}\"\n class=\"w-full\" />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template let-item #item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i4.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: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i3.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "directive", type: EditModeElementChangerDirective, selector: "[appEditModeElementChanger]", inputs: ["isEditMode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1351
|
+
}
|
|
1352
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: LazySelectComponent, decorators: [{
|
|
1353
|
+
type: Component,
|
|
1354
|
+
args: [{ selector: 'lib-lazy-select', imports: [
|
|
1355
|
+
AngularModule,
|
|
1356
|
+
PrimeModule,
|
|
1357
|
+
EditModeElementChangerDirective
|
|
1358
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazySelectComponent)], template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n [options]=\"selectDataList()\"\n [(ngModel)]=\"value\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n class=\"w-full\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\">\n <ng-template let-filter #filter>\n <input\n pInputText\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\"\n [ngModelOptions]=\"{standalone:true}\"\n class=\"w-full\" />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template let-item #item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n" }]
|
|
1359
|
+
}], ctorParameters: () => [], propDecorators: { placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: true }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: true }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }], scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }] } });
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Injection Tokens for Provider Interfaces
|
|
1363
|
+
*
|
|
1364
|
+
* These tokens enable dependency injection of provider implementations
|
|
1365
|
+
* without creating direct package dependencies.
|
|
1366
|
+
*
|
|
1367
|
+
* Pattern:
|
|
1368
|
+
* 1. ng-shared defines interfaces and tokens (no implementation)
|
|
1369
|
+
* 2. ng-auth provides implementations
|
|
1370
|
+
* 3. ng-iam and ng-storage inject via tokens
|
|
1371
|
+
* 4. app.config.ts wires everything together
|
|
1372
|
+
*
|
|
1373
|
+
* @example
|
|
1374
|
+
* // In IAM component
|
|
1375
|
+
* private readonly userProvider = inject(USER_PROVIDER);
|
|
1376
|
+
*
|
|
1377
|
+
* // In app.config.ts
|
|
1378
|
+
* providers: [
|
|
1379
|
+
* { provide: USER_PROVIDER, useClass: AuthUserProvider },
|
|
1380
|
+
* ]
|
|
1381
|
+
*/
|
|
1382
|
+
/**
|
|
1383
|
+
* User Provider Token
|
|
1384
|
+
*
|
|
1385
|
+
* Provides user data access for IAM user selection.
|
|
1386
|
+
*/
|
|
1387
|
+
const USER_PROVIDER = new InjectionToken('USER_PROVIDER', {
|
|
1388
|
+
providedIn: 'root',
|
|
1389
|
+
factory: () => {
|
|
1390
|
+
throw new Error('USER_PROVIDER not configured. Please provide an implementation in app.config.ts');
|
|
1391
|
+
},
|
|
1392
|
+
});
|
|
1393
|
+
/**
|
|
1394
|
+
* Company API Provider Token
|
|
1395
|
+
*
|
|
1396
|
+
* Provides company data access for IAM company selection.
|
|
1397
|
+
*/
|
|
1398
|
+
const COMPANY_API_PROVIDER = new InjectionToken('COMPANY_API_PROVIDER', {
|
|
1399
|
+
providedIn: 'root',
|
|
1400
|
+
factory: () => {
|
|
1401
|
+
throw new Error('COMPANY_API_PROVIDER not configured. Please provide an implementation in app.config.ts');
|
|
1402
|
+
},
|
|
1403
|
+
});
|
|
1404
|
+
/**
|
|
1405
|
+
* User Permission Provider Token
|
|
1406
|
+
*
|
|
1407
|
+
* Provides user permission assignment operations for IAM.
|
|
1408
|
+
*/
|
|
1409
|
+
const USER_PERMISSION_PROVIDER = new InjectionToken('USER_PERMISSION_PROVIDER', {
|
|
1410
|
+
providedIn: 'root',
|
|
1411
|
+
factory: () => {
|
|
1412
|
+
throw new Error('USER_PERMISSION_PROVIDER not configured. Please provide an implementation in app.config.ts');
|
|
1413
|
+
},
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Permission Guard
|
|
1418
|
+
*
|
|
1419
|
+
* Route-level guard for permission-based access control.
|
|
1420
|
+
* Validates permissions before allowing navigation.
|
|
1421
|
+
*
|
|
1422
|
+
* Features:
|
|
1423
|
+
* - Single permission check
|
|
1424
|
+
* - Complex ILogicNode logic trees
|
|
1425
|
+
* - Configurable redirect URL
|
|
1426
|
+
* - Debug logging for denied access
|
|
1427
|
+
*
|
|
1428
|
+
* @example
|
|
1429
|
+
* ```typescript
|
|
1430
|
+
* // Simple permission check
|
|
1431
|
+
* { path: 'users', canActivate: [permissionGuard('user.view')] }
|
|
1432
|
+
*
|
|
1433
|
+
* // Complex logic
|
|
1434
|
+
* { path: 'admin', canActivate: [permissionGuard({
|
|
1435
|
+
* id: 'root',
|
|
1436
|
+
* type: 'group',
|
|
1437
|
+
* operator: 'AND',
|
|
1438
|
+
* children: [
|
|
1439
|
+
* { id: '1', type: 'action', actionId: 'admin.view' },
|
|
1440
|
+
* { id: '2', type: 'action', actionId: 'admin.manage' }
|
|
1441
|
+
* ]
|
|
1442
|
+
* })] }
|
|
1443
|
+
*
|
|
1444
|
+
* // With custom redirect
|
|
1445
|
+
* { path: 'users', canActivate: [permissionGuard('user.view', '/access-denied')] }
|
|
1446
|
+
* ```
|
|
1447
|
+
*/
|
|
1448
|
+
function permissionGuard(permission, redirectTo = '/') {
|
|
1449
|
+
return () => {
|
|
1450
|
+
const permissionValidator = inject(PermissionValidatorService);
|
|
1451
|
+
const router = inject(Router);
|
|
1452
|
+
// Check if permissions are loaded
|
|
1453
|
+
if (!permissionValidator.isPermissionsLoaded()) {
|
|
1454
|
+
console.warn('[permissionGuard] Permissions not loaded, denying access to route');
|
|
1455
|
+
return router.createUrlTree([redirectTo]);
|
|
1456
|
+
}
|
|
1457
|
+
const userPermissions = permissionValidator.permissions();
|
|
1458
|
+
const hasPermission = evaluatePermission(permission, userPermissions);
|
|
1459
|
+
if (!hasPermission) {
|
|
1460
|
+
const permissionCode = typeof permission === 'string' ? permission : 'complex-logic';
|
|
1461
|
+
console.warn(`[permissionGuard] Access denied - missing permission: ${permissionCode}`);
|
|
1462
|
+
return router.createUrlTree([redirectTo]);
|
|
1463
|
+
}
|
|
1464
|
+
return true;
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Any Permission Guard (OR logic)
|
|
1469
|
+
*
|
|
1470
|
+
* Allows access if user has ANY of the specified permissions.
|
|
1471
|
+
*
|
|
1472
|
+
* @example
|
|
1473
|
+
* ```typescript
|
|
1474
|
+
* // Allow if user has view OR create permission
|
|
1475
|
+
* { path: 'users', canActivate: [anyPermissionGuard(['user.view', 'user.create'])] }
|
|
1476
|
+
* ```
|
|
1477
|
+
*/
|
|
1478
|
+
function anyPermissionGuard(permissions, redirectTo = '/') {
|
|
1479
|
+
return () => {
|
|
1480
|
+
const permissionValidator = inject(PermissionValidatorService);
|
|
1481
|
+
const router = inject(Router);
|
|
1482
|
+
// Validate permissions array
|
|
1483
|
+
if (!permissions || permissions.length === 0) {
|
|
1484
|
+
console.warn('[anyPermissionGuard] Empty permissions array provided, denying access');
|
|
1485
|
+
return router.createUrlTree([redirectTo]);
|
|
1486
|
+
}
|
|
1487
|
+
// Check if permissions are loaded
|
|
1488
|
+
if (!permissionValidator.isPermissionsLoaded()) {
|
|
1489
|
+
console.warn('[anyPermissionGuard] Permissions not loaded, denying access to route');
|
|
1490
|
+
return router.createUrlTree([redirectTo]);
|
|
1491
|
+
}
|
|
1492
|
+
const userPermissions = permissionValidator.permissions();
|
|
1493
|
+
const hasPermission = hasAnyPermission(permissions, userPermissions);
|
|
1494
|
+
if (!hasPermission) {
|
|
1495
|
+
console.warn(`[anyPermissionGuard] Access denied - missing any of: ${permissions.join(', ')}`);
|
|
1496
|
+
return router.createUrlTree([redirectTo]);
|
|
1497
|
+
}
|
|
1498
|
+
return true;
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* All Permissions Guard (AND logic)
|
|
1503
|
+
*
|
|
1504
|
+
* Allows access only if user has ALL of the specified permissions.
|
|
1505
|
+
*
|
|
1506
|
+
* @example
|
|
1507
|
+
* ```typescript
|
|
1508
|
+
* // Allow only if user has BOTH view AND create permissions
|
|
1509
|
+
* { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
|
|
1510
|
+
* ```
|
|
1511
|
+
*/
|
|
1512
|
+
function allPermissionsGuard(permissions, redirectTo = '/') {
|
|
1513
|
+
return () => {
|
|
1514
|
+
const permissionValidator = inject(PermissionValidatorService);
|
|
1515
|
+
const router = inject(Router);
|
|
1516
|
+
// Validate permissions array
|
|
1517
|
+
if (!permissions || permissions.length === 0) {
|
|
1518
|
+
console.warn('[allPermissionsGuard] Empty permissions array provided, denying access');
|
|
1519
|
+
return router.createUrlTree([redirectTo]);
|
|
1520
|
+
}
|
|
1521
|
+
// Check if permissions are loaded
|
|
1522
|
+
if (!permissionValidator.isPermissionsLoaded()) {
|
|
1523
|
+
console.warn('[allPermissionsGuard] Permissions not loaded, denying access to route');
|
|
1524
|
+
return router.createUrlTree([redirectTo]);
|
|
1525
|
+
}
|
|
1526
|
+
const userPermissions = permissionValidator.permissions();
|
|
1527
|
+
const hasPermission = hasAllPermissions(permissions, userPermissions);
|
|
1528
|
+
if (!hasPermission) {
|
|
1529
|
+
console.warn(`[allPermissionsGuard] Access denied - missing all of: ${permissions.join(', ')}`);
|
|
1530
|
+
return router.createUrlTree([redirectTo]);
|
|
1531
|
+
}
|
|
1532
|
+
return true;
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Interfaces
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* Generated bundle index. Do not edit.
|
|
1540
|
+
*/
|
|
1541
|
+
|
|
1542
|
+
export { AngularModule, ApiResourceService, ApiResourceService as ApiService, COMPANY_API_PROVIDER, ContactTypeEnum, CookieService, EditModeElementChangerDirective, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, USER_PERMISSION_PROVIDER, USER_PROVIDER, allPermissionsGuard, anyPermissionGuard, evaluateLogicNode, evaluatePermission, hasAllPermissions, hasAnyPermission, permissionGuard };
|
|
1543
|
+
//# sourceMappingURL=flusys-ng-shared.mjs.map
|