@flusys/ng-shared 1.1.1-beta → 3.0.0-rc
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/README.md +105 -3
- package/fesm2022/flusys-ng-shared.mjs +1091 -743
- package/fesm2022/flusys-ng-shared.mjs.map +1 -1
- package/package.json +3 -3
- package/types/flusys-ng-shared.d.ts +799 -586
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output,
|
|
3
|
-
import * as
|
|
2
|
+
import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output, NgModule, Injector, runInInjectionContext, resource, model, untracked, forwardRef, ChangeDetectionStrategy, Component, DestroyRef, viewChild, afterNextRender, InjectionToken, isDevMode } from '@angular/core';
|
|
3
|
+
import * as i3 from '@angular/common';
|
|
4
4
|
import { isPlatformServer, CommonModule, NgOptimizedImage, NgComponentOutlet, DatePipe } from '@angular/common';
|
|
5
5
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
6
|
-
import { APP_CONFIG, getServiceUrl
|
|
7
|
-
import { of, firstValueFrom,
|
|
6
|
+
import { APP_CONFIG, getServiceUrl } from '@flusys/ng-core';
|
|
7
|
+
import { of, firstValueFrom, map as map$1 } from 'rxjs';
|
|
8
8
|
import { map, tap, catchError } from 'rxjs/operators';
|
|
9
|
-
import * as i1$
|
|
9
|
+
import * as i1$1 from '@angular/forms';
|
|
10
10
|
import { NgControl, FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
|
-
import { RouterOutlet, RouterLink, Router } from '@angular/router';
|
|
11
|
+
import { RouterOutlet, RouterLink, RouterLinkActive, Router, ActivatedRoute } from '@angular/router';
|
|
12
12
|
import { AutoCompleteModule } from 'primeng/autocomplete';
|
|
13
13
|
import { AvatarModule } from 'primeng/avatar';
|
|
14
|
-
import * as i1$
|
|
14
|
+
import * as i1$2 from 'primeng/button';
|
|
15
15
|
import { ButtonModule } from 'primeng/button';
|
|
16
16
|
import { CardModule } from 'primeng/card';
|
|
17
|
-
import * as i1
|
|
17
|
+
import * as i1 from 'primeng/checkbox';
|
|
18
18
|
import { CheckboxModule } from 'primeng/checkbox';
|
|
19
19
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
|
20
20
|
import { DatePickerModule } from 'primeng/datepicker';
|
|
21
|
-
import * as
|
|
21
|
+
import * as i4 from 'primeng/dialog';
|
|
22
22
|
import { DialogModule } from 'primeng/dialog';
|
|
23
23
|
import { DividerModule } from 'primeng/divider';
|
|
24
24
|
import { FileUploadModule } from 'primeng/fileupload';
|
|
@@ -38,7 +38,7 @@ import * as i2$1 from 'primeng/progressbar';
|
|
|
38
38
|
import { ProgressBarModule } from 'primeng/progressbar';
|
|
39
39
|
import { RadioButtonModule } from 'primeng/radiobutton';
|
|
40
40
|
import { RippleModule } from 'primeng/ripple';
|
|
41
|
-
import * as i3 from 'primeng/select';
|
|
41
|
+
import * as i3$1 from 'primeng/select';
|
|
42
42
|
import { SelectModule } from 'primeng/select';
|
|
43
43
|
import { SelectButtonModule } from 'primeng/selectbutton';
|
|
44
44
|
import { SkeletonModule } from 'primeng/skeleton';
|
|
@@ -52,12 +52,132 @@ import { ToastModule } from 'primeng/toast';
|
|
|
52
52
|
import { ToggleSwitchModule } from 'primeng/toggleswitch';
|
|
53
53
|
import { TooltipModule } from 'primeng/tooltip';
|
|
54
54
|
import { TreeTableModule } from 'primeng/treetable';
|
|
55
|
-
import {
|
|
56
|
-
import
|
|
57
|
-
import { MessageService } from 'primeng/api';
|
|
55
|
+
import { MessageService, ConfirmationService } from 'primeng/api';
|
|
56
|
+
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Centralized Permission Codes
|
|
60
|
+
*
|
|
61
|
+
* Single source of truth for all permission codes used across the application.
|
|
62
|
+
* Use these constants instead of hardcoded strings to prevent typos and enable easy refactoring.
|
|
63
|
+
*
|
|
64
|
+
* Naming Convention: <entity>.<action>
|
|
65
|
+
* - entity: The resource being accessed (e.g., user, role, company)
|
|
66
|
+
* - action: The operation being performed (create, read, update, delete, assign)
|
|
67
|
+
*/
|
|
68
|
+
// ==================== AUTH MODULE ====================
|
|
69
|
+
const USER_PERMISSIONS = {
|
|
70
|
+
CREATE: 'user.create',
|
|
71
|
+
READ: 'user.read',
|
|
72
|
+
UPDATE: 'user.update',
|
|
73
|
+
DELETE: 'user.delete',
|
|
74
|
+
};
|
|
75
|
+
const COMPANY_PERMISSIONS = {
|
|
76
|
+
CREATE: 'company.create',
|
|
77
|
+
READ: 'company.read',
|
|
78
|
+
UPDATE: 'company.update',
|
|
79
|
+
DELETE: 'company.delete',
|
|
80
|
+
};
|
|
81
|
+
const BRANCH_PERMISSIONS = {
|
|
82
|
+
CREATE: 'branch.create',
|
|
83
|
+
READ: 'branch.read',
|
|
84
|
+
UPDATE: 'branch.update',
|
|
85
|
+
DELETE: 'branch.delete',
|
|
86
|
+
};
|
|
87
|
+
// ==================== IAM MODULE ====================
|
|
88
|
+
const ACTION_PERMISSIONS = {
|
|
89
|
+
CREATE: 'action.create',
|
|
90
|
+
READ: 'action.read',
|
|
91
|
+
UPDATE: 'action.update',
|
|
92
|
+
DELETE: 'action.delete',
|
|
93
|
+
};
|
|
94
|
+
const ROLE_PERMISSIONS = {
|
|
95
|
+
CREATE: 'role.create',
|
|
96
|
+
READ: 'role.read',
|
|
97
|
+
UPDATE: 'role.update',
|
|
98
|
+
DELETE: 'role.delete',
|
|
99
|
+
};
|
|
100
|
+
const ROLE_ACTION_PERMISSIONS = {
|
|
101
|
+
READ: 'role-action.read',
|
|
102
|
+
ASSIGN: 'role-action.assign',
|
|
103
|
+
};
|
|
104
|
+
const USER_ROLE_PERMISSIONS = {
|
|
105
|
+
READ: 'user-role.read',
|
|
106
|
+
ASSIGN: 'user-role.assign',
|
|
107
|
+
};
|
|
108
|
+
const USER_ACTION_PERMISSIONS = {
|
|
109
|
+
READ: 'user-action.read',
|
|
110
|
+
ASSIGN: 'user-action.assign',
|
|
111
|
+
};
|
|
112
|
+
const COMPANY_ACTION_PERMISSIONS = {
|
|
113
|
+
READ: 'company-action.read',
|
|
114
|
+
ASSIGN: 'company-action.assign',
|
|
115
|
+
};
|
|
116
|
+
// ==================== STORAGE MODULE ====================
|
|
117
|
+
const FILE_PERMISSIONS = {
|
|
118
|
+
CREATE: 'file.create',
|
|
119
|
+
READ: 'file.read',
|
|
120
|
+
UPDATE: 'file.update',
|
|
121
|
+
DELETE: 'file.delete',
|
|
122
|
+
};
|
|
123
|
+
const FOLDER_PERMISSIONS = {
|
|
124
|
+
CREATE: 'folder.create',
|
|
125
|
+
READ: 'folder.read',
|
|
126
|
+
UPDATE: 'folder.update',
|
|
127
|
+
DELETE: 'folder.delete',
|
|
128
|
+
};
|
|
129
|
+
const STORAGE_CONFIG_PERMISSIONS = {
|
|
130
|
+
CREATE: 'storage-config.create',
|
|
131
|
+
READ: 'storage-config.read',
|
|
132
|
+
UPDATE: 'storage-config.update',
|
|
133
|
+
DELETE: 'storage-config.delete',
|
|
134
|
+
};
|
|
135
|
+
// ==================== EMAIL MODULE ====================
|
|
136
|
+
const EMAIL_CONFIG_PERMISSIONS = {
|
|
137
|
+
CREATE: 'email-config.create',
|
|
138
|
+
READ: 'email-config.read',
|
|
139
|
+
UPDATE: 'email-config.update',
|
|
140
|
+
DELETE: 'email-config.delete',
|
|
141
|
+
};
|
|
142
|
+
const EMAIL_TEMPLATE_PERMISSIONS = {
|
|
143
|
+
CREATE: 'email-template.create',
|
|
144
|
+
READ: 'email-template.read',
|
|
145
|
+
UPDATE: 'email-template.update',
|
|
146
|
+
DELETE: 'email-template.delete',
|
|
147
|
+
};
|
|
148
|
+
// ==================== FORM BUILDER MODULE ====================
|
|
149
|
+
const FORM_PERMISSIONS = {
|
|
150
|
+
CREATE: 'form.create',
|
|
151
|
+
READ: 'form.read',
|
|
152
|
+
UPDATE: 'form.update',
|
|
153
|
+
DELETE: 'form.delete',
|
|
154
|
+
};
|
|
155
|
+
// ==================== AGGREGATED EXPORTS ====================
|
|
156
|
+
/**
|
|
157
|
+
* All permission codes grouped by module
|
|
158
|
+
*/
|
|
159
|
+
const PERMISSIONS = {
|
|
160
|
+
// Auth
|
|
161
|
+
USER: USER_PERMISSIONS,
|
|
162
|
+
COMPANY: COMPANY_PERMISSIONS,
|
|
163
|
+
BRANCH: BRANCH_PERMISSIONS,
|
|
164
|
+
// IAM
|
|
165
|
+
ACTION: ACTION_PERMISSIONS,
|
|
166
|
+
ROLE: ROLE_PERMISSIONS,
|
|
167
|
+
ROLE_ACTION: ROLE_ACTION_PERMISSIONS,
|
|
168
|
+
USER_ROLE: USER_ROLE_PERMISSIONS,
|
|
169
|
+
USER_ACTION: USER_ACTION_PERMISSIONS,
|
|
170
|
+
COMPANY_ACTION: COMPANY_ACTION_PERMISSIONS,
|
|
171
|
+
// Storage
|
|
172
|
+
FILE: FILE_PERMISSIONS,
|
|
173
|
+
FOLDER: FOLDER_PERMISSIONS,
|
|
174
|
+
STORAGE_CONFIG: STORAGE_CONFIG_PERMISSIONS,
|
|
175
|
+
// Email
|
|
176
|
+
EMAIL_CONFIG: EMAIL_CONFIG_PERMISSIONS,
|
|
177
|
+
EMAIL_TEMPLATE: EMAIL_TEMPLATE_PERMISSIONS,
|
|
178
|
+
// Form Builder
|
|
179
|
+
FORM: FORM_PERMISSIONS,
|
|
180
|
+
};
|
|
61
181
|
|
|
62
182
|
/**
|
|
63
183
|
* Common file type filters
|
|
@@ -151,9 +271,7 @@ class PlatformService {
|
|
|
151
271
|
}
|
|
152
272
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, decorators: [{
|
|
153
273
|
type: Injectable,
|
|
154
|
-
args: [{
|
|
155
|
-
providedIn: 'root'
|
|
156
|
-
}]
|
|
274
|
+
args: [{ providedIn: 'root' }]
|
|
157
275
|
}] });
|
|
158
276
|
|
|
159
277
|
class CookieService {
|
|
@@ -175,96 +293,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
175
293
|
|
|
176
294
|
/**
|
|
177
295
|
* Service to fetch file URLs from the backend.
|
|
178
|
-
*
|
|
179
|
-
* - Handles presigned URLs for cloud storage (AWS S3, Azure)
|
|
180
|
-
* - Auto-refreshes expired URLs
|
|
181
|
-
* - Validates file access permissions
|
|
182
|
-
* - Works with all storage providers (Local, S3, Azure, SFTP)
|
|
296
|
+
* Handles presigned URLs for cloud storage, auto-refresh, and caching.
|
|
183
297
|
*/
|
|
184
298
|
class FileUrlService {
|
|
185
299
|
http = inject(HttpClient);
|
|
186
300
|
appConfig = inject(APP_CONFIG);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Get file URL by ID from cache (reactive signal)
|
|
191
|
-
*/
|
|
301
|
+
_cache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_cache" }] : []));
|
|
302
|
+
cache = this._cache.asReadonly();
|
|
303
|
+
/** Get file URL by ID from cache (synchronous) */
|
|
192
304
|
getFileUrl(fileId) {
|
|
193
305
|
if (!fileId)
|
|
194
306
|
return null;
|
|
195
|
-
return this.
|
|
307
|
+
return this._cache().get(fileId)?.url ?? null;
|
|
196
308
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Get file URL signal (computed from cache)
|
|
199
|
-
*/
|
|
309
|
+
/** Get file URL as computed signal */
|
|
200
310
|
fileUrlSignal(fileId) {
|
|
201
311
|
return computed(() => this.getFileUrl(fileId));
|
|
202
312
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Fetch file URLs from backend and update cache.
|
|
205
|
-
* Skips IDs already in cache to prevent duplicate calls.
|
|
206
|
-
* Returns Observable of fetched files (including cached ones).
|
|
207
|
-
*/
|
|
313
|
+
/** Fetch file URLs from backend and update cache */
|
|
208
314
|
fetchFileUrls(fileIds, forceRefresh = false) {
|
|
209
315
|
if (!fileIds.length)
|
|
210
316
|
return of([]);
|
|
211
|
-
const cache = this.
|
|
212
|
-
// Filter out IDs already in cache (unless force refresh)
|
|
317
|
+
const cache = this._cache();
|
|
213
318
|
const missingIds = forceRefresh
|
|
214
319
|
? fileIds
|
|
215
320
|
: fileIds.filter((id) => !cache.has(id));
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const cachedFiles = fileIds
|
|
219
|
-
.map((id) => cache.get(id))
|
|
220
|
-
.filter((f) => !!f);
|
|
221
|
-
return of(cachedFiles);
|
|
321
|
+
if (!missingIds.length) {
|
|
322
|
+
return of(this.getFromCache(fileIds));
|
|
222
323
|
}
|
|
223
324
|
const requestDto = missingIds.map((id) => ({ id }));
|
|
224
325
|
return this.http
|
|
225
326
|
.post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto)
|
|
226
|
-
.pipe(map((response) => response.data ?? []), tap((files) =>
|
|
227
|
-
// Update cache with new files
|
|
228
|
-
const newCache = new Map(this.urlCache());
|
|
229
|
-
files.forEach((file) => newCache.set(file.id, file));
|
|
230
|
-
this.urlCache.set(newCache);
|
|
231
|
-
}), map(() => {
|
|
232
|
-
// Return all requested files (cached + newly fetched)
|
|
233
|
-
const allCache = this.urlCache();
|
|
234
|
-
return fileIds
|
|
235
|
-
.map((id) => allCache.get(id))
|
|
236
|
-
.filter((f) => !!f);
|
|
237
|
-
}), catchError(() => of([])));
|
|
327
|
+
.pipe(map((response) => response.data ?? []), tap((files) => this.addToCache(files)), map(() => this.getFromCache(fileIds)), catchError(() => of([])));
|
|
238
328
|
}
|
|
239
|
-
/**
|
|
240
|
-
* Fetch a single file URL.
|
|
241
|
-
* Uses cache if available to prevent duplicate calls.
|
|
242
|
-
* Returns Observable of file info or null if not found.
|
|
243
|
-
*/
|
|
329
|
+
/** Fetch single file URL (delegates to fetchFileUrls) */
|
|
244
330
|
fetchSingleFileUrl(fileId, forceRefresh = false) {
|
|
245
|
-
// Return from cache immediately if available
|
|
246
|
-
if (!forceRefresh) {
|
|
247
|
-
const cached = this.urlCache().get(fileId);
|
|
248
|
-
if (cached) {
|
|
249
|
-
return of(cached);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
331
|
return this.fetchFileUrls([fileId], forceRefresh).pipe(map((files) => files[0] ?? null));
|
|
253
332
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Clear the URL cache.
|
|
256
|
-
* Useful on logout or when switching contexts.
|
|
257
|
-
*/
|
|
333
|
+
/** Clear entire cache */
|
|
258
334
|
clearCache() {
|
|
259
|
-
this.
|
|
335
|
+
this._cache.set(new Map());
|
|
260
336
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Remove specific file from cache.
|
|
263
|
-
*/
|
|
337
|
+
/** Remove specific file from cache */
|
|
264
338
|
removeFromCache(fileId) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
339
|
+
this._cache.update((cache) => {
|
|
340
|
+
const next = new Map(cache);
|
|
341
|
+
next.delete(fileId);
|
|
342
|
+
return next;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
addToCache(files) {
|
|
346
|
+
this._cache.update((cache) => {
|
|
347
|
+
const next = new Map(cache);
|
|
348
|
+
for (const file of files) {
|
|
349
|
+
next.set(file.id, file);
|
|
350
|
+
}
|
|
351
|
+
return next;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
getFromCache(fileIds) {
|
|
355
|
+
const cache = this._cache();
|
|
356
|
+
return fileIds
|
|
357
|
+
.map((id) => cache.get(id))
|
|
358
|
+
.filter((f) => f !== undefined);
|
|
268
359
|
}
|
|
269
360
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
270
361
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, providedIn: 'root' });
|
|
@@ -275,79 +366,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
275
366
|
}] });
|
|
276
367
|
|
|
277
368
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
369
|
+
* Check if user has a specific permission using wildcard matching.
|
|
370
|
+
* Supports:
|
|
371
|
+
* - Exact match: 'user.create' matches 'user.create'
|
|
372
|
+
* - Full wildcard: '*' matches everything
|
|
373
|
+
* - Module wildcard: 'user.*' matches 'user.create', 'user.read', etc.
|
|
374
|
+
*/
|
|
375
|
+
function hasPermission(requiredPermission, userPermissions) {
|
|
376
|
+
// Exact match
|
|
377
|
+
if (userPermissions.includes(requiredPermission))
|
|
378
|
+
return true;
|
|
379
|
+
// Wildcard matching
|
|
380
|
+
for (const permission of userPermissions) {
|
|
381
|
+
// Full wildcard - grants all permissions
|
|
382
|
+
if (permission === '*')
|
|
383
|
+
return true;
|
|
384
|
+
// Module wildcard (e.g., 'user.*' matches 'user.create')
|
|
385
|
+
if (permission.endsWith('.*') &&
|
|
386
|
+
requiredPermission.startsWith(permission.slice(0, -1))) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
/** Evaluate permission logic (string or ILogicNode) against user permissions */
|
|
393
|
+
function evaluatePermission(logic, permissions) {
|
|
394
|
+
if (!logic)
|
|
395
|
+
return false;
|
|
396
|
+
if (typeof logic === 'string')
|
|
397
|
+
return hasPermission(logic, permissions);
|
|
398
|
+
return evaluateLogicNode(logic, permissions);
|
|
399
|
+
}
|
|
400
|
+
/** Recursively evaluate an ILogicNode tree */
|
|
401
|
+
function evaluateLogicNode(node, permissions) {
|
|
402
|
+
switch (node.type) {
|
|
403
|
+
case 'action':
|
|
404
|
+
return node.actionId ? hasPermission(node.actionId, permissions) : false;
|
|
405
|
+
case 'group':
|
|
406
|
+
if (!node.children || node.children.length === 0)
|
|
407
|
+
return false;
|
|
408
|
+
return node.operator === 'AND'
|
|
409
|
+
? node.children.every((child) => evaluateLogicNode(child, permissions))
|
|
410
|
+
: node.children.some((child) => evaluateLogicNode(child, permissions));
|
|
411
|
+
default:
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** Check if user has ANY of the specified permissions (OR logic) */
|
|
416
|
+
function hasAnyPermission(permissionCodes, permissions) {
|
|
417
|
+
if (!permissionCodes?.length)
|
|
418
|
+
return false;
|
|
419
|
+
return permissionCodes.some((code) => hasPermission(code, permissions));
|
|
420
|
+
}
|
|
421
|
+
/** Check if user has ALL of the specified permissions (AND logic) */
|
|
422
|
+
function hasAllPermissions(permissionCodes, permissions) {
|
|
423
|
+
if (!permissionCodes?.length)
|
|
424
|
+
return false;
|
|
425
|
+
return permissionCodes.every((code) => hasPermission(code, permissions));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Permission state management service.
|
|
430
|
+
* Provides signal-based storage and permission checking with wildcard support.
|
|
307
431
|
*/
|
|
308
432
|
class PermissionValidatorService {
|
|
309
|
-
// ==================== SIGNALS ====================
|
|
310
|
-
/**
|
|
311
|
-
* Private writable signal for permissions
|
|
312
|
-
*/
|
|
313
433
|
_permissions = signal([], ...(ngDevMode ? [{ debugName: "_permissions" }] : []));
|
|
314
|
-
/**
|
|
315
|
-
* Readonly permission signal for external consumers
|
|
316
|
-
*/
|
|
317
434
|
permissions = this._permissions.asReadonly();
|
|
318
|
-
/**
|
|
319
|
-
* Private writable signal for loaded state
|
|
320
|
-
*/
|
|
321
435
|
_isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "_isLoaded" }] : []));
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
* @param permissions - Array of permission codes
|
|
325
|
-
*/
|
|
436
|
+
isLoaded = this._isLoaded.asReadonly();
|
|
437
|
+
/** Set permissions (replaces existing) */
|
|
326
438
|
setPermissions(permissions) {
|
|
327
439
|
this._permissions.set(permissions);
|
|
328
440
|
this._isLoaded.set(true);
|
|
329
441
|
}
|
|
330
|
-
/**
|
|
331
|
-
* Clear all permissions
|
|
332
|
-
*/
|
|
442
|
+
/** Clear all permissions */
|
|
333
443
|
clearPermissions() {
|
|
334
444
|
this._permissions.set([]);
|
|
335
445
|
this._isLoaded.set(false);
|
|
336
446
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
* @param permissionCode - Required permission code
|
|
341
|
-
* @returns True if user has permission
|
|
342
|
-
*/
|
|
343
|
-
hasPermission(permissionCode) {
|
|
344
|
-
return this.permissions().includes(permissionCode);
|
|
447
|
+
/** Check if user has permission (supports wildcards: '*', 'module.*') */
|
|
448
|
+
hasPermission(code) {
|
|
449
|
+
return hasPermission(code, this._permissions());
|
|
345
450
|
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Check if permissions are loaded
|
|
349
|
-
* @returns True if permissions have been loaded
|
|
350
|
-
*/
|
|
451
|
+
/** @deprecated Use `isLoaded()` signal instead */
|
|
351
452
|
isPermissionsLoaded() {
|
|
352
453
|
return this._isLoaded();
|
|
353
454
|
}
|
|
@@ -356,9 +457,7 @@ class PermissionValidatorService {
|
|
|
356
457
|
}
|
|
357
458
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionValidatorService, decorators: [{
|
|
358
459
|
type: Injectable,
|
|
359
|
-
args: [{
|
|
360
|
-
providedIn: 'root',
|
|
361
|
-
}]
|
|
460
|
+
args: [{ providedIn: 'root' }]
|
|
362
461
|
}] });
|
|
363
462
|
|
|
364
463
|
class EditModeElementChangerDirective {
|
|
@@ -421,46 +520,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
421
520
|
type: Directive,
|
|
422
521
|
args: [{
|
|
423
522
|
selector: '[appEditModeElementChanger]',
|
|
424
|
-
standalone: true,
|
|
425
523
|
}]
|
|
426
524
|
}], ctorParameters: () => [], propDecorators: { isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }] } });
|
|
427
525
|
|
|
428
|
-
/** Evaluate permission logic (string or ILogicNode) against user permissions */
|
|
429
|
-
function evaluatePermission(logic, permissions) {
|
|
430
|
-
if (!logic)
|
|
431
|
-
return false;
|
|
432
|
-
if (typeof logic === 'string')
|
|
433
|
-
return permissions.includes(logic);
|
|
434
|
-
return evaluateLogicNode(logic, permissions);
|
|
435
|
-
}
|
|
436
|
-
/** Recursively evaluate an ILogicNode tree */
|
|
437
|
-
function evaluateLogicNode(node, permissions) {
|
|
438
|
-
switch (node.type) {
|
|
439
|
-
case 'action':
|
|
440
|
-
return node.actionId ? permissions.includes(node.actionId) : false;
|
|
441
|
-
case 'group':
|
|
442
|
-
if (!node.children || node.children.length === 0)
|
|
443
|
-
return false;
|
|
444
|
-
return node.operator === 'AND'
|
|
445
|
-
? node.children.every((child) => evaluateLogicNode(child, permissions))
|
|
446
|
-
: node.children.some((child) => evaluateLogicNode(child, permissions));
|
|
447
|
-
default:
|
|
448
|
-
return false;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
/** Check if user has ANY of the specified permissions (OR logic) */
|
|
452
|
-
function hasAnyPermission(permissionCodes, permissions) {
|
|
453
|
-
if (!permissionCodes?.length)
|
|
454
|
-
return false;
|
|
455
|
-
return permissionCodes.some((code) => permissions.includes(code));
|
|
456
|
-
}
|
|
457
|
-
/** Check if user has ALL of the specified permissions (AND logic) */
|
|
458
|
-
function hasAllPermissions(permissionCodes, permissions) {
|
|
459
|
-
if (!permissionCodes?.length)
|
|
460
|
-
return false;
|
|
461
|
-
return permissionCodes.every((code) => permissions.includes(code));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
526
|
/**
|
|
465
527
|
* HasPermission Directive
|
|
466
528
|
*
|
|
@@ -581,7 +643,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
581
643
|
type: Directive,
|
|
582
644
|
args: [{
|
|
583
645
|
selector: '[hasPermission]',
|
|
584
|
-
standalone: true,
|
|
585
646
|
}]
|
|
586
647
|
}], ctorParameters: () => [], propDecorators: { hasPermission: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPermission", required: false }] }] } });
|
|
587
648
|
|
|
@@ -610,7 +671,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
610
671
|
'[src]': 'imageSrc()',
|
|
611
672
|
'(error)': 'onError()',
|
|
612
673
|
},
|
|
613
|
-
standalone: true,
|
|
614
674
|
}]
|
|
615
675
|
}], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }] } });
|
|
616
676
|
|
|
@@ -648,18 +708,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
648
708
|
type: Directive,
|
|
649
709
|
args: [{
|
|
650
710
|
selector: '[appPreventDefault]',
|
|
651
|
-
|
|
711
|
+
host: {
|
|
712
|
+
'(click)': 'onClick($event)',
|
|
713
|
+
'(keydown)': 'onKeydown($event)',
|
|
714
|
+
'(keyup)': 'onKeyup($event)',
|
|
715
|
+
},
|
|
652
716
|
}]
|
|
653
|
-
}], 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"] }]
|
|
654
|
-
type: HostListener,
|
|
655
|
-
args: ['click', ['$event']]
|
|
656
|
-
}], onKeydown: [{
|
|
657
|
-
type: HostListener,
|
|
658
|
-
args: ['keydown', ['$event']]
|
|
659
|
-
}], onKeyup: [{
|
|
660
|
-
type: HostListener,
|
|
661
|
-
args: ['keyup', ['$event']]
|
|
662
|
-
}] } });
|
|
717
|
+
}], 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"] }] } });
|
|
663
718
|
|
|
664
719
|
class AngularModule {
|
|
665
720
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
@@ -668,6 +723,7 @@ class AngularModule {
|
|
|
668
723
|
ReactiveFormsModule,
|
|
669
724
|
RouterOutlet,
|
|
670
725
|
RouterLink,
|
|
726
|
+
RouterLinkActive,
|
|
671
727
|
IsEmptyImageDirective,
|
|
672
728
|
NgOptimizedImage,
|
|
673
729
|
NgComponentOutlet,
|
|
@@ -676,6 +732,7 @@ class AngularModule {
|
|
|
676
732
|
ReactiveFormsModule,
|
|
677
733
|
RouterOutlet,
|
|
678
734
|
RouterLink,
|
|
735
|
+
RouterLinkActive,
|
|
679
736
|
IsEmptyImageDirective,
|
|
680
737
|
NgOptimizedImage,
|
|
681
738
|
NgComponentOutlet,
|
|
@@ -695,6 +752,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
695
752
|
ReactiveFormsModule,
|
|
696
753
|
RouterOutlet,
|
|
697
754
|
RouterLink,
|
|
755
|
+
RouterLinkActive,
|
|
698
756
|
IsEmptyImageDirective,
|
|
699
757
|
NgOptimizedImage,
|
|
700
758
|
NgComponentOutlet,
|
|
@@ -707,6 +765,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
707
765
|
ReactiveFormsModule,
|
|
708
766
|
RouterOutlet,
|
|
709
767
|
RouterLink,
|
|
768
|
+
RouterLinkActive,
|
|
710
769
|
IsEmptyImageDirective,
|
|
711
770
|
NgOptimizedImage,
|
|
712
771
|
NgComponentOutlet,
|
|
@@ -883,7 +942,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
883
942
|
*/
|
|
884
943
|
class ApiResourceService {
|
|
885
944
|
baseUrl;
|
|
886
|
-
loaderService = inject(ApiLoaderService);
|
|
887
945
|
injector = inject(Injector);
|
|
888
946
|
http;
|
|
889
947
|
moduleApiName;
|
|
@@ -906,6 +964,12 @@ class ApiResourceService {
|
|
|
906
964
|
_listResource = null;
|
|
907
965
|
/** Whether the list resource has been initialized */
|
|
908
966
|
_resourceInitialized = false;
|
|
967
|
+
/**
|
|
968
|
+
* Signal to track resource initialization for computed signals.
|
|
969
|
+
* This allows computed signals to re-evaluate when the resource is created.
|
|
970
|
+
* Without this, computed signals would not detect when _listResource changes from null.
|
|
971
|
+
*/
|
|
972
|
+
_resourceInitSignal = signal(false, ...(ngDevMode ? [{ debugName: "_resourceInitSignal" }] : []));
|
|
909
973
|
/** Get or create the list resource (lazy initialization) */
|
|
910
974
|
get listResource() {
|
|
911
975
|
if (!this._listResource) {
|
|
@@ -932,20 +996,50 @@ class ApiResourceService {
|
|
|
932
996
|
return this.fetchAllAsync(search, filter);
|
|
933
997
|
} });
|
|
934
998
|
});
|
|
999
|
+
// Signal that resource is now initialized - triggers computed re-evaluation
|
|
1000
|
+
this._resourceInitSignal.set(true);
|
|
935
1001
|
}
|
|
936
1002
|
// ==========================================================================
|
|
937
1003
|
// Computed State Accessors
|
|
938
1004
|
// ==========================================================================
|
|
939
|
-
/**
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
/**
|
|
1005
|
+
/**
|
|
1006
|
+
* Whether data is currently loading.
|
|
1007
|
+
* Tracks _resourceInitSignal to re-evaluate when resource is created.
|
|
1008
|
+
*/
|
|
1009
|
+
isLoading = computed(() => {
|
|
1010
|
+
this._resourceInitSignal(); // Track initialization
|
|
1011
|
+
return this._listResource?.isLoading() ?? false;
|
|
1012
|
+
}, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
1013
|
+
/**
|
|
1014
|
+
* List data array.
|
|
1015
|
+
* Tracks _resourceInitSignal to re-evaluate when resource is created.
|
|
1016
|
+
*/
|
|
1017
|
+
data = computed(() => {
|
|
1018
|
+
this._resourceInitSignal(); // Track initialization
|
|
1019
|
+
return this._listResource?.value()?.data ?? [];
|
|
1020
|
+
}, ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
1021
|
+
/**
|
|
1022
|
+
* Total count of items.
|
|
1023
|
+
* Tracks _resourceInitSignal to re-evaluate when resource is created.
|
|
1024
|
+
*/
|
|
1025
|
+
total = computed(() => {
|
|
1026
|
+
this._resourceInitSignal(); // Track initialization
|
|
1027
|
+
return this._listResource?.value()?.meta?.total ?? 0;
|
|
1028
|
+
}, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1029
|
+
/**
|
|
1030
|
+
* Pagination metadata.
|
|
1031
|
+
* Tracks _resourceInitSignal to re-evaluate when resource is created.
|
|
1032
|
+
*/
|
|
1033
|
+
pageInfo = computed(() => {
|
|
1034
|
+
this._resourceInitSignal(); // Track initialization
|
|
1035
|
+
return this._listResource?.value()?.meta;
|
|
1036
|
+
}, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
|
|
1037
|
+
/**
|
|
1038
|
+
* Whether there are more pages.
|
|
1039
|
+
* Tracks _resourceInitSignal to re-evaluate when resource is created.
|
|
1040
|
+
*/
|
|
948
1041
|
hasMore = computed(() => {
|
|
1042
|
+
this._resourceInitSignal(); // Track initialization
|
|
949
1043
|
const meta = this._listResource?.value()?.meta;
|
|
950
1044
|
if (!meta)
|
|
951
1045
|
return false;
|
|
@@ -1120,38 +1214,6 @@ class ApiResourceService {
|
|
|
1120
1214
|
}
|
|
1121
1215
|
}
|
|
1122
1216
|
|
|
1123
|
-
class IconComponent {
|
|
1124
|
-
icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
1125
|
-
iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
|
|
1126
|
-
IconTypeEnum = IconTypeEnum; // Needed for template reference
|
|
1127
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1128
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", 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: `
|
|
1129
|
-
@if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
|
|
1130
|
-
<i [ngClass]="icon()"></i>
|
|
1131
|
-
}@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
|
|
1132
|
-
<img [alt]="icon()" [src]="icon()" />
|
|
1133
|
-
}@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
|
|
1134
|
-
{{ icon() }}
|
|
1135
|
-
}@else{ I } }
|
|
1136
|
-
`, 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"] }] });
|
|
1137
|
-
}
|
|
1138
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
|
|
1139
|
-
type: Component,
|
|
1140
|
-
args: [{
|
|
1141
|
-
selector: 'lib-icon',
|
|
1142
|
-
imports: [AngularModule],
|
|
1143
|
-
template: `
|
|
1144
|
-
@if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
|
|
1145
|
-
<i [ngClass]="icon()"></i>
|
|
1146
|
-
}@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
|
|
1147
|
-
<img [alt]="icon()" [src]="icon()" />
|
|
1148
|
-
}@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
|
|
1149
|
-
{{ icon() }}
|
|
1150
|
-
}@else{ I } }
|
|
1151
|
-
`,
|
|
1152
|
-
}]
|
|
1153
|
-
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
|
|
1154
|
-
|
|
1155
1217
|
/**
|
|
1156
1218
|
* Base class for form controls that support ALL Angular form patterns:
|
|
1157
1219
|
*
|
|
@@ -1275,6 +1337,77 @@ function provideValueAccessor(component) {
|
|
|
1275
1337
|
};
|
|
1276
1338
|
}
|
|
1277
1339
|
|
|
1340
|
+
class IconComponent {
|
|
1341
|
+
icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
1342
|
+
iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
|
|
1343
|
+
IconTypeEnum = IconTypeEnum; // Needed for template reference
|
|
1344
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1345
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", 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: `
|
|
1346
|
+
@if (icon()) {
|
|
1347
|
+
@if (iconType() === IconTypeEnum.PRIMENG_ICON) {
|
|
1348
|
+
<i [class]="icon()"></i>
|
|
1349
|
+
} @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
|
|
1350
|
+
<img [alt]="icon()" [src]="icon()" />
|
|
1351
|
+
} @else {
|
|
1352
|
+
<i class="pi pi-question"></i>
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1356
|
+
}
|
|
1357
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
|
|
1358
|
+
type: Component,
|
|
1359
|
+
args: [{
|
|
1360
|
+
selector: 'lib-icon',
|
|
1361
|
+
imports: [AngularModule],
|
|
1362
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1363
|
+
template: `
|
|
1364
|
+
@if (icon()) {
|
|
1365
|
+
@if (iconType() === IconTypeEnum.PRIMENG_ICON) {
|
|
1366
|
+
<i [class]="icon()"></i>
|
|
1367
|
+
} @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
|
|
1368
|
+
<img [alt]="icon()" [src]="icon()" />
|
|
1369
|
+
} @else {
|
|
1370
|
+
<i class="pi pi-question"></i>
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
`,
|
|
1374
|
+
}]
|
|
1375
|
+
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Check if scroll has reached near bottom and calculate next page if available.
|
|
1379
|
+
* Returns next pagination if should load more, null otherwise.
|
|
1380
|
+
*
|
|
1381
|
+
* @example
|
|
1382
|
+
* ```typescript
|
|
1383
|
+
* onScroll(event: Event): void {
|
|
1384
|
+
* const nextPagination = checkScrollPagination(event, {
|
|
1385
|
+
* pagination: this.pagination(),
|
|
1386
|
+
* total: this.total(),
|
|
1387
|
+
* isLoading: this.isLoading(),
|
|
1388
|
+
* });
|
|
1389
|
+
* if (nextPagination) {
|
|
1390
|
+
* this.onPagination.emit(nextPagination);
|
|
1391
|
+
* }
|
|
1392
|
+
* }
|
|
1393
|
+
* ```
|
|
1394
|
+
*/
|
|
1395
|
+
function checkScrollPagination(event, config) {
|
|
1396
|
+
const el = event.target;
|
|
1397
|
+
if (!(el instanceof HTMLElement))
|
|
1398
|
+
return null;
|
|
1399
|
+
const threshold = config.threshold ?? 50;
|
|
1400
|
+
const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
|
|
1401
|
+
if (!nearBottom || config.isLoading)
|
|
1402
|
+
return null;
|
|
1403
|
+
const { pagination, total } = config;
|
|
1404
|
+
const nextPage = pagination.currentPage + 1;
|
|
1405
|
+
const hasMore = nextPage * pagination.pageSize < (total ?? 0);
|
|
1406
|
+
if (!hasMore)
|
|
1407
|
+
return null;
|
|
1408
|
+
return { ...pagination, currentPage: nextPage };
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1278
1411
|
/**
|
|
1279
1412
|
* Lazy-loading multi-select component with search, pagination, and select-all.
|
|
1280
1413
|
*
|
|
@@ -1284,6 +1417,10 @@ function provideValueAccessor(component) {
|
|
|
1284
1417
|
* - Signal forms: `[formField]="formTree.field"`
|
|
1285
1418
|
*/
|
|
1286
1419
|
class LazyMultiSelectComponent extends BaseFormControl {
|
|
1420
|
+
destroyRef = inject(DestroyRef);
|
|
1421
|
+
onDocumentClickBound = this.handleDocumentClick.bind(this);
|
|
1422
|
+
// View references
|
|
1423
|
+
pSelectRef = viewChild.required('pSelect');
|
|
1287
1424
|
// Inputs
|
|
1288
1425
|
placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1289
1426
|
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
@@ -1291,14 +1428,15 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1291
1428
|
total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1292
1429
|
pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1293
1430
|
selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
|
|
1294
|
-
|
|
1431
|
+
// Two-way bound value
|
|
1295
1432
|
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1296
1433
|
// Outputs
|
|
1297
1434
|
onSearch = output();
|
|
1298
1435
|
onPagination = output();
|
|
1299
|
-
// UI
|
|
1436
|
+
// UI state
|
|
1300
1437
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1301
|
-
|
|
1438
|
+
openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
|
|
1439
|
+
// Computed values
|
|
1302
1440
|
selectedValueDisplay = computed(() => {
|
|
1303
1441
|
const selectedValues = this.value() ?? [];
|
|
1304
1442
|
if (selectedValues.length === 0)
|
|
@@ -1307,44 +1445,59 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1307
1445
|
return `${selectedValues.length} Items Selected`;
|
|
1308
1446
|
}
|
|
1309
1447
|
return this.selectDataList()
|
|
1310
|
-
.filter(item => selectedValues.includes(item.value))
|
|
1311
|
-
.map(item => item.label)
|
|
1448
|
+
.filter((item) => selectedValues.includes(item.value))
|
|
1449
|
+
.map((item) => item.label)
|
|
1312
1450
|
.join(', ');
|
|
1313
1451
|
}, ...(ngDevMode ? [{ debugName: "selectedValueDisplay" }] : []));
|
|
1314
|
-
/** Computed: Whether all items are selected (replaces isSelectAll signal) */
|
|
1315
1452
|
isSelectAll = computed(() => {
|
|
1316
1453
|
const selectedValues = this.value() ?? [];
|
|
1317
|
-
const allValues = this.selectDataList().map(item => item.value);
|
|
1454
|
+
const allValues = this.selectDataList().map((item) => item.value);
|
|
1318
1455
|
if (selectedValues.length === 0 || allValues.length === 0)
|
|
1319
1456
|
return false;
|
|
1320
|
-
return allValues.every(val => selectedValues.includes(val));
|
|
1457
|
+
return allValues.every((val) => selectedValues.includes(val));
|
|
1321
1458
|
}, ...(ngDevMode ? [{ debugName: "isSelectAll" }] : []));
|
|
1322
1459
|
constructor() {
|
|
1323
1460
|
super();
|
|
1324
1461
|
this.initializeFormControl();
|
|
1325
|
-
// Search debounce effect
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1462
|
+
// Search debounce using effect
|
|
1463
|
+
let debounceTimeout = null;
|
|
1464
|
+
let previousValue = this.searchTerm();
|
|
1465
|
+
effect((onCleanup) => {
|
|
1466
|
+
const currentValue = this.searchTerm();
|
|
1467
|
+
// Skip unchanged values
|
|
1468
|
+
if (currentValue === previousValue)
|
|
1469
|
+
return;
|
|
1470
|
+
previousValue = currentValue;
|
|
1471
|
+
// Clear existing timeout
|
|
1472
|
+
if (debounceTimeout)
|
|
1473
|
+
clearTimeout(debounceTimeout);
|
|
1474
|
+
// Debounced emit
|
|
1475
|
+
debounceTimeout = setTimeout(() => {
|
|
1476
|
+
this.onSearch.emit(currentValue);
|
|
1477
|
+
}, 500);
|
|
1478
|
+
onCleanup(() => {
|
|
1479
|
+
if (debounceTimeout)
|
|
1480
|
+
clearTimeout(debounceTimeout);
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
// Document click listener for closing dropdown
|
|
1484
|
+
afterNextRender(() => {
|
|
1485
|
+
document.addEventListener('click', this.onDocumentClickBound);
|
|
1486
|
+
});
|
|
1487
|
+
this.destroyRef.onDestroy(() => {
|
|
1488
|
+
document.removeEventListener('click', this.onDocumentClickBound);
|
|
1330
1489
|
});
|
|
1331
1490
|
}
|
|
1332
|
-
onScrollBound = this.onScroll.bind(this);
|
|
1333
|
-
multiScrollContainer = viewChild.required('multiScrollContainer');
|
|
1334
1491
|
onScroll(event) {
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
this.onPagination.emit({ ...pagination, currentPage: nextPage });
|
|
1343
|
-
}
|
|
1492
|
+
const nextPagination = checkScrollPagination(event, {
|
|
1493
|
+
pagination: this.pagination(),
|
|
1494
|
+
total: this.total(),
|
|
1495
|
+
isLoading: this.isLoading(),
|
|
1496
|
+
});
|
|
1497
|
+
if (nextPagination) {
|
|
1498
|
+
this.onPagination.emit(nextPagination);
|
|
1344
1499
|
}
|
|
1345
1500
|
}
|
|
1346
|
-
pSelectRef = viewChild.required('pSelect');
|
|
1347
|
-
openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
|
|
1348
1501
|
onSelectClick(event) {
|
|
1349
1502
|
if (this.disabled())
|
|
1350
1503
|
return;
|
|
@@ -1363,7 +1516,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1363
1516
|
}
|
|
1364
1517
|
}
|
|
1365
1518
|
isSelected(data) {
|
|
1366
|
-
return this.value()?.includes(data.value);
|
|
1519
|
+
return this.value()?.includes(data.value) ?? false;
|
|
1367
1520
|
}
|
|
1368
1521
|
key(option) {
|
|
1369
1522
|
return option.value;
|
|
@@ -1376,8 +1529,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1376
1529
|
}
|
|
1377
1530
|
}
|
|
1378
1531
|
else {
|
|
1379
|
-
|
|
1380
|
-
this.value.set(updated);
|
|
1532
|
+
this.value.set(previousValue.filter((v) => v !== option.value));
|
|
1381
1533
|
}
|
|
1382
1534
|
}
|
|
1383
1535
|
changeSelectAll(event) {
|
|
@@ -1393,15 +1545,12 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1393
1545
|
this.value.set([]);
|
|
1394
1546
|
}
|
|
1395
1547
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1396
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", 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" },
|
|
1548
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", 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" }, providers: [provideValueAccessor(LazyMultiSelectComponent)], viewQueries: [{ 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)\" [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\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\">\n <i class=\"pi pi-times\"></i>\n </span>\n\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 class=\"p-multiselect-dropdown-icon p-icon\" aria-hidden=\"true\">\n <path 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\" fill=\"currentColor\" />\n </svg>\n </span>\n </div>\n\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\n binary=\"true\"\n [ngModel]=\"isSelectAll()\"\n [disabled]=\"disabled()\"\n (onChange)=\"changeSelectAll($event)\"\n />\n <input\n type=\"text\"\n pInputText\n class=\"w-full\"\n placeholder=\"Search...\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\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)) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelected(data)\"\n [disabled]=\"disabled()\"\n (onChange)=\"selectValue($event, data)\"\n />\n <span>{{ data.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{position:absolute;top:100%;left:0;right:0;z-index:var(--p-overlay-select-zindex, 1004);margin-top:var(--p-select-overlay-offset, 2px);background:var(--p-select-overlay-background, var(--p-surface-0));border:1px solid var(--p-select-overlay-border-color, var(--p-surface-200));border-radius:var(--p-select-overlay-border-radius, var(--p-border-radius));box-shadow:var(--p-select-overlay-shadow, var(--p-overlay-shadow))}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-200);background:var(--p-surface-50)}:host-context(.p-dark) .p-select-header,.dark .p-select-header{border-color:var(--p-surface-700);background:var(--p-surface-800)}.p-select-list-container{max-height:10rem;overflow-y:auto}@media(min-width:640px){.p-select-list-container{max-height:12.5rem}}.p-select-list{margin:0;padding:.25rem 0;list-style:none}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease}.p-select-option:hover{background:var(--p-select-option-focus-background, var(--p-surface-100));color:var(--p-select-option-focus-color, var(--p-text-color))}:host-context(.p-dark) .p-select-option:hover,.dark .p-select-option:hover{background:var(--p-surface-700)}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background, var(--p-primary-50));color:var(--p-select-option-selected-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected,.dark .p-select-option.p-select-option-selected{background:var(--p-primary-900)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background, var(--p-primary-100));color:var(--p-select-option-selected-focus-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected:hover,.dark .p-select-option.p-select-option-selected:hover{background:var(--p-primary-800)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-color-secondary);cursor:pointer;transition:color .2s ease}.p-select-clear-icon:hover{color:var(--p-text-color)}\n"], dependencies: [{ kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i1.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: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1397
1549
|
}
|
|
1398
1550
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
|
|
1399
1551
|
type: Component,
|
|
1400
|
-
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)\"
|
|
1401
|
-
}], 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"] }]
|
|
1402
|
-
type: HostListener,
|
|
1403
|
-
args: ['document:click', ['$event']]
|
|
1404
|
-
}] } });
|
|
1552
|
+
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)\" [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\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\">\n <i class=\"pi pi-times\"></i>\n </span>\n\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 class=\"p-multiselect-dropdown-icon p-icon\" aria-hidden=\"true\">\n <path 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\" fill=\"currentColor\" />\n </svg>\n </span>\n </div>\n\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\n binary=\"true\"\n [ngModel]=\"isSelectAll()\"\n [disabled]=\"disabled()\"\n (onChange)=\"changeSelectAll($event)\"\n />\n <input\n type=\"text\"\n pInputText\n class=\"w-full\"\n placeholder=\"Search...\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\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)) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelected(data)\"\n [disabled]=\"disabled()\"\n (onChange)=\"selectValue($event, data)\"\n />\n <span>{{ data.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{position:absolute;top:100%;left:0;right:0;z-index:var(--p-overlay-select-zindex, 1004);margin-top:var(--p-select-overlay-offset, 2px);background:var(--p-select-overlay-background, var(--p-surface-0));border:1px solid var(--p-select-overlay-border-color, var(--p-surface-200));border-radius:var(--p-select-overlay-border-radius, var(--p-border-radius));box-shadow:var(--p-select-overlay-shadow, var(--p-overlay-shadow))}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-200);background:var(--p-surface-50)}:host-context(.p-dark) .p-select-header,.dark .p-select-header{border-color:var(--p-surface-700);background:var(--p-surface-800)}.p-select-list-container{max-height:10rem;overflow-y:auto}@media(min-width:640px){.p-select-list-container{max-height:12.5rem}}.p-select-list{margin:0;padding:.25rem 0;list-style:none}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease}.p-select-option:hover{background:var(--p-select-option-focus-background, var(--p-surface-100));color:var(--p-select-option-focus-color, var(--p-text-color))}:host-context(.p-dark) .p-select-option:hover,.dark .p-select-option:hover{background:var(--p-surface-700)}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background, var(--p-primary-50));color:var(--p-select-option-selected-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected,.dark .p-select-option.p-select-option-selected{background:var(--p-primary-900)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background, var(--p-primary-100));color:var(--p-select-option-selected-focus-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected:hover,.dark .p-select-option.p-select-option-selected:hover{background:var(--p-primary-800)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-color-secondary);cursor:pointer;transition:color .2s ease}.p-select-clear-icon:hover{color:var(--p-text-color)}\n"] }]
|
|
1553
|
+
}], ctorParameters: () => [], propDecorators: { pSelectRef: [{ type: i0.ViewChild, args: ['pSelect', { isSignal: true }] }], 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"] }] } });
|
|
1405
1554
|
|
|
1406
1555
|
/**
|
|
1407
1556
|
* Lazy-loading single select component with search and pagination.
|
|
@@ -1412,6 +1561,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1412
1561
|
* - Signal forms: `[formField]="formTree.field"`
|
|
1413
1562
|
*/
|
|
1414
1563
|
class LazySelectComponent extends BaseFormControl {
|
|
1564
|
+
destroyRef = inject(DestroyRef);
|
|
1565
|
+
onScrollBound = this.onScroll.bind(this);
|
|
1566
|
+
scrollTargetEl = null;
|
|
1567
|
+
// View references
|
|
1568
|
+
scrollContainer = viewChild.required('scrollContainer');
|
|
1415
1569
|
// Inputs
|
|
1416
1570
|
placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1417
1571
|
optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
|
|
@@ -1421,77 +1575,85 @@ class LazySelectComponent extends BaseFormControl {
|
|
|
1421
1575
|
total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1422
1576
|
pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1423
1577
|
selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
|
|
1424
|
-
|
|
1578
|
+
// Two-way bound value
|
|
1425
1579
|
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1426
1580
|
// Outputs
|
|
1427
1581
|
onSearch = output();
|
|
1428
1582
|
onPagination = output();
|
|
1429
|
-
// UI
|
|
1583
|
+
// UI state
|
|
1430
1584
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1431
|
-
|
|
1585
|
+
isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
|
|
1432
1586
|
constructor() {
|
|
1433
1587
|
super();
|
|
1434
1588
|
this.initializeFormControl();
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1589
|
+
// Search debounce using effect
|
|
1590
|
+
let debounceTimeout = null;
|
|
1591
|
+
let previousValue = this.searchTerm();
|
|
1592
|
+
effect((onCleanup) => {
|
|
1593
|
+
const currentValue = this.searchTerm();
|
|
1594
|
+
// Skip unchanged values
|
|
1595
|
+
if (currentValue === previousValue)
|
|
1596
|
+
return;
|
|
1597
|
+
previousValue = currentValue;
|
|
1598
|
+
// Clear existing timeout
|
|
1599
|
+
if (debounceTimeout)
|
|
1600
|
+
clearTimeout(debounceTimeout);
|
|
1601
|
+
// Debounced emit
|
|
1602
|
+
debounceTimeout = setTimeout(() => {
|
|
1603
|
+
this.onSearch.emit(currentValue);
|
|
1604
|
+
}, 500);
|
|
1605
|
+
onCleanup(() => {
|
|
1606
|
+
if (debounceTimeout)
|
|
1607
|
+
clearTimeout(debounceTimeout);
|
|
1608
|
+
});
|
|
1609
|
+
});
|
|
1610
|
+
// Cleanup scroll listener on destroy
|
|
1611
|
+
this.destroyRef.onDestroy(() => {
|
|
1612
|
+
if (this.scrollTargetEl) {
|
|
1613
|
+
this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
|
|
1614
|
+
}
|
|
1439
1615
|
});
|
|
1440
1616
|
}
|
|
1441
|
-
// Signal to toggle panel
|
|
1442
|
-
isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
|
|
1443
|
-
scrollTargetEl = null;
|
|
1444
|
-
onScrollBound = this.onScroll.bind(this);
|
|
1445
|
-
scrollContainer = viewChild.required('scrollContainer');
|
|
1446
1617
|
onScroll(event) {
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
this.onPagination.emit({ ...pagination, currentPage: nextPage });
|
|
1455
|
-
}
|
|
1618
|
+
const nextPagination = checkScrollPagination(event, {
|
|
1619
|
+
pagination: this.pagination(),
|
|
1620
|
+
total: this.total(),
|
|
1621
|
+
isLoading: this.isLoading(),
|
|
1622
|
+
});
|
|
1623
|
+
if (nextPagination) {
|
|
1624
|
+
this.onPagination.emit(nextPagination);
|
|
1456
1625
|
}
|
|
1457
1626
|
}
|
|
1458
|
-
// Toggle panel and manage scroll event
|
|
1459
1627
|
showPanel() {
|
|
1460
1628
|
if (this.disabled())
|
|
1461
1629
|
return;
|
|
1462
|
-
this.isPanelShow.update(prev => !prev);
|
|
1630
|
+
this.isPanelShow.update((prev) => !prev);
|
|
1463
1631
|
const isNowVisible = this.isPanelShow();
|
|
1464
1632
|
if (isNowVisible) {
|
|
1465
|
-
|
|
1633
|
+
afterNextRender(() => {
|
|
1466
1634
|
const containerEl = this.scrollContainer().nativeElement;
|
|
1467
1635
|
const target = containerEl.querySelector('.p-select-list-container');
|
|
1468
1636
|
if (target) {
|
|
1469
1637
|
target.addEventListener('scroll', this.onScrollBound);
|
|
1470
1638
|
this.scrollTargetEl = target;
|
|
1471
1639
|
}
|
|
1472
|
-
},
|
|
1640
|
+
}, { injector: this.injector });
|
|
1473
1641
|
}
|
|
1474
|
-
else {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
this.scrollTargetEl = null;
|
|
1478
|
-
}
|
|
1642
|
+
else if (this.scrollTargetEl) {
|
|
1643
|
+
this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
|
|
1644
|
+
this.scrollTargetEl = null;
|
|
1479
1645
|
}
|
|
1480
1646
|
}
|
|
1481
1647
|
onBlur() {
|
|
1482
1648
|
this.markAsTouched();
|
|
1483
1649
|
}
|
|
1484
1650
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1485
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", 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
|
|
1651
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", 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 class=\"w-full\"\n [options]=\"selectDataList()\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n [(ngModel)]=\"value\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\"\n >\n <ng-template #filter let-filter>\n <input\n pInputText\n class=\"w-full\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template #item let-item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: AngularModule }, { 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: PrimeModule }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i3$1.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 });
|
|
1486
1652
|
}
|
|
1487
1653
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, decorators: [{
|
|
1488
1654
|
type: Component,
|
|
1489
|
-
args: [{ selector: 'lib-lazy-select', imports: [
|
|
1490
|
-
|
|
1491
|
-
PrimeModule,
|
|
1492
|
-
EditModeElementChangerDirective
|
|
1493
|
-
], 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" }]
|
|
1494
|
-
}], 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 }] }] } });
|
|
1655
|
+
args: [{ selector: 'lib-lazy-select', imports: [AngularModule, PrimeModule, EditModeElementChangerDirective], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazySelectComponent)], template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n class=\"w-full\"\n [options]=\"selectDataList()\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n [(ngModel)]=\"value\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\"\n >\n <ng-template #filter let-filter>\n <input\n pInputText\n class=\"w-full\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template #item let-item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n" }]
|
|
1656
|
+
}], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], 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"] }] } });
|
|
1495
1657
|
|
|
1496
1658
|
/**
|
|
1497
1659
|
* Injection Tokens for Provider Interfaces
|
|
@@ -1589,56 +1751,33 @@ const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
|
|
|
1589
1751
|
*/
|
|
1590
1752
|
const USER_LIST_PROVIDER = new InjectionToken('USER_LIST_PROVIDER');
|
|
1591
1753
|
|
|
1592
|
-
const DEFAULT_PAGE_SIZE$
|
|
1754
|
+
const DEFAULT_PAGE_SIZE$1 = 20;
|
|
1593
1755
|
/**
|
|
1594
|
-
*
|
|
1756
|
+
* Base class for user selection components.
|
|
1757
|
+
* Provides shared user loading, pagination, and search functionality.
|
|
1595
1758
|
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
* Features:
|
|
1599
|
-
* - Search with debouncing (handled by lazy-select)
|
|
1600
|
-
* - Infinite scroll pagination
|
|
1601
|
-
* - Filter active users by default (configurable)
|
|
1602
|
-
* - Supports additional filters via `additionalFilters` input
|
|
1603
|
-
*
|
|
1604
|
-
* @example
|
|
1605
|
-
* ```html
|
|
1606
|
-
* <!-- Simple usage - uses USER_PROVIDER internally -->
|
|
1607
|
-
* <lib-user-select
|
|
1608
|
-
* [(value)]="selectedUserId"
|
|
1609
|
-
* [isEditMode]="true"
|
|
1610
|
-
* />
|
|
1611
|
-
*
|
|
1612
|
-
* <!-- With custom loadUsers function -->
|
|
1613
|
-
* <lib-user-select
|
|
1614
|
-
* [(value)]="selectedUserId"
|
|
1615
|
-
* [isEditMode]="true"
|
|
1616
|
-
* [loadUsers]="customLoadUsers"
|
|
1617
|
-
* />
|
|
1618
|
-
* ```
|
|
1759
|
+
* Subclasses must implement:
|
|
1760
|
+
* - `setupValueEffect()` to track value changes and emit selection events
|
|
1619
1761
|
*/
|
|
1620
|
-
class
|
|
1762
|
+
class BaseUserSelectComponent {
|
|
1621
1763
|
destroyRef = inject(DestroyRef);
|
|
1764
|
+
injector = inject(Injector);
|
|
1622
1765
|
userProvider = inject(USER_PROVIDER);
|
|
1623
1766
|
abortController = null;
|
|
1624
|
-
// Optional: custom function to load users (uses USER_PROVIDER if not provided)
|
|
1625
|
-
loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
|
|
1626
1767
|
// Inputs
|
|
1768
|
+
loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
|
|
1627
1769
|
placeHolder = input('Select User', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1628
1770
|
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
1629
1771
|
filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
|
|
1630
1772
|
additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
|
|
1631
|
-
pageSize = input(DEFAULT_PAGE_SIZE$
|
|
1632
|
-
// Two-way bound value
|
|
1633
|
-
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1773
|
+
pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
|
|
1634
1774
|
// Outputs
|
|
1635
|
-
userSelected = output();
|
|
1636
1775
|
onError = output();
|
|
1637
1776
|
// Internal state
|
|
1638
1777
|
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
1639
1778
|
users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
|
|
1640
1779
|
total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1641
|
-
pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$
|
|
1780
|
+
pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1642
1781
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1643
1782
|
// Computed dropdown data
|
|
1644
1783
|
dropdownUsers = computed(() => this.users().map((user) => ({
|
|
@@ -1661,20 +1800,8 @@ class UserSelectComponent {
|
|
|
1661
1800
|
afterNextRender(() => {
|
|
1662
1801
|
this.fetchUsers();
|
|
1663
1802
|
});
|
|
1664
|
-
//
|
|
1665
|
-
|
|
1666
|
-
const selectedId = this.value();
|
|
1667
|
-
const users = this.users();
|
|
1668
|
-
untracked(() => {
|
|
1669
|
-
if (selectedId) {
|
|
1670
|
-
const user = users.find((u) => u.id === selectedId);
|
|
1671
|
-
this.userSelected.emit(user ?? null);
|
|
1672
|
-
}
|
|
1673
|
-
else {
|
|
1674
|
-
this.userSelected.emit(null);
|
|
1675
|
-
}
|
|
1676
|
-
});
|
|
1677
|
-
});
|
|
1803
|
+
// Setup value change tracking (implemented by subclass)
|
|
1804
|
+
this.setupValueEffect();
|
|
1678
1805
|
}
|
|
1679
1806
|
handleSearch(search) {
|
|
1680
1807
|
this.searchTerm.set(search);
|
|
@@ -1686,6 +1813,12 @@ class UserSelectComponent {
|
|
|
1686
1813
|
this.pagination.set(pagination);
|
|
1687
1814
|
this.fetchUsers(true);
|
|
1688
1815
|
}
|
|
1816
|
+
/** Reload users (useful when filters change externally) */
|
|
1817
|
+
reload() {
|
|
1818
|
+
this.pagination.update((p) => ({ ...p, currentPage: 0 }));
|
|
1819
|
+
this.users.set([]);
|
|
1820
|
+
this.fetchUsers();
|
|
1821
|
+
}
|
|
1689
1822
|
async fetchUsers(append = false) {
|
|
1690
1823
|
if (this.isLoading())
|
|
1691
1824
|
return;
|
|
@@ -1743,14 +1876,63 @@ class UserSelectComponent {
|
|
|
1743
1876
|
})),
|
|
1744
1877
|
})));
|
|
1745
1878
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1879
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseUserSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1880
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: BaseUserSelectComponent, isStandalone: true, inputs: { loadUsers: { classPropertyName: "loadUsers", publicName: "loadUsers", isSignal: true, isRequired: false, transformFunction: null }, placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, filterActive: { classPropertyName: "filterActive", publicName: "filterActive", isSignal: true, isRequired: false, transformFunction: null }, additionalFilters: { classPropertyName: "additionalFilters", publicName: "additionalFilters", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onError: "onError" }, ngImport: i0 });
|
|
1881
|
+
}
|
|
1882
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseUserSelectComponent, decorators: [{
|
|
1883
|
+
type: Directive
|
|
1884
|
+
}], ctorParameters: () => [], propDecorators: { loadUsers: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadUsers", required: false }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], filterActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterActive", required: false }] }], additionalFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "additionalFilters", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* User Select Component - Single user selection with lazy loading.
|
|
1888
|
+
*
|
|
1889
|
+
* Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
|
|
1890
|
+
*
|
|
1891
|
+
* Features:
|
|
1892
|
+
* - Search with debouncing (handled by lazy-select)
|
|
1893
|
+
* - Infinite scroll pagination
|
|
1894
|
+
* - Filter active users by default (configurable)
|
|
1895
|
+
* - Supports additional filters via `additionalFilters` input
|
|
1896
|
+
*
|
|
1897
|
+
* @example
|
|
1898
|
+
* ```html
|
|
1899
|
+
* <!-- Simple usage - uses USER_PROVIDER internally -->
|
|
1900
|
+
* <lib-user-select
|
|
1901
|
+
* [(value)]="selectedUserId"
|
|
1902
|
+
* [isEditMode]="true"
|
|
1903
|
+
* />
|
|
1904
|
+
*
|
|
1905
|
+
* <!-- With custom loadUsers function -->
|
|
1906
|
+
* <lib-user-select
|
|
1907
|
+
* [(value)]="selectedUserId"
|
|
1908
|
+
* [isEditMode]="true"
|
|
1909
|
+
* [loadUsers]="customLoadUsers"
|
|
1910
|
+
* />
|
|
1911
|
+
* ```
|
|
1912
|
+
*/
|
|
1913
|
+
class UserSelectComponent extends BaseUserSelectComponent {
|
|
1914
|
+
// Two-way bound value
|
|
1915
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1916
|
+
// Outputs
|
|
1917
|
+
userSelected = output();
|
|
1918
|
+
setupValueEffect() {
|
|
1919
|
+
// Emit selected user when value changes
|
|
1920
|
+
effect(() => {
|
|
1921
|
+
const selectedId = this.value();
|
|
1922
|
+
const users = this.users();
|
|
1923
|
+
untracked(() => {
|
|
1924
|
+
if (selectedId) {
|
|
1925
|
+
const user = users.find((u) => u.id === selectedId);
|
|
1926
|
+
this.userSelected.emit(user ?? null);
|
|
1927
|
+
}
|
|
1928
|
+
else {
|
|
1929
|
+
this.userSelected.emit(null);
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1751
1933
|
}
|
|
1752
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserSelectComponent, deps:
|
|
1753
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserSelectComponent, isStandalone: true, selector: "lib-user-select", inputs: {
|
|
1934
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserSelectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1935
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserSelectComponent, isStandalone: true, selector: "lib-user-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", userSelected: "userSelected" }, usesInheritance: true, ngImport: i0, template: `
|
|
1754
1936
|
<lib-lazy-select
|
|
1755
1937
|
[(value)]="value"
|
|
1756
1938
|
[placeHolder]="placeHolder()"
|
|
@@ -1770,7 +1952,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1770
1952
|
type: Component,
|
|
1771
1953
|
args: [{
|
|
1772
1954
|
selector: 'lib-user-select',
|
|
1773
|
-
standalone: true,
|
|
1774
1955
|
imports: [AngularModule, PrimeModule, LazySelectComponent],
|
|
1775
1956
|
template: `
|
|
1776
1957
|
<lib-lazy-select
|
|
@@ -1789,9 +1970,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1789
1970
|
`,
|
|
1790
1971
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1791
1972
|
}]
|
|
1792
|
-
}],
|
|
1973
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], userSelected: [{ type: i0.Output, args: ["userSelected"] }] } });
|
|
1793
1974
|
|
|
1794
|
-
const DEFAULT_PAGE_SIZE$1 = 20;
|
|
1795
1975
|
/**
|
|
1796
1976
|
* User Multi-Select Component - Multiple user selection with lazy loading.
|
|
1797
1977
|
*
|
|
@@ -1820,50 +2000,12 @@ const DEFAULT_PAGE_SIZE$1 = 20;
|
|
|
1820
2000
|
* />
|
|
1821
2001
|
* ```
|
|
1822
2002
|
*/
|
|
1823
|
-
class UserMultiSelectComponent {
|
|
1824
|
-
destroyRef = inject(DestroyRef);
|
|
1825
|
-
userProvider = inject(USER_PROVIDER);
|
|
1826
|
-
abortController = null;
|
|
1827
|
-
// Optional: custom function to load users (uses USER_PROVIDER if not provided)
|
|
1828
|
-
loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
|
|
1829
|
-
// Inputs
|
|
1830
|
-
placeHolder = input('Select Users', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1831
|
-
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
1832
|
-
filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
|
|
1833
|
-
additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
|
|
1834
|
-
pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
|
|
2003
|
+
class UserMultiSelectComponent extends BaseUserSelectComponent {
|
|
1835
2004
|
// Two-way bound value
|
|
1836
2005
|
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1837
2006
|
// Outputs
|
|
1838
2007
|
usersSelected = output();
|
|
1839
|
-
|
|
1840
|
-
// Internal state
|
|
1841
|
-
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
1842
|
-
users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
|
|
1843
|
-
total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1844
|
-
pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1845
|
-
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1846
|
-
// Computed dropdown data
|
|
1847
|
-
dropdownUsers = computed(() => this.users().map((user) => ({
|
|
1848
|
-
label: user.name || user.email,
|
|
1849
|
-
value: user.id,
|
|
1850
|
-
})), ...(ngDevMode ? [{ debugName: "dropdownUsers" }] : []));
|
|
1851
|
-
constructor() {
|
|
1852
|
-
// Cleanup on destroy
|
|
1853
|
-
this.destroyRef.onDestroy(() => {
|
|
1854
|
-
this.abortController?.abort();
|
|
1855
|
-
});
|
|
1856
|
-
// Update page size from input
|
|
1857
|
-
effect(() => {
|
|
1858
|
-
const size = this.pageSize();
|
|
1859
|
-
untracked(() => {
|
|
1860
|
-
this.pagination.update((p) => ({ ...p, pageSize: size }));
|
|
1861
|
-
});
|
|
1862
|
-
});
|
|
1863
|
-
// Load initial users after render
|
|
1864
|
-
afterNextRender(() => {
|
|
1865
|
-
this.fetchUsers();
|
|
1866
|
-
});
|
|
2008
|
+
setupValueEffect() {
|
|
1867
2009
|
// Emit selected users when value changes
|
|
1868
2010
|
effect(() => {
|
|
1869
2011
|
const selectedIds = this.value() ?? [];
|
|
@@ -1874,81 +2016,8 @@ class UserMultiSelectComponent {
|
|
|
1874
2016
|
});
|
|
1875
2017
|
});
|
|
1876
2018
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
this.pagination.update((p) => ({ ...p, currentPage: 0 }));
|
|
1880
|
-
this.users.set([]);
|
|
1881
|
-
this.fetchUsers();
|
|
1882
|
-
}
|
|
1883
|
-
handlePagination(pagination) {
|
|
1884
|
-
this.pagination.set(pagination);
|
|
1885
|
-
this.fetchUsers(true);
|
|
1886
|
-
}
|
|
1887
|
-
async fetchUsers(append = false) {
|
|
1888
|
-
if (this.isLoading())
|
|
1889
|
-
return;
|
|
1890
|
-
// Cancel previous request
|
|
1891
|
-
this.abortController?.abort();
|
|
1892
|
-
this.abortController = new AbortController();
|
|
1893
|
-
this.isLoading.set(true);
|
|
1894
|
-
try {
|
|
1895
|
-
const pag = this.pagination();
|
|
1896
|
-
const filter = {
|
|
1897
|
-
page: pag.currentPage,
|
|
1898
|
-
pageSize: pag.pageSize,
|
|
1899
|
-
search: this.searchTerm(),
|
|
1900
|
-
...this.additionalFilters(),
|
|
1901
|
-
};
|
|
1902
|
-
// Use custom loadUsers if provided, otherwise use USER_PROVIDER
|
|
1903
|
-
const customLoadUsers = this.loadUsers();
|
|
1904
|
-
const response = await firstValueFrom(customLoadUsers
|
|
1905
|
-
? customLoadUsers(filter)
|
|
1906
|
-
: this.loadUsersFromProvider(filter));
|
|
1907
|
-
if (response.success && response.data) {
|
|
1908
|
-
if (append) {
|
|
1909
|
-
this.users.update((current) => [...current, ...response.data]);
|
|
1910
|
-
}
|
|
1911
|
-
else {
|
|
1912
|
-
this.users.set(response.data);
|
|
1913
|
-
}
|
|
1914
|
-
this.total.set(response.meta?.total);
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
catch (error) {
|
|
1918
|
-
if (error.name !== 'AbortError') {
|
|
1919
|
-
this.onError.emit(error);
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
finally {
|
|
1923
|
-
this.isLoading.set(false);
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
/** Load users from USER_PROVIDER with active filter */
|
|
1927
|
-
loadUsersFromProvider(filter) {
|
|
1928
|
-
return this.userProvider
|
|
1929
|
-
.getUsers({
|
|
1930
|
-
page: filter.page,
|
|
1931
|
-
pageSize: filter.pageSize,
|
|
1932
|
-
search: filter.search,
|
|
1933
|
-
isActive: this.filterActive() ? true : undefined,
|
|
1934
|
-
})
|
|
1935
|
-
.pipe(map$1((res) => ({
|
|
1936
|
-
...res,
|
|
1937
|
-
data: res.data?.map((u) => ({
|
|
1938
|
-
id: u.id,
|
|
1939
|
-
name: u.name,
|
|
1940
|
-
email: u.email,
|
|
1941
|
-
})),
|
|
1942
|
-
})));
|
|
1943
|
-
}
|
|
1944
|
-
/** Reload users (useful when filters change externally) */
|
|
1945
|
-
reload() {
|
|
1946
|
-
this.pagination.update((p) => ({ ...p, currentPage: 0 }));
|
|
1947
|
-
this.users.set([]);
|
|
1948
|
-
this.fetchUsers();
|
|
1949
|
-
}
|
|
1950
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1951
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserMultiSelectComponent, isStandalone: true, selector: "lib-user-multi-select", inputs: { loadUsers: { classPropertyName: "loadUsers", publicName: "loadUsers", isSignal: true, isRequired: false, transformFunction: null }, placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, filterActive: { classPropertyName: "filterActive", publicName: "filterActive", isSignal: true, isRequired: false, transformFunction: null }, additionalFilters: { classPropertyName: "additionalFilters", publicName: "additionalFilters", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", usersSelected: "usersSelected", onError: "onError" }, ngImport: i0, template: `
|
|
2019
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserMultiSelectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
2020
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserMultiSelectComponent, isStandalone: true, selector: "lib-user-multi-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", usersSelected: "usersSelected" }, usesInheritance: true, ngImport: i0, template: `
|
|
1952
2021
|
<lib-lazy-multi-select
|
|
1953
2022
|
[(value)]="value"
|
|
1954
2023
|
[placeHolder]="placeHolder()"
|
|
@@ -1966,7 +2035,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1966
2035
|
type: Component,
|
|
1967
2036
|
args: [{
|
|
1968
2037
|
selector: 'lib-user-multi-select',
|
|
1969
|
-
standalone: true,
|
|
1970
2038
|
imports: [AngularModule, PrimeModule, LazyMultiSelectComponent],
|
|
1971
2039
|
template: `
|
|
1972
2040
|
<lib-lazy-multi-select
|
|
@@ -1983,47 +2051,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1983
2051
|
`,
|
|
1984
2052
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1985
2053
|
}]
|
|
1986
|
-
}],
|
|
2054
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], usersSelected: [{ type: i0.Output, args: ["usersSelected"] }] } });
|
|
1987
2055
|
|
|
1988
2056
|
/**
|
|
1989
2057
|
* File Uploader Component - Drag & drop file upload with type filtering.
|
|
1990
2058
|
*
|
|
1991
2059
|
* Pass your own `uploadFile` function - works with any storage API.
|
|
1992
|
-
*
|
|
1993
|
-
* Features:
|
|
1994
|
-
* - Drag & drop support
|
|
1995
|
-
* - File type filtering (images, documents, etc.)
|
|
1996
|
-
* - Upload progress indication
|
|
1997
|
-
* - Multiple file support (optional)
|
|
1998
|
-
* - Image compression options
|
|
1999
|
-
*
|
|
2000
|
-
* @example
|
|
2001
|
-
* ```typescript
|
|
2002
|
-
* // In component
|
|
2003
|
-
* readonly uploadService = inject(UploadService);
|
|
2004
|
-
*
|
|
2005
|
-
* readonly uploadFile: UploadFileFn = (file, options) =>
|
|
2006
|
-
* this.uploadService.uploadSingleFile(file, options);
|
|
2007
|
-
* ```
|
|
2008
|
-
*
|
|
2009
|
-
* ```html
|
|
2010
|
-
* <!-- Single image upload -->
|
|
2011
|
-
* <lib-file-uploader
|
|
2012
|
-
* [uploadFile]="uploadFile"
|
|
2013
|
-
* [acceptTypes]="['image/*']"
|
|
2014
|
-
* [multiple]="false"
|
|
2015
|
-
* (fileUploaded)="onFileUploaded($event)"
|
|
2016
|
-
* />
|
|
2017
|
-
*
|
|
2018
|
-
* <!-- Multiple document upload -->
|
|
2019
|
-
* <lib-file-uploader
|
|
2020
|
-
* [uploadFile]="uploadFile"
|
|
2021
|
-
* [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
|
|
2022
|
-
* [multiple]="true"
|
|
2023
|
-
* [maxFiles]="5"
|
|
2024
|
-
* (filesUploaded)="onFilesUploaded($event)"
|
|
2025
|
-
* />
|
|
2026
|
-
* ```
|
|
2027
2060
|
*/
|
|
2028
2061
|
class FileUploaderComponent {
|
|
2029
2062
|
messageService = inject(MessageService);
|
|
@@ -2169,11 +2202,7 @@ class FileUploaderComponent {
|
|
|
2169
2202
|
});
|
|
2170
2203
|
}
|
|
2171
2204
|
catch (error) {
|
|
2172
|
-
|
|
2173
|
-
severity: 'error',
|
|
2174
|
-
summary: 'Upload Failed',
|
|
2175
|
-
detail: error.message || 'Failed to upload file',
|
|
2176
|
-
});
|
|
2205
|
+
// Error toast handled by global interceptor
|
|
2177
2206
|
this.onError.emit(error);
|
|
2178
2207
|
}
|
|
2179
2208
|
finally {
|
|
@@ -2195,37 +2224,41 @@ class FileUploaderComponent {
|
|
|
2195
2224
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2196
2225
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: FileUploaderComponent, isStandalone: true, selector: "lib-file-uploader", inputs: { uploadFile: { classPropertyName: "uploadFile", publicName: "uploadFile", isSignal: true, isRequired: true, transformFunction: null }, acceptTypes: { classPropertyName: "acceptTypes", publicName: "acceptTypes", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxSizeMb: { classPropertyName: "maxSizeMb", publicName: "maxSizeMb", isSignal: true, isRequired: false, transformFunction: null }, uploadOptions: { classPropertyName: "uploadOptions", publicName: "uploadOptions", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showPreview: { classPropertyName: "showPreview", publicName: "showPreview", isSignal: true, isRequired: false, transformFunction: null }, autoUpload: { classPropertyName: "autoUpload", publicName: "autoUpload", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileUploaded: "fileUploaded", filesUploaded: "filesUploaded", onError: "onError", fileSelected: "fileSelected" }, ngImport: i0, template: `
|
|
2197
2226
|
<div
|
|
2198
|
-
class="
|
|
2199
|
-
[class.
|
|
2200
|
-
[class.disabled]="disabled()"
|
|
2227
|
+
class="w-full"
|
|
2228
|
+
[class.opacity-60]="disabled()"
|
|
2201
2229
|
(dragover)="onDragOver($event)"
|
|
2202
2230
|
(dragleave)="onDragLeave($event)"
|
|
2203
2231
|
(drop)="onDrop($event)"
|
|
2204
2232
|
>
|
|
2205
|
-
<!-- Upload Area -->
|
|
2206
|
-
<div
|
|
2233
|
+
<!-- Upload Area - Responsive padding -->
|
|
2234
|
+
<div
|
|
2235
|
+
class="upload-zone border-2 border-dashed rounded-lg p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-200 text-center"
|
|
2236
|
+
[class.drag-over]="isDragOver()"
|
|
2237
|
+
[class.cursor-not-allowed]="disabled()"
|
|
2238
|
+
(click)="fileInput.click()"
|
|
2239
|
+
>
|
|
2207
2240
|
@if (isUploading()) {
|
|
2208
|
-
<div class="
|
|
2209
|
-
<i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
|
|
2210
|
-
<p class="mt-2">Uploading {{ uploadingFileName() }}...</p>
|
|
2241
|
+
<div class="flex flex-col items-center">
|
|
2242
|
+
<i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
|
|
2243
|
+
<p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
|
|
2211
2244
|
@if (uploadProgress() > 0) {
|
|
2212
|
-
<p-progressBar [value]="uploadProgress()" [showValue]="true" />
|
|
2245
|
+
<p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
|
|
2213
2246
|
}
|
|
2214
2247
|
</div>
|
|
2215
2248
|
} @else {
|
|
2216
|
-
<div class="
|
|
2217
|
-
<i class="pi pi-cloud-upload text-4xl text-primary"></i>
|
|
2218
|
-
<p class="mt-2 mb-1 font-semibold">
|
|
2249
|
+
<div class="flex flex-col items-center">
|
|
2250
|
+
<i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
|
|
2251
|
+
<p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
|
|
2219
2252
|
{{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
|
|
2220
2253
|
</p>
|
|
2221
|
-
<p class="text-sm text-color-secondary">
|
|
2254
|
+
<p class="text-xs sm:text-sm text-color-secondary px-2">
|
|
2222
2255
|
@if (acceptTypesDisplay()) {
|
|
2223
2256
|
Allowed: {{ acceptTypesDisplay() }}
|
|
2224
2257
|
} @else {
|
|
2225
2258
|
All file types allowed
|
|
2226
2259
|
}
|
|
2227
2260
|
@if (maxSizeMb()) {
|
|
2228
|
-
(Max {{ maxSizeMb() }}MB)
|
|
2261
|
+
<span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
|
|
2229
2262
|
}
|
|
2230
2263
|
</p>
|
|
2231
2264
|
</div>
|
|
@@ -2236,71 +2269,76 @@ class FileUploaderComponent {
|
|
|
2236
2269
|
<input
|
|
2237
2270
|
#fileInput
|
|
2238
2271
|
type="file"
|
|
2272
|
+
class="hidden"
|
|
2239
2273
|
[accept]="acceptString()"
|
|
2240
2274
|
[multiple]="multiple()"
|
|
2241
2275
|
[disabled]="disabled() || isUploading()"
|
|
2242
2276
|
(change)="onFileSelected($event)"
|
|
2243
|
-
class="hidden"
|
|
2244
2277
|
/>
|
|
2245
2278
|
|
|
2246
|
-
<!-- Selected Files Preview -->
|
|
2279
|
+
<!-- Selected Files Preview - Responsive layout -->
|
|
2247
2280
|
@if (selectedFiles().length > 0 && showPreview()) {
|
|
2248
|
-
<div class="
|
|
2281
|
+
<div class="mt-3 space-y-2">
|
|
2249
2282
|
@for (file of selectedFiles(); track file.name) {
|
|
2250
|
-
<div class="file-item flex
|
|
2251
|
-
<i [class]="getFileIcon(file)"></i>
|
|
2252
|
-
<span class="flex-1 text-
|
|
2253
|
-
<span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
|
|
2254
|
-
<button
|
|
2255
|
-
pButton
|
|
2256
|
-
type="button"
|
|
2283
|
+
<div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
|
|
2284
|
+
<i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
|
|
2285
|
+
<span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
|
|
2286
|
+
<span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
|
|
2287
|
+
<p-button
|
|
2257
2288
|
icon="pi pi-times"
|
|
2258
|
-
|
|
2259
|
-
|
|
2289
|
+
[text]="true"
|
|
2290
|
+
[rounded]="true"
|
|
2291
|
+
size="small"
|
|
2292
|
+
severity="secondary"
|
|
2260
2293
|
[disabled]="isUploading()"
|
|
2261
|
-
|
|
2294
|
+
(onClick)="removeFile(file)"
|
|
2295
|
+
/>
|
|
2262
2296
|
</div>
|
|
2263
2297
|
}
|
|
2264
2298
|
</div>
|
|
2265
2299
|
}
|
|
2266
2300
|
</div>
|
|
2267
|
-
`, isInline: true, styles: [".
|
|
2301
|
+
`, isInline: true, styles: [".upload-zone{border-color:var(--p-surface-300);background:var(--p-surface-50)}.upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-100)}.upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .upload-zone,.dark .upload-zone{border-color:var(--p-surface-600);background:var(--p-surface-800)}:host-context(.p-dark) .upload-zone:hover:not(.cursor-not-allowed),.dark .upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-700)}:host-context(.p-dark) .upload-zone.drag-over,.dark .upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-900)}.file-preview-item{border:1px solid var(--p-surface-200);background:var(--p-surface-0)}:host-context(.p-dark) .file-preview-item,.dark .file-preview-item{border-color:var(--p-surface-600);background:var(--p-surface-800)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i1$2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i2$1.ProgressBar, selector: "p-progressBar, p-progressbar, p-progress-bar", inputs: ["value", "showValue", "styleClass", "valueStyleClass", "unit", "mode", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2268
2302
|
}
|
|
2269
2303
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, decorators: [{
|
|
2270
2304
|
type: Component,
|
|
2271
|
-
args: [{ selector: 'lib-file-uploader',
|
|
2305
|
+
args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule], template: `
|
|
2272
2306
|
<div
|
|
2273
|
-
class="
|
|
2274
|
-
[class.
|
|
2275
|
-
[class.disabled]="disabled()"
|
|
2307
|
+
class="w-full"
|
|
2308
|
+
[class.opacity-60]="disabled()"
|
|
2276
2309
|
(dragover)="onDragOver($event)"
|
|
2277
2310
|
(dragleave)="onDragLeave($event)"
|
|
2278
2311
|
(drop)="onDrop($event)"
|
|
2279
2312
|
>
|
|
2280
|
-
<!-- Upload Area -->
|
|
2281
|
-
<div
|
|
2313
|
+
<!-- Upload Area - Responsive padding -->
|
|
2314
|
+
<div
|
|
2315
|
+
class="upload-zone border-2 border-dashed rounded-lg p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-200 text-center"
|
|
2316
|
+
[class.drag-over]="isDragOver()"
|
|
2317
|
+
[class.cursor-not-allowed]="disabled()"
|
|
2318
|
+
(click)="fileInput.click()"
|
|
2319
|
+
>
|
|
2282
2320
|
@if (isUploading()) {
|
|
2283
|
-
<div class="
|
|
2284
|
-
<i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
|
|
2285
|
-
<p class="mt-2">Uploading {{ uploadingFileName() }}...</p>
|
|
2321
|
+
<div class="flex flex-col items-center">
|
|
2322
|
+
<i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
|
|
2323
|
+
<p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
|
|
2286
2324
|
@if (uploadProgress() > 0) {
|
|
2287
|
-
<p-progressBar [value]="uploadProgress()" [showValue]="true" />
|
|
2325
|
+
<p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
|
|
2288
2326
|
}
|
|
2289
2327
|
</div>
|
|
2290
2328
|
} @else {
|
|
2291
|
-
<div class="
|
|
2292
|
-
<i class="pi pi-cloud-upload text-4xl text-primary"></i>
|
|
2293
|
-
<p class="mt-2 mb-1 font-semibold">
|
|
2329
|
+
<div class="flex flex-col items-center">
|
|
2330
|
+
<i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
|
|
2331
|
+
<p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
|
|
2294
2332
|
{{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
|
|
2295
2333
|
</p>
|
|
2296
|
-
<p class="text-sm text-color-secondary">
|
|
2334
|
+
<p class="text-xs sm:text-sm text-color-secondary px-2">
|
|
2297
2335
|
@if (acceptTypesDisplay()) {
|
|
2298
2336
|
Allowed: {{ acceptTypesDisplay() }}
|
|
2299
2337
|
} @else {
|
|
2300
2338
|
All file types allowed
|
|
2301
2339
|
}
|
|
2302
2340
|
@if (maxSizeMb()) {
|
|
2303
|
-
(Max {{ maxSizeMb() }}MB)
|
|
2341
|
+
<span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
|
|
2304
2342
|
}
|
|
2305
2343
|
</p>
|
|
2306
2344
|
</div>
|
|
@@ -2311,35 +2349,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2311
2349
|
<input
|
|
2312
2350
|
#fileInput
|
|
2313
2351
|
type="file"
|
|
2352
|
+
class="hidden"
|
|
2314
2353
|
[accept]="acceptString()"
|
|
2315
2354
|
[multiple]="multiple()"
|
|
2316
2355
|
[disabled]="disabled() || isUploading()"
|
|
2317
2356
|
(change)="onFileSelected($event)"
|
|
2318
|
-
class="hidden"
|
|
2319
2357
|
/>
|
|
2320
2358
|
|
|
2321
|
-
<!-- Selected Files Preview -->
|
|
2359
|
+
<!-- Selected Files Preview - Responsive layout -->
|
|
2322
2360
|
@if (selectedFiles().length > 0 && showPreview()) {
|
|
2323
|
-
<div class="
|
|
2361
|
+
<div class="mt-3 space-y-2">
|
|
2324
2362
|
@for (file of selectedFiles(); track file.name) {
|
|
2325
|
-
<div class="file-item flex
|
|
2326
|
-
<i [class]="getFileIcon(file)"></i>
|
|
2327
|
-
<span class="flex-1 text-
|
|
2328
|
-
<span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
|
|
2329
|
-
<button
|
|
2330
|
-
pButton
|
|
2331
|
-
type="button"
|
|
2363
|
+
<div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
|
|
2364
|
+
<i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
|
|
2365
|
+
<span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
|
|
2366
|
+
<span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
|
|
2367
|
+
<p-button
|
|
2332
2368
|
icon="pi pi-times"
|
|
2333
|
-
|
|
2334
|
-
|
|
2369
|
+
[text]="true"
|
|
2370
|
+
[rounded]="true"
|
|
2371
|
+
size="small"
|
|
2372
|
+
severity="secondary"
|
|
2335
2373
|
[disabled]="isUploading()"
|
|
2336
|
-
|
|
2374
|
+
(onClick)="removeFile(file)"
|
|
2375
|
+
/>
|
|
2337
2376
|
</div>
|
|
2338
2377
|
}
|
|
2339
2378
|
</div>
|
|
2340
2379
|
}
|
|
2341
2380
|
</div>
|
|
2342
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".
|
|
2381
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".upload-zone{border-color:var(--p-surface-300);background:var(--p-surface-50)}.upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-100)}.upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .upload-zone,.dark .upload-zone{border-color:var(--p-surface-600);background:var(--p-surface-800)}:host-context(.p-dark) .upload-zone:hover:not(.cursor-not-allowed),.dark .upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-700)}:host-context(.p-dark) .upload-zone.drag-over,.dark .upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-900)}.file-preview-item{border:1px solid var(--p-surface-200);background:var(--p-surface-0)}:host-context(.p-dark) .file-preview-item,.dark .file-preview-item{border-color:var(--p-surface-600);background:var(--p-surface-800)}\n"] }]
|
|
2343
2382
|
}], propDecorators: { uploadFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadFile", required: true }] }], acceptTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "acceptTypes", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], maxSizeMb: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSizeMb", required: false }] }], uploadOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadOptions", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreview", required: false }] }], autoUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoUpload", required: false }] }], fileUploaded: [{ type: i0.Output, args: ["fileUploaded"] }], filesUploaded: [{ type: i0.Output, args: ["filesUploaded"] }], onError: [{ type: i0.Output, args: ["onError"] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }] } });
|
|
2344
2383
|
|
|
2345
2384
|
const DEFAULT_PAGE_SIZE = 20;
|
|
@@ -2454,16 +2493,14 @@ class FileSelectorDialogComponent {
|
|
|
2454
2493
|
}, 500);
|
|
2455
2494
|
}
|
|
2456
2495
|
onScroll(event) {
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
this.fetchFiles(true);
|
|
2466
|
-
}
|
|
2496
|
+
const nextPagination = checkScrollPagination(event, {
|
|
2497
|
+
pagination: this.pagination(),
|
|
2498
|
+
total: this.total(),
|
|
2499
|
+
isLoading: this.isLoading(),
|
|
2500
|
+
});
|
|
2501
|
+
if (nextPagination) {
|
|
2502
|
+
this.pagination.set(nextPagination);
|
|
2503
|
+
this.fetchFiles(true);
|
|
2467
2504
|
}
|
|
2468
2505
|
}
|
|
2469
2506
|
toggleSelection(file) {
|
|
@@ -2574,11 +2611,13 @@ class FileSelectorDialogComponent {
|
|
|
2574
2611
|
[closable]="true"
|
|
2575
2612
|
[draggable]="false"
|
|
2576
2613
|
[resizable]="false"
|
|
2577
|
-
[
|
|
2614
|
+
[breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
|
|
2615
|
+
[style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
|
|
2616
|
+
styleClass="file-selector-dialog"
|
|
2578
2617
|
(onHide)="onDialogHide()"
|
|
2579
2618
|
>
|
|
2580
2619
|
<!-- Search Bar -->
|
|
2581
|
-
<div class="flex gap-2 mb-3">
|
|
2620
|
+
<div class="flex flex-col sm:flex-row gap-2 mb-3">
|
|
2582
2621
|
<span class="p-input-icon-left flex-1">
|
|
2583
2622
|
<i class="pi pi-search"></i>
|
|
2584
2623
|
<input
|
|
@@ -2591,25 +2630,25 @@ class FileSelectorDialogComponent {
|
|
|
2591
2630
|
/>
|
|
2592
2631
|
</span>
|
|
2593
2632
|
@if (multiple()) {
|
|
2594
|
-
<span class="text-sm text-color-secondary
|
|
2633
|
+
<span class="text-sm text-color-secondary self-center whitespace-nowrap">
|
|
2595
2634
|
{{ selectedFiles().length }} selected
|
|
2596
2635
|
</span>
|
|
2597
2636
|
}
|
|
2598
2637
|
</div>
|
|
2599
2638
|
|
|
2600
|
-
<!-- File Grid -->
|
|
2639
|
+
<!-- File Grid - Responsive columns -->
|
|
2601
2640
|
<div
|
|
2602
2641
|
class="file-grid"
|
|
2603
2642
|
#scrollContainer
|
|
2604
2643
|
(scroll)="onScroll($event)"
|
|
2605
2644
|
>
|
|
2606
2645
|
@if (isLoading() && files().length === 0) {
|
|
2607
|
-
<div class="flex justify-
|
|
2608
|
-
<i class="pi pi-spin pi-spinner text-4xl"></i>
|
|
2646
|
+
<div class="col-span-full flex justify-center p-4">
|
|
2647
|
+
<i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
|
|
2609
2648
|
</div>
|
|
2610
2649
|
} @else if (files().length === 0) {
|
|
2611
|
-
<div class="text-center p-4 text-color-secondary">
|
|
2612
|
-
<i class="pi pi-inbox text-4xl mb-2"></i>
|
|
2650
|
+
<div class="col-span-full text-center p-4 text-color-secondary">
|
|
2651
|
+
<i class="pi pi-inbox text-4xl mb-2 block"></i>
|
|
2613
2652
|
<p>No files found</p>
|
|
2614
2653
|
</div>
|
|
2615
2654
|
} @else {
|
|
@@ -2623,54 +2662,59 @@ class FileSelectorDialogComponent {
|
|
|
2623
2662
|
<!-- File Preview -->
|
|
2624
2663
|
<div class="file-preview">
|
|
2625
2664
|
@if (isImage(file) && file.url) {
|
|
2626
|
-
<img [src]="file.url" [alt]="file.name" class="
|
|
2665
|
+
<img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
|
|
2627
2666
|
} @else {
|
|
2628
|
-
<i [class]="getFileIcon(file)" class="
|
|
2667
|
+
<i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
|
|
2629
2668
|
}
|
|
2630
2669
|
@if (isSelected(file)) {
|
|
2631
2670
|
<div class="selected-overlay">
|
|
2632
|
-
<i class="pi pi-check"></i>
|
|
2671
|
+
<i class="pi pi-check text-xl sm:text-2xl"></i>
|
|
2633
2672
|
</div>
|
|
2634
2673
|
}
|
|
2635
2674
|
</div>
|
|
2636
2675
|
|
|
2637
2676
|
<!-- File Info -->
|
|
2638
|
-
<div class="
|
|
2639
|
-
<span class="
|
|
2640
|
-
|
|
2677
|
+
<div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
|
|
2678
|
+
<span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
|
|
2679
|
+
{{ file.name }}
|
|
2680
|
+
</span>
|
|
2681
|
+
<span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
|
|
2641
2682
|
</div>
|
|
2642
2683
|
</div>
|
|
2643
2684
|
}
|
|
2644
2685
|
|
|
2645
2686
|
@if (isLoading()) {
|
|
2646
|
-
<div class="flex justify-
|
|
2647
|
-
<i class="pi pi-spin pi-spinner"></i>
|
|
2687
|
+
<div class="col-span-full flex justify-center p-2">
|
|
2688
|
+
<i class="pi pi-spin pi-spinner text-color-secondary"></i>
|
|
2648
2689
|
</div>
|
|
2649
2690
|
}
|
|
2650
2691
|
}
|
|
2651
2692
|
</div>
|
|
2652
2693
|
|
|
2653
2694
|
<!-- Footer -->
|
|
2654
|
-
<ng-template
|
|
2655
|
-
<
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2695
|
+
<ng-template #footer>
|
|
2696
|
+
<div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
|
|
2697
|
+
<button
|
|
2698
|
+
pButton
|
|
2699
|
+
label="Cancel"
|
|
2700
|
+
class="p-button-text w-full sm:w-auto"
|
|
2701
|
+
(click)="onCancel()"
|
|
2702
|
+
></button>
|
|
2703
|
+
<button
|
|
2704
|
+
pButton
|
|
2705
|
+
[label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
|
|
2706
|
+
[disabled]="selectedFiles().length === 0"
|
|
2707
|
+
class="w-full sm:w-auto"
|
|
2708
|
+
(click)="onConfirm()"
|
|
2709
|
+
></button>
|
|
2710
|
+
</div>
|
|
2667
2711
|
</ng-template>
|
|
2668
2712
|
</p-dialog>
|
|
2669
|
-
`, isInline: true, styles: [".file-grid{display:grid;grid-template-columns:repeat(
|
|
2713
|
+
`, isInline: true, styles: [".file-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem;max-height:300px;overflow-y:auto;padding:.5rem}@media(min-width:480px){.file-grid{grid-template-columns:repeat(3,1fr);max-height:350px}}@media(min-width:640px){.file-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:1rem;max-height:400px}}.file-card{border:2px solid var(--p-surface-300);border-radius:var(--p-border-radius);cursor:pointer;transition:all .2s ease;overflow:hidden;background:var(--p-surface-0)}:host-context(.p-dark) .file-card,.dark .file-card{border-color:var(--p-surface-600);background:var(--p-surface-800)}.file-card:hover:not(.disabled){border-color:var(--p-primary-color);transform:translateY(-2px);box-shadow:var(--p-overlay-shadow)}.file-card.selected{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .file-card.selected,.dark .file-card.selected{background:var(--p-primary-900)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:80px;display:flex;align-items:center;justify-content:center;background:var(--p-surface-100)}@media(min-width:640px){.file-preview{height:100px}}:host-context(.p-dark) .file-preview,.dark .file-preview{background:var(--p-surface-700)}.selected-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--p-primary-500-rgb, 59, 130, 246),.3)}.selected-overlay i{color:var(--p-primary-color);background:var(--p-surface-0);border-radius:50%;padding:.5rem}:host-context(.p-dark) .selected-overlay i,.dark .selected-overlay i{background:var(--p-surface-900)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { 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: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$2.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i4.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2670
2714
|
}
|
|
2671
2715
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
|
|
2672
2716
|
type: Component,
|
|
2673
|
-
args: [{ selector: 'lib-file-selector-dialog',
|
|
2717
|
+
args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule], template: `
|
|
2674
2718
|
<p-dialog
|
|
2675
2719
|
[header]="header()"
|
|
2676
2720
|
[(visible)]="visible"
|
|
@@ -2678,11 +2722,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2678
2722
|
[closable]="true"
|
|
2679
2723
|
[draggable]="false"
|
|
2680
2724
|
[resizable]="false"
|
|
2681
|
-
[
|
|
2725
|
+
[breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
|
|
2726
|
+
[style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
|
|
2727
|
+
styleClass="file-selector-dialog"
|
|
2682
2728
|
(onHide)="onDialogHide()"
|
|
2683
2729
|
>
|
|
2684
2730
|
<!-- Search Bar -->
|
|
2685
|
-
<div class="flex gap-2 mb-3">
|
|
2731
|
+
<div class="flex flex-col sm:flex-row gap-2 mb-3">
|
|
2686
2732
|
<span class="p-input-icon-left flex-1">
|
|
2687
2733
|
<i class="pi pi-search"></i>
|
|
2688
2734
|
<input
|
|
@@ -2695,25 +2741,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2695
2741
|
/>
|
|
2696
2742
|
</span>
|
|
2697
2743
|
@if (multiple()) {
|
|
2698
|
-
<span class="text-sm text-color-secondary
|
|
2744
|
+
<span class="text-sm text-color-secondary self-center whitespace-nowrap">
|
|
2699
2745
|
{{ selectedFiles().length }} selected
|
|
2700
2746
|
</span>
|
|
2701
2747
|
}
|
|
2702
2748
|
</div>
|
|
2703
2749
|
|
|
2704
|
-
<!-- File Grid -->
|
|
2750
|
+
<!-- File Grid - Responsive columns -->
|
|
2705
2751
|
<div
|
|
2706
2752
|
class="file-grid"
|
|
2707
2753
|
#scrollContainer
|
|
2708
2754
|
(scroll)="onScroll($event)"
|
|
2709
2755
|
>
|
|
2710
2756
|
@if (isLoading() && files().length === 0) {
|
|
2711
|
-
<div class="flex justify-
|
|
2712
|
-
<i class="pi pi-spin pi-spinner text-4xl"></i>
|
|
2757
|
+
<div class="col-span-full flex justify-center p-4">
|
|
2758
|
+
<i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
|
|
2713
2759
|
</div>
|
|
2714
2760
|
} @else if (files().length === 0) {
|
|
2715
|
-
<div class="text-center p-4 text-color-secondary">
|
|
2716
|
-
<i class="pi pi-inbox text-4xl mb-2"></i>
|
|
2761
|
+
<div class="col-span-full text-center p-4 text-color-secondary">
|
|
2762
|
+
<i class="pi pi-inbox text-4xl mb-2 block"></i>
|
|
2717
2763
|
<p>No files found</p>
|
|
2718
2764
|
</div>
|
|
2719
2765
|
} @else {
|
|
@@ -2727,182 +2773,484 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2727
2773
|
<!-- File Preview -->
|
|
2728
2774
|
<div class="file-preview">
|
|
2729
2775
|
@if (isImage(file) && file.url) {
|
|
2730
|
-
<img [src]="file.url" [alt]="file.name" class="
|
|
2776
|
+
<img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
|
|
2731
2777
|
} @else {
|
|
2732
|
-
<i [class]="getFileIcon(file)" class="
|
|
2778
|
+
<i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
|
|
2733
2779
|
}
|
|
2734
2780
|
@if (isSelected(file)) {
|
|
2735
2781
|
<div class="selected-overlay">
|
|
2736
|
-
<i class="pi pi-check"></i>
|
|
2782
|
+
<i class="pi pi-check text-xl sm:text-2xl"></i>
|
|
2737
2783
|
</div>
|
|
2738
2784
|
}
|
|
2739
2785
|
</div>
|
|
2740
2786
|
|
|
2741
2787
|
<!-- File Info -->
|
|
2742
|
-
<div class="
|
|
2743
|
-
<span class="
|
|
2744
|
-
|
|
2788
|
+
<div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
|
|
2789
|
+
<span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
|
|
2790
|
+
{{ file.name }}
|
|
2791
|
+
</span>
|
|
2792
|
+
<span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
|
|
2745
2793
|
</div>
|
|
2746
2794
|
</div>
|
|
2747
2795
|
}
|
|
2748
2796
|
|
|
2749
2797
|
@if (isLoading()) {
|
|
2750
|
-
<div class="flex justify-
|
|
2751
|
-
<i class="pi pi-spin pi-spinner"></i>
|
|
2798
|
+
<div class="col-span-full flex justify-center p-2">
|
|
2799
|
+
<i class="pi pi-spin pi-spinner text-color-secondary"></i>
|
|
2752
2800
|
</div>
|
|
2753
2801
|
}
|
|
2754
2802
|
}
|
|
2755
2803
|
</div>
|
|
2756
2804
|
|
|
2757
2805
|
<!-- Footer -->
|
|
2758
|
-
<ng-template
|
|
2759
|
-
<
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2806
|
+
<ng-template #footer>
|
|
2807
|
+
<div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
|
|
2808
|
+
<button
|
|
2809
|
+
pButton
|
|
2810
|
+
label="Cancel"
|
|
2811
|
+
class="p-button-text w-full sm:w-auto"
|
|
2812
|
+
(click)="onCancel()"
|
|
2813
|
+
></button>
|
|
2814
|
+
<button
|
|
2815
|
+
pButton
|
|
2816
|
+
[label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
|
|
2817
|
+
[disabled]="selectedFiles().length === 0"
|
|
2818
|
+
class="w-full sm:w-auto"
|
|
2819
|
+
(click)="onConfirm()"
|
|
2820
|
+
></button>
|
|
2821
|
+
</div>
|
|
2771
2822
|
</ng-template>
|
|
2772
2823
|
</p-dialog>
|
|
2773
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-grid{display:grid;grid-template-columns:repeat(
|
|
2824
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem;max-height:300px;overflow-y:auto;padding:.5rem}@media(min-width:480px){.file-grid{grid-template-columns:repeat(3,1fr);max-height:350px}}@media(min-width:640px){.file-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:1rem;max-height:400px}}.file-card{border:2px solid var(--p-surface-300);border-radius:var(--p-border-radius);cursor:pointer;transition:all .2s ease;overflow:hidden;background:var(--p-surface-0)}:host-context(.p-dark) .file-card,.dark .file-card{border-color:var(--p-surface-600);background:var(--p-surface-800)}.file-card:hover:not(.disabled){border-color:var(--p-primary-color);transform:translateY(-2px);box-shadow:var(--p-overlay-shadow)}.file-card.selected{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .file-card.selected,.dark .file-card.selected{background:var(--p-primary-900)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:80px;display:flex;align-items:center;justify-content:center;background:var(--p-surface-100)}@media(min-width:640px){.file-preview{height:100px}}:host-context(.p-dark) .file-preview,.dark .file-preview{background:var(--p-surface-700)}.selected-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--p-primary-500-rgb, 59, 130, 246),.3)}.selected-overlay i{color:var(--p-primary-color);background:var(--p-surface-0);border-radius:50%;padding:.5rem}:host-context(.p-dark) .selected-overlay i,.dark .selected-overlay i{background:var(--p-surface-900)}\n"] }]
|
|
2774
2825
|
}], ctorParameters: () => [], propDecorators: { loadFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadFiles", required: true }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], acceptTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "acceptTypes", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelection", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }, { type: i0.Output, args: ["visibleChange"] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
|
|
2775
2826
|
|
|
2776
|
-
|
|
2777
|
-
const devLog = (message) => {
|
|
2778
|
-
if (isDevMode())
|
|
2779
|
-
console.log(message);
|
|
2780
|
-
};
|
|
2781
|
-
/**
|
|
2782
|
-
* Permission Guard
|
|
2783
|
-
*
|
|
2784
|
-
* Route-level guard for permission-based access control.
|
|
2785
|
-
* Validates permissions before allowing navigation.
|
|
2786
|
-
*
|
|
2787
|
-
* Features:
|
|
2788
|
-
* - Single permission check
|
|
2789
|
-
* - Complex ILogicNode logic trees
|
|
2790
|
-
* - Configurable redirect URL
|
|
2791
|
-
* - Debug logging for denied access
|
|
2792
|
-
*
|
|
2793
|
-
* @example
|
|
2794
|
-
* ```typescript
|
|
2795
|
-
* // Simple permission check
|
|
2796
|
-
* { path: 'users', canActivate: [permissionGuard('user.view')] }
|
|
2797
|
-
*
|
|
2798
|
-
* // Complex logic
|
|
2799
|
-
* { path: 'admin', canActivate: [permissionGuard({
|
|
2800
|
-
* id: 'root',
|
|
2801
|
-
* type: 'group',
|
|
2802
|
-
* operator: 'AND',
|
|
2803
|
-
* children: [
|
|
2804
|
-
* { id: '1', type: 'action', actionId: 'admin.view' },
|
|
2805
|
-
* { id: '2', type: 'action', actionId: 'admin.manage' }
|
|
2806
|
-
* ]
|
|
2807
|
-
* })] }
|
|
2808
|
-
*
|
|
2809
|
-
* // With custom redirect
|
|
2810
|
-
* { path: 'users', canActivate: [permissionGuard('user.view', '/access-denied')] }
|
|
2811
|
-
* ```
|
|
2812
|
-
*/
|
|
2813
|
-
function permissionGuard(permission, redirectTo = '/') {
|
|
2827
|
+
function createGuard(guardName, redirectTo, evaluate, getDenialMessage) {
|
|
2814
2828
|
return () => {
|
|
2815
2829
|
const permissionValidator = inject(PermissionValidatorService);
|
|
2816
2830
|
const router = inject(Router);
|
|
2817
|
-
// Check if permissions are loaded
|
|
2818
2831
|
if (!permissionValidator.isPermissionsLoaded()) {
|
|
2819
|
-
|
|
2832
|
+
if (isDevMode()) {
|
|
2833
|
+
console.log(`[${guardName}] Permissions not loaded, denying access`);
|
|
2834
|
+
}
|
|
2820
2835
|
return router.createUrlTree([redirectTo]);
|
|
2821
2836
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
devLog(`[permissionGuard] Access denied - missing permission: ${permissionCode}`);
|
|
2837
|
+
if (!evaluate(permissionValidator.permissions())) {
|
|
2838
|
+
if (isDevMode()) {
|
|
2839
|
+
console.log(`[${guardName}] ${getDenialMessage()}`);
|
|
2840
|
+
}
|
|
2827
2841
|
return router.createUrlTree([redirectTo]);
|
|
2828
2842
|
}
|
|
2829
2843
|
return true;
|
|
2830
2844
|
};
|
|
2831
2845
|
}
|
|
2832
2846
|
/**
|
|
2833
|
-
*
|
|
2847
|
+
* Permission Guard - Single permission or ILogicNode check.
|
|
2834
2848
|
*
|
|
2835
|
-
*
|
|
2849
|
+
* @example
|
|
2850
|
+
* ```typescript
|
|
2851
|
+
* { path: 'users', canActivate: [permissionGuard('user.view')] }
|
|
2852
|
+
* { path: 'admin', canActivate: [permissionGuard(logicNode, '/access-denied')] }
|
|
2853
|
+
* ```
|
|
2854
|
+
*/
|
|
2855
|
+
function permissionGuard(permission, redirectTo = '/') {
|
|
2856
|
+
const code = typeof permission === 'string' ? permission : 'complex-logic';
|
|
2857
|
+
return createGuard('permissionGuard', redirectTo, (perms) => evaluatePermission(permission, perms), () => `Access denied - missing: ${code}`);
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Any Permission Guard (OR logic) - Access if user has ANY permission.
|
|
2836
2861
|
*
|
|
2837
2862
|
* @example
|
|
2838
2863
|
* ```typescript
|
|
2839
|
-
* // Allow if user has view OR create permission
|
|
2840
2864
|
* { path: 'users', canActivate: [anyPermissionGuard(['user.view', 'user.create'])] }
|
|
2841
2865
|
* ```
|
|
2842
2866
|
*/
|
|
2843
2867
|
function anyPermissionGuard(permissions, redirectTo = '/') {
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
if (!permissionValidator.isPermissionsLoaded()) {
|
|
2854
|
-
devLog('[anyPermissionGuard] Permissions not loaded, denying access to route');
|
|
2855
|
-
return router.createUrlTree([redirectTo]);
|
|
2856
|
-
}
|
|
2857
|
-
const userPermissions = permissionValidator.permissions();
|
|
2858
|
-
const hasPermission = hasAnyPermission(permissions, userPermissions);
|
|
2859
|
-
if (!hasPermission) {
|
|
2860
|
-
devLog(`[anyPermissionGuard] Access denied - missing any of: ${permissions.join(', ')}`);
|
|
2861
|
-
return router.createUrlTree([redirectTo]);
|
|
2862
|
-
}
|
|
2863
|
-
return true;
|
|
2864
|
-
};
|
|
2868
|
+
if (!permissions?.length) {
|
|
2869
|
+
return () => {
|
|
2870
|
+
if (isDevMode()) {
|
|
2871
|
+
console.log('[anyPermissionGuard] Empty permissions array, denying');
|
|
2872
|
+
}
|
|
2873
|
+
return inject(Router).createUrlTree([redirectTo]);
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
return createGuard('anyPermissionGuard', redirectTo, (perms) => hasAnyPermission(permissions, perms), () => `Access denied - missing any of: ${permissions.join(', ')}`);
|
|
2865
2877
|
}
|
|
2866
2878
|
/**
|
|
2867
|
-
* All Permissions Guard (AND logic)
|
|
2868
|
-
*
|
|
2869
|
-
* Allows access only if user has ALL of the specified permissions.
|
|
2879
|
+
* All Permissions Guard (AND logic) - Access only if user has ALL permissions.
|
|
2870
2880
|
*
|
|
2871
2881
|
* @example
|
|
2872
2882
|
* ```typescript
|
|
2873
|
-
* // Allow only if user has BOTH view AND create permissions
|
|
2874
2883
|
* { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
|
|
2875
2884
|
* ```
|
|
2876
2885
|
*/
|
|
2877
2886
|
function allPermissionsGuard(permissions, redirectTo = '/') {
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2887
|
+
if (!permissions?.length) {
|
|
2888
|
+
return () => {
|
|
2889
|
+
if (isDevMode()) {
|
|
2890
|
+
console.log('[allPermissionsGuard] Empty permissions array, denying');
|
|
2891
|
+
}
|
|
2892
|
+
return inject(Router).createUrlTree([redirectTo]);
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
return createGuard('allPermissionsGuard', redirectTo, (perms) => hasAllPermissions(permissions, perms), () => `Access denied - missing all of: ${permissions.join(', ')}`);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
/**
|
|
2899
|
+
* Base class for form page components that handle create/edit operations.
|
|
2900
|
+
* Provides common functionality for loading existing items, form submission,
|
|
2901
|
+
* navigation, and toast notifications.
|
|
2902
|
+
*
|
|
2903
|
+
* ## Features
|
|
2904
|
+
* - Automatic route parameter handling (loads item when ID is present)
|
|
2905
|
+
* - Edit mode detection based on existing item
|
|
2906
|
+
* - Unified submit handler for create/update operations
|
|
2907
|
+
* - Cancel navigation
|
|
2908
|
+
* - Toast messages for success/validation errors
|
|
2909
|
+
*
|
|
2910
|
+
* ## Usage
|
|
2911
|
+
*
|
|
2912
|
+
* ```typescript
|
|
2913
|
+
* interface IProductFormModel {
|
|
2914
|
+
* name: string;
|
|
2915
|
+
* price: number;
|
|
2916
|
+
* }
|
|
2917
|
+
*
|
|
2918
|
+
* @Component({
|
|
2919
|
+
* selector: 'app-product-form',
|
|
2920
|
+
* standalone: true,
|
|
2921
|
+
* changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2922
|
+
* template: `...`
|
|
2923
|
+
* })
|
|
2924
|
+
* export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
|
|
2925
|
+
* private readonly productService = inject(ProductApiService);
|
|
2926
|
+
*
|
|
2927
|
+
* // Form model signal (private writable, public readonly)
|
|
2928
|
+
* private readonly _formModel = signal<IProductFormModel>({ name: '', price: 0 });
|
|
2929
|
+
* readonly formModel = this._formModel.asReadonly();
|
|
2930
|
+
*
|
|
2931
|
+
* // Required abstract implementations
|
|
2932
|
+
* getFormModel(): Signal<IProductFormModel> {
|
|
2933
|
+
* return this.formModel;
|
|
2934
|
+
* }
|
|
2935
|
+
*
|
|
2936
|
+
* getResourceRoute(): string {
|
|
2937
|
+
* return '/products';
|
|
2938
|
+
* }
|
|
2939
|
+
*
|
|
2940
|
+
* getResourceName(): string {
|
|
2941
|
+
* return 'Product';
|
|
2942
|
+
* }
|
|
2943
|
+
*
|
|
2944
|
+
* isFormValid(): boolean {
|
|
2945
|
+
* const model = this.formModel();
|
|
2946
|
+
* return model.name.trim().length > 0 && model.price > 0;
|
|
2947
|
+
* }
|
|
2948
|
+
*
|
|
2949
|
+
* loadItem(id: string): void {
|
|
2950
|
+
* this.isLoading.set(true);
|
|
2951
|
+
* this.productService.findById(id)
|
|
2952
|
+
* .pipe(takeUntilDestroyed(this.destroyRef))
|
|
2953
|
+
* .subscribe({
|
|
2954
|
+
* next: (response) => {
|
|
2955
|
+
* if (response.success && response.data) {
|
|
2956
|
+
* this.existingItem.set(response.data);
|
|
2957
|
+
* this._formModel.set({
|
|
2958
|
+
* name: response.data.name,
|
|
2959
|
+
* price: response.data.price,
|
|
2960
|
+
* });
|
|
2961
|
+
* }
|
|
2962
|
+
* this.isLoading.set(false);
|
|
2963
|
+
* },
|
|
2964
|
+
* error: () => {
|
|
2965
|
+
* this.router.navigate([this.getResourceRoute()]);
|
|
2966
|
+
* this.isLoading.set(false);
|
|
2967
|
+
* },
|
|
2968
|
+
* });
|
|
2969
|
+
* }
|
|
2970
|
+
*
|
|
2971
|
+
* createItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
|
|
2972
|
+
* return this.productService.insert(model);
|
|
2973
|
+
* }
|
|
2974
|
+
*
|
|
2975
|
+
* updateItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
|
|
2976
|
+
* return this.productService.update({ id: this.existingItem()!.id, ...model });
|
|
2977
|
+
* }
|
|
2978
|
+
* }
|
|
2979
|
+
* ```
|
|
2980
|
+
*
|
|
2981
|
+
* @template T The entity/interface type being edited
|
|
2982
|
+
* @template TFormModel The form model interface
|
|
2983
|
+
*/
|
|
2984
|
+
class BaseFormPage {
|
|
2985
|
+
router = inject(Router);
|
|
2986
|
+
route = inject(ActivatedRoute);
|
|
2987
|
+
messageService = inject(MessageService);
|
|
2988
|
+
destroyRef = inject(DestroyRef);
|
|
2989
|
+
routeParams = toSignal(this.route.paramMap);
|
|
2990
|
+
initialized = false;
|
|
2991
|
+
/** Loading state for async operations */
|
|
2992
|
+
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
2993
|
+
/** The existing item when in edit mode, null when creating */
|
|
2994
|
+
existingItem = signal(null, ...(ngDevMode ? [{ debugName: "existingItem" }] : []));
|
|
2995
|
+
/** Whether the form is in edit mode (has existing item) */
|
|
2996
|
+
isEditMode = computed(() => !!this.existingItem(), ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
2997
|
+
constructor() {
|
|
2998
|
+
effect(() => {
|
|
2999
|
+
const params = this.routeParams();
|
|
3000
|
+
if (!params || this.initialized)
|
|
3001
|
+
return;
|
|
3002
|
+
this.initialized = true;
|
|
3003
|
+
const id = params.get('id');
|
|
3004
|
+
if (id && id !== 'new') {
|
|
3005
|
+
this.loadItem(id);
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Handle form submission.
|
|
3011
|
+
* Validates the form, then calls createItem or updateItem based on mode.
|
|
3012
|
+
* Shows appropriate toast messages and navigates back on success.
|
|
3013
|
+
*/
|
|
3014
|
+
onSubmit() {
|
|
3015
|
+
if (!this.isFormValid()) {
|
|
3016
|
+
this.showValidationError();
|
|
3017
|
+
return;
|
|
2896
3018
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
3019
|
+
this.isLoading.set(true);
|
|
3020
|
+
const model = this.getFormModel()();
|
|
3021
|
+
const operation$ = this.isEditMode()
|
|
3022
|
+
? this.updateItem(model)
|
|
3023
|
+
: this.createItem(model);
|
|
3024
|
+
operation$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
3025
|
+
next: () => {
|
|
3026
|
+
const action = this.isEditMode() ? 'updated' : 'created';
|
|
3027
|
+
this.showSuccess(`${this.getResourceName()} ${action} successfully.`);
|
|
3028
|
+
this.router.navigate([this.getResourceRoute()]);
|
|
3029
|
+
},
|
|
3030
|
+
error: () => {
|
|
3031
|
+
this.isLoading.set(false);
|
|
3032
|
+
},
|
|
3033
|
+
complete: () => {
|
|
3034
|
+
this.isLoading.set(false);
|
|
3035
|
+
},
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Handle cancel action.
|
|
3040
|
+
* Navigates back to the resource list.
|
|
3041
|
+
*/
|
|
3042
|
+
onCancel() {
|
|
3043
|
+
this.router.navigate([this.getResourceRoute()]);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Show validation error toast.
|
|
3047
|
+
* Override to customize the validation error message.
|
|
3048
|
+
*/
|
|
3049
|
+
showValidationError() {
|
|
3050
|
+
this.messageService.add({
|
|
3051
|
+
severity: 'error',
|
|
3052
|
+
summary: 'Validation Error',
|
|
3053
|
+
detail: 'Please fill in all required fields.',
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Show success toast.
|
|
3058
|
+
* @param detail The success message to display
|
|
3059
|
+
*/
|
|
3060
|
+
showSuccess(detail) {
|
|
3061
|
+
this.messageService.add({
|
|
3062
|
+
severity: 'success',
|
|
3063
|
+
summary: 'Success',
|
|
3064
|
+
detail,
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Show error toast.
|
|
3069
|
+
* @param detail The error message to display
|
|
3070
|
+
*/
|
|
3071
|
+
showError(detail) {
|
|
3072
|
+
this.messageService.add({
|
|
3073
|
+
severity: 'error',
|
|
3074
|
+
summary: 'Error',
|
|
3075
|
+
detail,
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
3079
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: BaseFormPage, isStandalone: true, ngImport: i0 });
|
|
3080
|
+
}
|
|
3081
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormPage, decorators: [{
|
|
3082
|
+
type: Directive
|
|
3083
|
+
}], ctorParameters: () => [] });
|
|
3084
|
+
|
|
3085
|
+
/**
|
|
3086
|
+
* Base List Page
|
|
3087
|
+
* Abstract directive providing common signals, computed values, and utilities
|
|
3088
|
+
* for list page components across all feature packages.
|
|
3089
|
+
*
|
|
3090
|
+
* Features:
|
|
3091
|
+
* - Pagination state management (pageSize, currentPage, total)
|
|
3092
|
+
* - Loading state
|
|
3093
|
+
* - CRUD navigation helpers
|
|
3094
|
+
* - Message display utilities
|
|
3095
|
+
* - Delete confirmation with API integration
|
|
3096
|
+
* - Company feature flag support
|
|
3097
|
+
*
|
|
3098
|
+
* Usage:
|
|
3099
|
+
* ```typescript
|
|
3100
|
+
* @Component({ ... })
|
|
3101
|
+
* export class UserListComponent extends BaseListPage<IUser> {
|
|
3102
|
+
* getResourceRoute(): string { return '/users'; }
|
|
3103
|
+
* getDeleteConfirmMessage(user: IUser): string { return `Delete "${user.name}"?`; }
|
|
3104
|
+
* async loadData(): Promise<void> { ... }
|
|
3105
|
+
* }
|
|
3106
|
+
* ```
|
|
3107
|
+
*/
|
|
3108
|
+
class BaseListPage {
|
|
3109
|
+
router = inject(Router);
|
|
3110
|
+
messageService = inject(MessageService);
|
|
3111
|
+
appConfig = inject(APP_CONFIG);
|
|
3112
|
+
confirmationService = inject(ConfirmationService);
|
|
3113
|
+
destroyRef = inject(DestroyRef);
|
|
3114
|
+
/** Items list */
|
|
3115
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
3116
|
+
/** Loading state */
|
|
3117
|
+
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
3118
|
+
/** Total records for pagination */
|
|
3119
|
+
total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
3120
|
+
/** Page size */
|
|
3121
|
+
pageSize = signal(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
|
|
3122
|
+
/** First record index (for p-table lazy load) */
|
|
3123
|
+
first = signal(0, ...(ngDevMode ? [{ debugName: "first" }] : []));
|
|
3124
|
+
/** Current page (0-based for API, derived from first/pageSize) */
|
|
3125
|
+
currentPage = computed(() => Math.floor(this.first() / this.pageSize()), ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
|
|
3126
|
+
/** Show company info if company feature enabled */
|
|
3127
|
+
showCompanyInfo = computed(() => this.appConfig.enableCompanyFeature, ...(ngDevMode ? [{ debugName: "showCompanyInfo" }] : []));
|
|
3128
|
+
/**
|
|
3129
|
+
* Navigate to create page
|
|
3130
|
+
*/
|
|
3131
|
+
onCreate() {
|
|
3132
|
+
this.router.navigate([this.getResourceRoute(), 'new']);
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Navigate to edit page
|
|
3136
|
+
* @param id The ID of the item to edit
|
|
3137
|
+
*/
|
|
3138
|
+
onEdit(id) {
|
|
3139
|
+
this.router.navigate([this.getResourceRoute(), id]);
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Handle page change from p-table lazy load
|
|
3143
|
+
*/
|
|
3144
|
+
onPageChange(event) {
|
|
3145
|
+
this.first.set(event.first ?? 0);
|
|
3146
|
+
this.pageSize.set(event.rows ?? 10);
|
|
3147
|
+
this.loadData();
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Get pagination params for API call
|
|
3151
|
+
* Returns 0-based page for backend API
|
|
3152
|
+
*/
|
|
3153
|
+
getPaginationParams() {
|
|
3154
|
+
return {
|
|
3155
|
+
currentPage: this.currentPage(),
|
|
3156
|
+
pageSize: this.pageSize(),
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Show success toast message
|
|
3161
|
+
*/
|
|
3162
|
+
showSuccess(detail, summary = 'Success') {
|
|
3163
|
+
this.messageService.add({ severity: 'success', summary, detail });
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Show error toast message
|
|
3167
|
+
*/
|
|
3168
|
+
showError(detail, summary = 'Error') {
|
|
3169
|
+
this.messageService.add({ severity: 'error', summary, detail });
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Show info toast message
|
|
3173
|
+
*/
|
|
3174
|
+
showInfo(detail, summary = 'Info') {
|
|
3175
|
+
this.messageService.add({ severity: 'info', summary, detail });
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Show warning toast message
|
|
3179
|
+
*/
|
|
3180
|
+
showWarn(detail, summary = 'Warning') {
|
|
3181
|
+
this.messageService.add({ severity: 'warn', summary, detail });
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Delete an item with confirmation dialog
|
|
3185
|
+
* @param item The item to delete
|
|
3186
|
+
* @param idGetter Function to extract ID from item
|
|
3187
|
+
* @param deleteApiCall Function that returns Observable for delete API call
|
|
3188
|
+
* @param options Optional configuration
|
|
3189
|
+
*/
|
|
3190
|
+
onDelete(item, idGetter, deleteApiCall, options) {
|
|
3191
|
+
this.confirmationService.confirm({
|
|
3192
|
+
message: this.getDeleteConfirmMessage(item),
|
|
3193
|
+
header: options?.header ?? 'Confirm Delete',
|
|
3194
|
+
icon: 'pi pi-exclamation-triangle',
|
|
3195
|
+
acceptButtonStyleClass: 'p-button-danger',
|
|
3196
|
+
accept: () => {
|
|
3197
|
+
deleteApiCall(idGetter(item))
|
|
3198
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
3199
|
+
.subscribe({
|
|
3200
|
+
next: () => {
|
|
3201
|
+
this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
|
|
3202
|
+
this.loadData();
|
|
3203
|
+
},
|
|
3204
|
+
error: () => {
|
|
3205
|
+
this.showError(options?.errorMessage ?? 'Failed to delete item.');
|
|
3206
|
+
},
|
|
3207
|
+
});
|
|
3208
|
+
},
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Delete an item with confirmation dialog using async/await
|
|
3213
|
+
* @param item The item to delete
|
|
3214
|
+
* @param idGetter Function to extract ID from item
|
|
3215
|
+
* @param deleteApiCall Async function for delete API call
|
|
3216
|
+
* @param options Optional configuration
|
|
3217
|
+
*/
|
|
3218
|
+
async onDeleteAsync(item, idGetter, deleteApiCall, options) {
|
|
3219
|
+
this.confirmationService.confirm({
|
|
3220
|
+
message: this.getDeleteConfirmMessage(item),
|
|
3221
|
+
header: options?.header ?? 'Confirm Delete',
|
|
3222
|
+
icon: 'pi pi-exclamation-triangle',
|
|
3223
|
+
acceptButtonStyleClass: 'p-button-danger',
|
|
3224
|
+
accept: async () => {
|
|
3225
|
+
try {
|
|
3226
|
+
await deleteApiCall(idGetter(item));
|
|
3227
|
+
this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
|
|
3228
|
+
await this.loadData();
|
|
3229
|
+
}
|
|
3230
|
+
catch {
|
|
3231
|
+
this.showError(options?.errorMessage ?? 'Failed to delete item.');
|
|
3232
|
+
}
|
|
3233
|
+
},
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* Navigate to a route
|
|
3238
|
+
*/
|
|
3239
|
+
navigateTo(path) {
|
|
3240
|
+
this.router.navigate(path);
|
|
3241
|
+
}
|
|
3242
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
3243
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: BaseListPage, isStandalone: true, ngImport: i0 });
|
|
2899
3244
|
}
|
|
3245
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, decorators: [{
|
|
3246
|
+
type: Directive
|
|
3247
|
+
}] });
|
|
2900
3248
|
|
|
2901
|
-
//
|
|
3249
|
+
// Constants
|
|
2902
3250
|
|
|
2903
3251
|
/**
|
|
2904
3252
|
* Generated bundle index. Do not edit.
|
|
2905
3253
|
*/
|
|
2906
3254
|
|
|
2907
|
-
export { AUTH_STATE_PROVIDER, AngularModule, ApiResourceService, ApiResourceService as ApiService, COMPANY_API_PROVIDER, ContactTypeEnum, CookieService, EditModeElementChangerDirective, FILE_TYPE_FILTERS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, PROFILE_PERMISSION_PROVIDER, PROFILE_UPLOAD_PROVIDER, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, USER_LIST_PROVIDER, USER_PERMISSION_PROVIDER, USER_PROVIDER, UserMultiSelectComponent, UserSelectComponent, allPermissionsGuard, anyPermissionGuard, evaluateLogicNode, evaluatePermission, formatFileSize, getAcceptString, getFileIconClass, hasAllPermissions, hasAnyPermission, isFileTypeAllowed, permissionGuard };
|
|
3255
|
+
export { ACTION_PERMISSIONS, AUTH_STATE_PROVIDER, AngularModule, ApiResourceService, ApiResourceService as ApiService, BRANCH_PERMISSIONS, BaseFormControl, BaseFormPage, BaseListPage, BaseUserSelectComponent, COMPANY_ACTION_PERMISSIONS, COMPANY_API_PROVIDER, COMPANY_PERMISSIONS, ContactTypeEnum, CookieService, EMAIL_CONFIG_PERMISSIONS, EMAIL_TEMPLATE_PERMISSIONS, EditModeElementChangerDirective, FILE_PERMISSIONS, FILE_TYPE_FILTERS, FOLDER_PERMISSIONS, FORM_PERMISSIONS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, PERMISSIONS, PROFILE_PERMISSION_PROVIDER, PROFILE_UPLOAD_PROVIDER, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, ROLE_ACTION_PERMISSIONS, ROLE_PERMISSIONS, STORAGE_CONFIG_PERMISSIONS, USER_ACTION_PERMISSIONS, USER_LIST_PROVIDER, USER_PERMISSIONS, USER_PERMISSION_PROVIDER, USER_PROVIDER, USER_ROLE_PERMISSIONS, UserMultiSelectComponent, UserSelectComponent, allPermissionsGuard, anyPermissionGuard, checkScrollPagination, evaluateLogicNode, evaluatePermission, formatFileSize, getAcceptString, getFileIconClass, hasAllPermissions, hasAnyPermission, hasPermission, isFileTypeAllowed, permissionGuard, provideValueAccessor };
|
|
2908
3256
|
//# sourceMappingURL=flusys-ng-shared.mjs.map
|