@flusys/ng-shared 1.0.0-beta → 1.0.0
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 +411 -2
- package/fesm2022/flusys-ng-shared.mjs +1262 -839
- package/fesm2022/flusys-ng-shared.mjs.map +1 -1
- package/package.json +8 -8
- package/types/flusys-ng-shared.d.ts +968 -475
|
@@ -1,22 +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,
|
|
8
|
-
import { tap, catchError
|
|
9
|
-
import * as i1$
|
|
6
|
+
import { APP_CONFIG, getServiceUrl } from '@flusys/ng-core';
|
|
7
|
+
import { of, firstValueFrom, map as map$1 } from 'rxjs';
|
|
8
|
+
import { map, tap, catchError } from 'rxjs/operators';
|
|
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
|
-
import
|
|
13
|
+
import { AvatarModule } from 'primeng/avatar';
|
|
14
|
+
import * as i1$2 from 'primeng/button';
|
|
14
15
|
import { ButtonModule } from 'primeng/button';
|
|
15
16
|
import { CardModule } from 'primeng/card';
|
|
16
|
-
import * as
|
|
17
|
+
import * as i1 from 'primeng/checkbox';
|
|
17
18
|
import { CheckboxModule } from 'primeng/checkbox';
|
|
19
|
+
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
|
18
20
|
import { DatePickerModule } from 'primeng/datepicker';
|
|
19
|
-
import * as
|
|
21
|
+
import * as i4 from 'primeng/dialog';
|
|
20
22
|
import { DialogModule } from 'primeng/dialog';
|
|
21
23
|
import { DividerModule } from 'primeng/divider';
|
|
22
24
|
import { FileUploadModule } from 'primeng/fileupload';
|
|
@@ -24,7 +26,7 @@ import { IconFieldModule } from 'primeng/iconfield';
|
|
|
24
26
|
import { ImageModule } from 'primeng/image';
|
|
25
27
|
import { InputIconModule } from 'primeng/inputicon';
|
|
26
28
|
import { InputNumberModule } from 'primeng/inputnumber';
|
|
27
|
-
import * as
|
|
29
|
+
import * as i2 from 'primeng/inputtext';
|
|
28
30
|
import { InputTextModule } from 'primeng/inputtext';
|
|
29
31
|
import { ListboxModule } from 'primeng/listbox';
|
|
30
32
|
import { MultiSelectModule } from 'primeng/multiselect';
|
|
@@ -36,24 +38,146 @@ import * as i2$1 from 'primeng/progressbar';
|
|
|
36
38
|
import { ProgressBarModule } from 'primeng/progressbar';
|
|
37
39
|
import { RadioButtonModule } from 'primeng/radiobutton';
|
|
38
40
|
import { RippleModule } from 'primeng/ripple';
|
|
39
|
-
import * as i3 from 'primeng/select';
|
|
41
|
+
import * as i3$1 from 'primeng/select';
|
|
40
42
|
import { SelectModule } from 'primeng/select';
|
|
41
43
|
import { SelectButtonModule } from 'primeng/selectbutton';
|
|
44
|
+
import { SkeletonModule } from 'primeng/skeleton';
|
|
42
45
|
import { SplitButtonModule } from 'primeng/splitbutton';
|
|
43
46
|
import { StepsModule } from 'primeng/steps';
|
|
44
47
|
import { TableModule } from 'primeng/table';
|
|
45
48
|
import { TabsModule } from 'primeng/tabs';
|
|
46
49
|
import { TagModule } from 'primeng/tag';
|
|
47
50
|
import { TextareaModule } from 'primeng/textarea';
|
|
51
|
+
import { ToastModule } from 'primeng/toast';
|
|
48
52
|
import { ToggleSwitchModule } from 'primeng/toggleswitch';
|
|
49
53
|
import { TooltipModule } from 'primeng/tooltip';
|
|
50
54
|
import { TreeTableModule } from 'primeng/treetable';
|
|
51
|
-
import {
|
|
52
|
-
import
|
|
53
|
-
import { MessageService } from 'primeng/api';
|
|
55
|
+
import { MessageService, ConfirmationService } from 'primeng/api';
|
|
56
|
+
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
};
|
|
57
181
|
|
|
58
182
|
/**
|
|
59
183
|
* Common file type filters
|
|
@@ -147,9 +271,7 @@ class PlatformService {
|
|
|
147
271
|
}
|
|
148
272
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, decorators: [{
|
|
149
273
|
type: Injectable,
|
|
150
|
-
args: [{
|
|
151
|
-
providedIn: 'root'
|
|
152
|
-
}]
|
|
274
|
+
args: [{ providedIn: 'root' }]
|
|
153
275
|
}] });
|
|
154
276
|
|
|
155
277
|
class CookieService {
|
|
@@ -171,67 +293,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
171
293
|
|
|
172
294
|
/**
|
|
173
295
|
* Service to fetch file URLs from the backend.
|
|
174
|
-
*
|
|
175
|
-
* - Handles presigned URLs for cloud storage (AWS S3, Azure)
|
|
176
|
-
* - Auto-refreshes expired URLs
|
|
177
|
-
* - Validates file access permissions
|
|
178
|
-
* - Works with all storage providers (Local, S3, Azure, SFTP)
|
|
296
|
+
* Handles presigned URLs for cloud storage, auto-refresh, and caching.
|
|
179
297
|
*/
|
|
180
298
|
class FileUrlService {
|
|
181
299
|
http = inject(HttpClient);
|
|
182
300
|
appConfig = inject(APP_CONFIG);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get file URL by ID from cache (reactive signal)
|
|
187
|
-
*/
|
|
301
|
+
_cache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_cache" }] : []));
|
|
302
|
+
cache = this._cache.asReadonly();
|
|
303
|
+
/** Get file URL by ID from cache (synchronous) */
|
|
188
304
|
getFileUrl(fileId) {
|
|
189
305
|
if (!fileId)
|
|
190
306
|
return null;
|
|
191
|
-
return this.
|
|
307
|
+
return this._cache().get(fileId)?.url ?? null;
|
|
192
308
|
}
|
|
193
|
-
/**
|
|
194
|
-
* Get file URL signal (computed from cache)
|
|
195
|
-
*/
|
|
309
|
+
/** Get file URL as computed signal */
|
|
196
310
|
fileUrlSignal(fileId) {
|
|
197
311
|
return computed(() => this.getFileUrl(fileId));
|
|
198
312
|
}
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
* Returns Observable of fetched files.
|
|
202
|
-
*/
|
|
203
|
-
fetchFileUrls(fileIds) {
|
|
313
|
+
/** Fetch file URLs from backend and update cache */
|
|
314
|
+
fetchFileUrls(fileIds, forceRefresh = false) {
|
|
204
315
|
if (!fileIds.length)
|
|
205
316
|
return of([]);
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
* Useful on logout or when switching contexts.
|
|
224
|
-
*/
|
|
317
|
+
const cache = this._cache();
|
|
318
|
+
const missingIds = forceRefresh
|
|
319
|
+
? fileIds
|
|
320
|
+
: fileIds.filter((id) => !cache.has(id));
|
|
321
|
+
if (!missingIds.length) {
|
|
322
|
+
return of(this.getFromCache(fileIds));
|
|
323
|
+
}
|
|
324
|
+
const requestDto = missingIds.map((id) => ({ id }));
|
|
325
|
+
return this.http
|
|
326
|
+
.post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto)
|
|
327
|
+
.pipe(map((response) => response.data ?? []), tap((files) => this.addToCache(files)), map(() => this.getFromCache(fileIds)), catchError(() => of([])));
|
|
328
|
+
}
|
|
329
|
+
/** Fetch single file URL (delegates to fetchFileUrls) */
|
|
330
|
+
fetchSingleFileUrl(fileId, forceRefresh = false) {
|
|
331
|
+
return this.fetchFileUrls([fileId], forceRefresh).pipe(map((files) => files[0] ?? null));
|
|
332
|
+
}
|
|
333
|
+
/** Clear entire cache */
|
|
225
334
|
clearCache() {
|
|
226
|
-
this.
|
|
335
|
+
this._cache.set(new Map());
|
|
227
336
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Remove specific file from cache.
|
|
230
|
-
*/
|
|
337
|
+
/** Remove specific file from cache */
|
|
231
338
|
removeFromCache(fileId) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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);
|
|
235
359
|
}
|
|
236
360
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
237
361
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, providedIn: 'root' });
|
|
@@ -242,79 +366,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
242
366
|
}] });
|
|
243
367
|
|
|
244
368
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
274
431
|
*/
|
|
275
432
|
class PermissionValidatorService {
|
|
276
|
-
// ==================== SIGNALS ====================
|
|
277
|
-
/**
|
|
278
|
-
* Private writable signal for permissions
|
|
279
|
-
*/
|
|
280
433
|
_permissions = signal([], ...(ngDevMode ? [{ debugName: "_permissions" }] : []));
|
|
281
|
-
/**
|
|
282
|
-
* Readonly permission signal for external consumers
|
|
283
|
-
*/
|
|
284
434
|
permissions = this._permissions.asReadonly();
|
|
285
|
-
/**
|
|
286
|
-
* Private writable signal for loaded state
|
|
287
|
-
*/
|
|
288
435
|
_isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "_isLoaded" }] : []));
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
* @param permissions - Array of permission codes
|
|
292
|
-
*/
|
|
436
|
+
isLoaded = this._isLoaded.asReadonly();
|
|
437
|
+
/** Set permissions (replaces existing) */
|
|
293
438
|
setPermissions(permissions) {
|
|
294
439
|
this._permissions.set(permissions);
|
|
295
440
|
this._isLoaded.set(true);
|
|
296
441
|
}
|
|
297
|
-
/**
|
|
298
|
-
* Clear all permissions
|
|
299
|
-
*/
|
|
442
|
+
/** Clear all permissions */
|
|
300
443
|
clearPermissions() {
|
|
301
444
|
this._permissions.set([]);
|
|
302
445
|
this._isLoaded.set(false);
|
|
303
446
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
* @param permissionCode - Required permission code
|
|
308
|
-
* @returns True if user has permission
|
|
309
|
-
*/
|
|
310
|
-
hasPermission(permissionCode) {
|
|
311
|
-
return this.permissions().includes(permissionCode);
|
|
447
|
+
/** Check if user has permission (supports wildcards: '*', 'module.*') */
|
|
448
|
+
hasPermission(code) {
|
|
449
|
+
return hasPermission(code, this._permissions());
|
|
312
450
|
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Check if permissions are loaded
|
|
316
|
-
* @returns True if permissions have been loaded
|
|
317
|
-
*/
|
|
451
|
+
/** @deprecated Use `isLoaded()` signal instead */
|
|
318
452
|
isPermissionsLoaded() {
|
|
319
453
|
return this._isLoaded();
|
|
320
454
|
}
|
|
@@ -323,9 +457,7 @@ class PermissionValidatorService {
|
|
|
323
457
|
}
|
|
324
458
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionValidatorService, decorators: [{
|
|
325
459
|
type: Injectable,
|
|
326
|
-
args: [{
|
|
327
|
-
providedIn: 'root',
|
|
328
|
-
}]
|
|
460
|
+
args: [{ providedIn: 'root' }]
|
|
329
461
|
}] });
|
|
330
462
|
|
|
331
463
|
class EditModeElementChangerDirective {
|
|
@@ -388,46 +520,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
388
520
|
type: Directive,
|
|
389
521
|
args: [{
|
|
390
522
|
selector: '[appEditModeElementChanger]',
|
|
391
|
-
standalone: true,
|
|
392
523
|
}]
|
|
393
524
|
}], ctorParameters: () => [], propDecorators: { isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }] } });
|
|
394
525
|
|
|
395
|
-
/** Evaluate permission logic (string or ILogicNode) against user permissions */
|
|
396
|
-
function evaluatePermission(logic, permissions) {
|
|
397
|
-
if (!logic)
|
|
398
|
-
return false;
|
|
399
|
-
if (typeof logic === 'string')
|
|
400
|
-
return permissions.includes(logic);
|
|
401
|
-
return evaluateLogicNode(logic, permissions);
|
|
402
|
-
}
|
|
403
|
-
/** Recursively evaluate an ILogicNode tree */
|
|
404
|
-
function evaluateLogicNode(node, permissions) {
|
|
405
|
-
switch (node.type) {
|
|
406
|
-
case 'action':
|
|
407
|
-
return node.actionId ? permissions.includes(node.actionId) : false;
|
|
408
|
-
case 'group':
|
|
409
|
-
if (!node.children || node.children.length === 0)
|
|
410
|
-
return false;
|
|
411
|
-
return node.operator === 'AND'
|
|
412
|
-
? node.children.every((child) => evaluateLogicNode(child, permissions))
|
|
413
|
-
: node.children.some((child) => evaluateLogicNode(child, permissions));
|
|
414
|
-
default:
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
/** Check if user has ANY of the specified permissions (OR logic) */
|
|
419
|
-
function hasAnyPermission(permissionCodes, permissions) {
|
|
420
|
-
if (!permissionCodes?.length)
|
|
421
|
-
return false;
|
|
422
|
-
return permissionCodes.some((code) => permissions.includes(code));
|
|
423
|
-
}
|
|
424
|
-
/** Check if user has ALL of the specified permissions (AND logic) */
|
|
425
|
-
function hasAllPermissions(permissionCodes, permissions) {
|
|
426
|
-
if (!permissionCodes?.length)
|
|
427
|
-
return false;
|
|
428
|
-
return permissionCodes.every((code) => permissions.includes(code));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
526
|
/**
|
|
432
527
|
* HasPermission Directive
|
|
433
528
|
*
|
|
@@ -548,7 +643,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
548
643
|
type: Directive,
|
|
549
644
|
args: [{
|
|
550
645
|
selector: '[hasPermission]',
|
|
551
|
-
standalone: true,
|
|
552
646
|
}]
|
|
553
647
|
}], ctorParameters: () => [], propDecorators: { hasPermission: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPermission", required: false }] }] } });
|
|
554
648
|
|
|
@@ -577,7 +671,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
577
671
|
'[src]': 'imageSrc()',
|
|
578
672
|
'(error)': 'onError()',
|
|
579
673
|
},
|
|
580
|
-
standalone: true,
|
|
581
674
|
}]
|
|
582
675
|
}], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }] } });
|
|
583
676
|
|
|
@@ -615,18 +708,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
615
708
|
type: Directive,
|
|
616
709
|
args: [{
|
|
617
710
|
selector: '[appPreventDefault]',
|
|
618
|
-
|
|
711
|
+
host: {
|
|
712
|
+
'(click)': 'onClick($event)',
|
|
713
|
+
'(keydown)': 'onKeydown($event)',
|
|
714
|
+
'(keyup)': 'onKeyup($event)',
|
|
715
|
+
},
|
|
619
716
|
}]
|
|
620
|
-
}], 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"] }]
|
|
621
|
-
type: HostListener,
|
|
622
|
-
args: ['click', ['$event']]
|
|
623
|
-
}], onKeydown: [{
|
|
624
|
-
type: HostListener,
|
|
625
|
-
args: ['keydown', ['$event']]
|
|
626
|
-
}], onKeyup: [{
|
|
627
|
-
type: HostListener,
|
|
628
|
-
args: ['keyup', ['$event']]
|
|
629
|
-
}] } });
|
|
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"] }] } });
|
|
630
718
|
|
|
631
719
|
class AngularModule {
|
|
632
720
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
@@ -635,6 +723,7 @@ class AngularModule {
|
|
|
635
723
|
ReactiveFormsModule,
|
|
636
724
|
RouterOutlet,
|
|
637
725
|
RouterLink,
|
|
726
|
+
RouterLinkActive,
|
|
638
727
|
IsEmptyImageDirective,
|
|
639
728
|
NgOptimizedImage,
|
|
640
729
|
NgComponentOutlet,
|
|
@@ -643,6 +732,7 @@ class AngularModule {
|
|
|
643
732
|
ReactiveFormsModule,
|
|
644
733
|
RouterOutlet,
|
|
645
734
|
RouterLink,
|
|
735
|
+
RouterLinkActive,
|
|
646
736
|
IsEmptyImageDirective,
|
|
647
737
|
NgOptimizedImage,
|
|
648
738
|
NgComponentOutlet,
|
|
@@ -662,6 +752,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
662
752
|
ReactiveFormsModule,
|
|
663
753
|
RouterOutlet,
|
|
664
754
|
RouterLink,
|
|
755
|
+
RouterLinkActive,
|
|
665
756
|
IsEmptyImageDirective,
|
|
666
757
|
NgOptimizedImage,
|
|
667
758
|
NgComponentOutlet,
|
|
@@ -674,6 +765,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
674
765
|
ReactiveFormsModule,
|
|
675
766
|
RouterOutlet,
|
|
676
767
|
RouterLink,
|
|
768
|
+
RouterLinkActive,
|
|
677
769
|
IsEmptyImageDirective,
|
|
678
770
|
NgOptimizedImage,
|
|
679
771
|
NgComponentOutlet,
|
|
@@ -684,109 +776,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
684
776
|
|
|
685
777
|
class PrimeModule {
|
|
686
778
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
687
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, exports: [
|
|
688
|
-
|
|
689
|
-
SelectButtonModule,
|
|
690
|
-
PasswordModule,
|
|
779
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, exports: [AutoCompleteModule,
|
|
780
|
+
AvatarModule,
|
|
691
781
|
ButtonModule,
|
|
692
|
-
|
|
782
|
+
CardModule,
|
|
693
783
|
CheckboxModule,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
TableModule,
|
|
699
|
-
InputNumberModule,
|
|
700
|
-
TextareaModule,
|
|
701
|
-
ProgressBarModule,
|
|
784
|
+
ConfirmDialogModule,
|
|
785
|
+
DatePickerModule,
|
|
786
|
+
DialogModule,
|
|
787
|
+
DividerModule,
|
|
702
788
|
FileUploadModule,
|
|
703
|
-
CardModule,
|
|
704
|
-
SelectModule,
|
|
705
|
-
InputIconModule,
|
|
706
789
|
IconFieldModule,
|
|
707
|
-
|
|
790
|
+
ImageModule,
|
|
791
|
+
InputIconModule,
|
|
792
|
+
InputNumberModule,
|
|
793
|
+
InputTextModule,
|
|
708
794
|
ListboxModule,
|
|
795
|
+
MultiSelectModule,
|
|
796
|
+
PaginatorModule,
|
|
797
|
+
PanelModule,
|
|
798
|
+
PasswordModule,
|
|
799
|
+
PopoverModule,
|
|
800
|
+
ProgressBarModule,
|
|
709
801
|
RadioButtonModule,
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
802
|
+
RippleModule,
|
|
803
|
+
SelectButtonModule,
|
|
804
|
+
SelectModule,
|
|
805
|
+
SkeletonModule,
|
|
713
806
|
SplitButtonModule,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
AutoCompleteModule,
|
|
807
|
+
StepsModule,
|
|
808
|
+
TableModule,
|
|
717
809
|
TabsModule,
|
|
718
|
-
DialogModule,
|
|
719
|
-
TreeTableModule] });
|
|
720
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, imports: [InputTextModule,
|
|
721
810
|
TagModule,
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
811
|
+
TextareaModule,
|
|
812
|
+
ToastModule,
|
|
813
|
+
ToggleSwitchModule,
|
|
725
814
|
TooltipModule,
|
|
815
|
+
TreeTableModule] });
|
|
816
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, imports: [AutoCompleteModule,
|
|
817
|
+
AvatarModule,
|
|
818
|
+
ButtonModule,
|
|
819
|
+
CardModule,
|
|
726
820
|
CheckboxModule,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
TableModule,
|
|
732
|
-
InputNumberModule,
|
|
733
|
-
TextareaModule,
|
|
734
|
-
ProgressBarModule,
|
|
821
|
+
ConfirmDialogModule,
|
|
822
|
+
DatePickerModule,
|
|
823
|
+
DialogModule,
|
|
824
|
+
DividerModule,
|
|
735
825
|
FileUploadModule,
|
|
736
|
-
CardModule,
|
|
737
|
-
SelectModule,
|
|
738
|
-
InputIconModule,
|
|
739
826
|
IconFieldModule,
|
|
740
|
-
|
|
827
|
+
ImageModule,
|
|
828
|
+
InputIconModule,
|
|
829
|
+
InputNumberModule,
|
|
830
|
+
InputTextModule,
|
|
741
831
|
ListboxModule,
|
|
832
|
+
MultiSelectModule,
|
|
833
|
+
PaginatorModule,
|
|
834
|
+
PanelModule,
|
|
835
|
+
PasswordModule,
|
|
836
|
+
PopoverModule,
|
|
837
|
+
ProgressBarModule,
|
|
742
838
|
RadioButtonModule,
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
839
|
+
RippleModule,
|
|
840
|
+
SelectButtonModule,
|
|
841
|
+
SelectModule,
|
|
842
|
+
SkeletonModule,
|
|
746
843
|
SplitButtonModule,
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
AutoCompleteModule,
|
|
844
|
+
StepsModule,
|
|
845
|
+
TableModule,
|
|
750
846
|
TabsModule,
|
|
751
|
-
|
|
847
|
+
TagModule,
|
|
848
|
+
TextareaModule,
|
|
849
|
+
ToastModule,
|
|
850
|
+
ToggleSwitchModule,
|
|
851
|
+
TooltipModule,
|
|
752
852
|
TreeTableModule] });
|
|
753
853
|
}
|
|
754
854
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, decorators: [{
|
|
755
855
|
type: NgModule,
|
|
756
856
|
args: [{
|
|
757
857
|
exports: [
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
SelectButtonModule,
|
|
761
|
-
PasswordModule,
|
|
858
|
+
AutoCompleteModule,
|
|
859
|
+
AvatarModule,
|
|
762
860
|
ButtonModule,
|
|
763
|
-
TooltipModule,
|
|
764
|
-
CheckboxModule,
|
|
765
|
-
StepsModule,
|
|
766
|
-
RippleModule,
|
|
767
|
-
PanelModule,
|
|
768
|
-
PaginatorModule,
|
|
769
|
-
TableModule,
|
|
770
|
-
InputNumberModule,
|
|
771
|
-
TextareaModule,
|
|
772
|
-
ProgressBarModule,
|
|
773
|
-
FileUploadModule,
|
|
774
861
|
CardModule,
|
|
775
|
-
|
|
776
|
-
|
|
862
|
+
CheckboxModule,
|
|
863
|
+
ConfirmDialogModule,
|
|
864
|
+
DatePickerModule,
|
|
865
|
+
DialogModule,
|
|
866
|
+
DividerModule,
|
|
867
|
+
FileUploadModule,
|
|
777
868
|
IconFieldModule,
|
|
778
|
-
|
|
869
|
+
ImageModule,
|
|
870
|
+
InputIconModule,
|
|
871
|
+
InputNumberModule,
|
|
872
|
+
InputTextModule,
|
|
779
873
|
ListboxModule,
|
|
874
|
+
MultiSelectModule,
|
|
875
|
+
PaginatorModule,
|
|
876
|
+
PanelModule,
|
|
877
|
+
PasswordModule,
|
|
878
|
+
PopoverModule,
|
|
879
|
+
ProgressBarModule,
|
|
780
880
|
RadioButtonModule,
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
881
|
+
RippleModule,
|
|
882
|
+
SelectButtonModule,
|
|
883
|
+
SelectModule,
|
|
884
|
+
SkeletonModule,
|
|
784
885
|
SplitButtonModule,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
AutoCompleteModule,
|
|
886
|
+
StepsModule,
|
|
887
|
+
TableModule,
|
|
788
888
|
TabsModule,
|
|
789
|
-
|
|
889
|
+
TagModule,
|
|
890
|
+
TextareaModule,
|
|
891
|
+
ToastModule,
|
|
892
|
+
ToggleSwitchModule,
|
|
893
|
+
TooltipModule,
|
|
790
894
|
TreeTableModule,
|
|
791
895
|
],
|
|
792
896
|
}]
|
|
@@ -838,7 +942,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
838
942
|
*/
|
|
839
943
|
class ApiResourceService {
|
|
840
944
|
baseUrl;
|
|
841
|
-
loaderService = inject(ApiLoaderService);
|
|
842
945
|
injector = inject(Injector);
|
|
843
946
|
http;
|
|
844
947
|
moduleApiName;
|
|
@@ -861,6 +964,12 @@ class ApiResourceService {
|
|
|
861
964
|
_listResource = null;
|
|
862
965
|
/** Whether the list resource has been initialized */
|
|
863
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" }] : []));
|
|
864
973
|
/** Get or create the list resource (lazy initialization) */
|
|
865
974
|
get listResource() {
|
|
866
975
|
if (!this._listResource) {
|
|
@@ -887,20 +996,50 @@ class ApiResourceService {
|
|
|
887
996
|
return this.fetchAllAsync(search, filter);
|
|
888
997
|
} });
|
|
889
998
|
});
|
|
999
|
+
// Signal that resource is now initialized - triggers computed re-evaluation
|
|
1000
|
+
this._resourceInitSignal.set(true);
|
|
890
1001
|
}
|
|
891
1002
|
// ==========================================================================
|
|
892
1003
|
// Computed State Accessors
|
|
893
1004
|
// ==========================================================================
|
|
894
|
-
/**
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
/**
|
|
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
|
+
*/
|
|
903
1041
|
hasMore = computed(() => {
|
|
1042
|
+
this._resourceInitSignal(); // Track initialization
|
|
904
1043
|
const meta = this._listResource?.value()?.meta;
|
|
905
1044
|
if (!meta)
|
|
906
1045
|
return false;
|
|
@@ -1075,38 +1214,6 @@ class ApiResourceService {
|
|
|
1075
1214
|
}
|
|
1076
1215
|
}
|
|
1077
1216
|
|
|
1078
|
-
class IconComponent {
|
|
1079
|
-
icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
1080
|
-
iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
|
|
1081
|
-
IconTypeEnum = IconTypeEnum; // Needed for template reference
|
|
1082
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1083
|
-
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: `
|
|
1084
|
-
@if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
|
|
1085
|
-
<i [ngClass]="icon()"></i>
|
|
1086
|
-
}@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
|
|
1087
|
-
<img [alt]="icon()" [src]="icon()" />
|
|
1088
|
-
}@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
|
|
1089
|
-
{{ icon() }}
|
|
1090
|
-
}@else{ I } }
|
|
1091
|
-
`, 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"] }] });
|
|
1092
|
-
}
|
|
1093
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
|
|
1094
|
-
type: Component,
|
|
1095
|
-
args: [{
|
|
1096
|
-
selector: 'lib-icon',
|
|
1097
|
-
imports: [AngularModule],
|
|
1098
|
-
template: `
|
|
1099
|
-
@if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
|
|
1100
|
-
<i [ngClass]="icon()"></i>
|
|
1101
|
-
}@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
|
|
1102
|
-
<img [alt]="icon()" [src]="icon()" />
|
|
1103
|
-
}@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
|
|
1104
|
-
{{ icon() }}
|
|
1105
|
-
}@else{ I } }
|
|
1106
|
-
`,
|
|
1107
|
-
}]
|
|
1108
|
-
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
|
|
1109
|
-
|
|
1110
1217
|
/**
|
|
1111
1218
|
* Base class for form controls that support ALL Angular form patterns:
|
|
1112
1219
|
*
|
|
@@ -1230,6 +1337,77 @@ function provideValueAccessor(component) {
|
|
|
1230
1337
|
};
|
|
1231
1338
|
}
|
|
1232
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
|
+
|
|
1233
1411
|
/**
|
|
1234
1412
|
* Lazy-loading multi-select component with search, pagination, and select-all.
|
|
1235
1413
|
*
|
|
@@ -1239,6 +1417,10 @@ function provideValueAccessor(component) {
|
|
|
1239
1417
|
* - Signal forms: `[formField]="formTree.field"`
|
|
1240
1418
|
*/
|
|
1241
1419
|
class LazyMultiSelectComponent extends BaseFormControl {
|
|
1420
|
+
destroyRef = inject(DestroyRef);
|
|
1421
|
+
onDocumentClickBound = this.handleDocumentClick.bind(this);
|
|
1422
|
+
// View references
|
|
1423
|
+
pSelectRef = viewChild.required('pSelect');
|
|
1242
1424
|
// Inputs
|
|
1243
1425
|
placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1244
1426
|
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
@@ -1246,14 +1428,15 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1246
1428
|
total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1247
1429
|
pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1248
1430
|
selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
|
|
1249
|
-
|
|
1431
|
+
// Two-way bound value
|
|
1250
1432
|
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1251
1433
|
// Outputs
|
|
1252
1434
|
onSearch = output();
|
|
1253
1435
|
onPagination = output();
|
|
1254
|
-
// UI
|
|
1436
|
+
// UI state
|
|
1255
1437
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1256
|
-
|
|
1438
|
+
openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
|
|
1439
|
+
// Computed values
|
|
1257
1440
|
selectedValueDisplay = computed(() => {
|
|
1258
1441
|
const selectedValues = this.value() ?? [];
|
|
1259
1442
|
if (selectedValues.length === 0)
|
|
@@ -1262,44 +1445,59 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1262
1445
|
return `${selectedValues.length} Items Selected`;
|
|
1263
1446
|
}
|
|
1264
1447
|
return this.selectDataList()
|
|
1265
|
-
.filter(item => selectedValues.includes(item.value))
|
|
1266
|
-
.map(item => item.label)
|
|
1448
|
+
.filter((item) => selectedValues.includes(item.value))
|
|
1449
|
+
.map((item) => item.label)
|
|
1267
1450
|
.join(', ');
|
|
1268
1451
|
}, ...(ngDevMode ? [{ debugName: "selectedValueDisplay" }] : []));
|
|
1269
|
-
/** Computed: Whether all items are selected (replaces isSelectAll signal) */
|
|
1270
1452
|
isSelectAll = computed(() => {
|
|
1271
1453
|
const selectedValues = this.value() ?? [];
|
|
1272
|
-
const allValues = this.selectDataList().map(item => item.value);
|
|
1454
|
+
const allValues = this.selectDataList().map((item) => item.value);
|
|
1273
1455
|
if (selectedValues.length === 0 || allValues.length === 0)
|
|
1274
1456
|
return false;
|
|
1275
|
-
return allValues.every(val => selectedValues.includes(val));
|
|
1457
|
+
return allValues.every((val) => selectedValues.includes(val));
|
|
1276
1458
|
}, ...(ngDevMode ? [{ debugName: "isSelectAll" }] : []));
|
|
1277
1459
|
constructor() {
|
|
1278
1460
|
super();
|
|
1279
1461
|
this.initializeFormControl();
|
|
1280
|
-
// Search debounce effect
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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);
|
|
1285
1489
|
});
|
|
1286
1490
|
}
|
|
1287
|
-
onScrollBound = this.onScroll.bind(this);
|
|
1288
|
-
multiScrollContainer = viewChild.required('multiScrollContainer');
|
|
1289
1491
|
onScroll(event) {
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
this.onPagination.emit({ ...pagination, currentPage: nextPage });
|
|
1298
|
-
}
|
|
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);
|
|
1299
1499
|
}
|
|
1300
1500
|
}
|
|
1301
|
-
pSelectRef = viewChild.required('pSelect');
|
|
1302
|
-
openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
|
|
1303
1501
|
onSelectClick(event) {
|
|
1304
1502
|
if (this.disabled())
|
|
1305
1503
|
return;
|
|
@@ -1318,7 +1516,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1318
1516
|
}
|
|
1319
1517
|
}
|
|
1320
1518
|
isSelected(data) {
|
|
1321
|
-
return this.value()?.includes(data.value);
|
|
1519
|
+
return this.value()?.includes(data.value) ?? false;
|
|
1322
1520
|
}
|
|
1323
1521
|
key(option) {
|
|
1324
1522
|
return option.value;
|
|
@@ -1331,8 +1529,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1331
1529
|
}
|
|
1332
1530
|
}
|
|
1333
1531
|
else {
|
|
1334
|
-
|
|
1335
|
-
this.value.set(updated);
|
|
1532
|
+
this.value.set(previousValue.filter((v) => v !== option.value));
|
|
1336
1533
|
}
|
|
1337
1534
|
}
|
|
1338
1535
|
changeSelectAll(event) {
|
|
@@ -1348,15 +1545,12 @@ class LazyMultiSelectComponent extends BaseFormControl {
|
|
|
1348
1545
|
this.value.set([]);
|
|
1349
1546
|
}
|
|
1350
1547
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1351
|
-
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 });
|
|
1352
1549
|
}
|
|
1353
1550
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
|
|
1354
1551
|
type: Component,
|
|
1355
|
-
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)\"
|
|
1356
|
-
}], 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"] }]
|
|
1357
|
-
type: HostListener,
|
|
1358
|
-
args: ['document:click', ['$event']]
|
|
1359
|
-
}] } });
|
|
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"] }] } });
|
|
1360
1554
|
|
|
1361
1555
|
/**
|
|
1362
1556
|
* Lazy-loading single select component with search and pagination.
|
|
@@ -1367,6 +1561,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1367
1561
|
* - Signal forms: `[formField]="formTree.field"`
|
|
1368
1562
|
*/
|
|
1369
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');
|
|
1370
1569
|
// Inputs
|
|
1371
1570
|
placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1372
1571
|
optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
|
|
@@ -1376,77 +1575,85 @@ class LazySelectComponent extends BaseFormControl {
|
|
|
1376
1575
|
total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1377
1576
|
pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1378
1577
|
selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
|
|
1379
|
-
|
|
1578
|
+
// Two-way bound value
|
|
1380
1579
|
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1381
1580
|
// Outputs
|
|
1382
1581
|
onSearch = output();
|
|
1383
1582
|
onPagination = output();
|
|
1384
|
-
// UI
|
|
1583
|
+
// UI state
|
|
1385
1584
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1386
|
-
|
|
1585
|
+
isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
|
|
1387
1586
|
constructor() {
|
|
1388
1587
|
super();
|
|
1389
1588
|
this.initializeFormControl();
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
}
|
|
1394
1615
|
});
|
|
1395
1616
|
}
|
|
1396
|
-
// Signal to toggle panel
|
|
1397
|
-
isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
|
|
1398
|
-
scrollTargetEl = null;
|
|
1399
|
-
onScrollBound = this.onScroll.bind(this);
|
|
1400
|
-
scrollContainer = viewChild.required('scrollContainer');
|
|
1401
1617
|
onScroll(event) {
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
this.onPagination.emit({ ...pagination, currentPage: nextPage });
|
|
1410
|
-
}
|
|
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);
|
|
1411
1625
|
}
|
|
1412
1626
|
}
|
|
1413
|
-
// Toggle panel and manage scroll event
|
|
1414
1627
|
showPanel() {
|
|
1415
1628
|
if (this.disabled())
|
|
1416
1629
|
return;
|
|
1417
|
-
this.isPanelShow.update(prev => !prev);
|
|
1630
|
+
this.isPanelShow.update((prev) => !prev);
|
|
1418
1631
|
const isNowVisible = this.isPanelShow();
|
|
1419
1632
|
if (isNowVisible) {
|
|
1420
|
-
|
|
1633
|
+
afterNextRender(() => {
|
|
1421
1634
|
const containerEl = this.scrollContainer().nativeElement;
|
|
1422
1635
|
const target = containerEl.querySelector('.p-select-list-container');
|
|
1423
1636
|
if (target) {
|
|
1424
1637
|
target.addEventListener('scroll', this.onScrollBound);
|
|
1425
1638
|
this.scrollTargetEl = target;
|
|
1426
1639
|
}
|
|
1427
|
-
},
|
|
1640
|
+
}, { injector: this.injector });
|
|
1428
1641
|
}
|
|
1429
|
-
else {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
this.scrollTargetEl = null;
|
|
1433
|
-
}
|
|
1642
|
+
else if (this.scrollTargetEl) {
|
|
1643
|
+
this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
|
|
1644
|
+
this.scrollTargetEl = null;
|
|
1434
1645
|
}
|
|
1435
1646
|
}
|
|
1436
1647
|
onBlur() {
|
|
1437
1648
|
this.markAsTouched();
|
|
1438
1649
|
}
|
|
1439
1650
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1440
|
-
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 });
|
|
1441
1652
|
}
|
|
1442
1653
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, decorators: [{
|
|
1443
1654
|
type: Component,
|
|
1444
|
-
args: [{ selector: 'lib-lazy-select', imports: [
|
|
1445
|
-
|
|
1446
|
-
PrimeModule,
|
|
1447
|
-
EditModeElementChangerDirective
|
|
1448
|
-
], 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" }]
|
|
1449
|
-
}], 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"] }] } });
|
|
1450
1657
|
|
|
1451
1658
|
/**
|
|
1452
1659
|
* Injection Tokens for Provider Interfaces
|
|
@@ -1513,57 +1720,64 @@ const AUTH_STATE_PROVIDER = new InjectionToken('AUTH_STATE_PROVIDER', {
|
|
|
1513
1720
|
throw new Error('AUTH_STATE_PROVIDER not configured. Please provide an implementation in app.config.ts');
|
|
1514
1721
|
},
|
|
1515
1722
|
});
|
|
1516
|
-
|
|
1517
|
-
const DEFAULT_PAGE_SIZE$2 = 20;
|
|
1518
1723
|
/**
|
|
1519
|
-
*
|
|
1724
|
+
* Profile Permission Provider Token
|
|
1520
1725
|
*
|
|
1521
|
-
*
|
|
1726
|
+
* Provides user permission data for profile display.
|
|
1727
|
+
* Optional - if not configured, profile permissions section is hidden.
|
|
1728
|
+
* Use with `inject(PROFILE_PERMISSION_PROVIDER, { optional: true })`.
|
|
1729
|
+
*/
|
|
1730
|
+
const PROFILE_PERMISSION_PROVIDER = new InjectionToken('PROFILE_PERMISSION_PROVIDER');
|
|
1731
|
+
/**
|
|
1732
|
+
* Profile Upload Provider Token
|
|
1522
1733
|
*
|
|
1523
|
-
*
|
|
1524
|
-
* -
|
|
1525
|
-
*
|
|
1526
|
-
|
|
1527
|
-
|
|
1734
|
+
* Provides file upload functionality for profile pictures.
|
|
1735
|
+
* Optional - if not configured or storage not enabled, upload section is hidden.
|
|
1736
|
+
* Use with `inject(PROFILE_UPLOAD_PROVIDER, { optional: true })`.
|
|
1737
|
+
*/
|
|
1738
|
+
const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
|
|
1739
|
+
/**
|
|
1740
|
+
* User List Provider Token
|
|
1741
|
+
*
|
|
1742
|
+
* Provides extra actions, columns, and data enrichment for user list.
|
|
1743
|
+
* Optional - if not configured, default user list behavior is used.
|
|
1744
|
+
* Use with `inject(USER_LIST_PROVIDER, { optional: true })`.
|
|
1528
1745
|
*
|
|
1529
1746
|
* @example
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
|
|
1535
|
-
|
|
1747
|
+
* // In app.config.ts
|
|
1748
|
+
* providers: [
|
|
1749
|
+
* { provide: USER_LIST_PROVIDER, useClass: MyUserListProvider },
|
|
1750
|
+
* ]
|
|
1751
|
+
*/
|
|
1752
|
+
const USER_LIST_PROVIDER = new InjectionToken('USER_LIST_PROVIDER');
|
|
1753
|
+
|
|
1754
|
+
const DEFAULT_PAGE_SIZE$1 = 20;
|
|
1755
|
+
/**
|
|
1756
|
+
* Base class for user selection components.
|
|
1757
|
+
* Provides shared user loading, pagination, and search functionality.
|
|
1536
1758
|
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
* [(value)]="selectedUserId"
|
|
1540
|
-
* [isEditMode]="true"
|
|
1541
|
-
* [loadUsers]="customLoadUsers"
|
|
1542
|
-
* />
|
|
1543
|
-
* ```
|
|
1759
|
+
* Subclasses must implement:
|
|
1760
|
+
* - `setupValueEffect()` to track value changes and emit selection events
|
|
1544
1761
|
*/
|
|
1545
|
-
class
|
|
1762
|
+
class BaseUserSelectComponent {
|
|
1546
1763
|
destroyRef = inject(DestroyRef);
|
|
1764
|
+
injector = inject(Injector);
|
|
1547
1765
|
userProvider = inject(USER_PROVIDER);
|
|
1548
1766
|
abortController = null;
|
|
1549
|
-
// Optional: custom function to load users (uses USER_PROVIDER if not provided)
|
|
1550
|
-
loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
|
|
1551
1767
|
// Inputs
|
|
1768
|
+
loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
|
|
1552
1769
|
placeHolder = input('Select User', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
|
|
1553
1770
|
isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
1554
1771
|
filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
|
|
1555
1772
|
additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
|
|
1556
|
-
pageSize = input(DEFAULT_PAGE_SIZE$
|
|
1557
|
-
// Two-way bound value
|
|
1558
|
-
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1773
|
+
pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
|
|
1559
1774
|
// Outputs
|
|
1560
|
-
userSelected = output();
|
|
1561
1775
|
onError = output();
|
|
1562
1776
|
// Internal state
|
|
1563
1777
|
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
1564
1778
|
users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
|
|
1565
1779
|
total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1566
|
-
pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$
|
|
1780
|
+
pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1567
1781
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1568
1782
|
// Computed dropdown data
|
|
1569
1783
|
dropdownUsers = computed(() => this.users().map((user) => ({
|
|
@@ -1586,20 +1800,8 @@ class UserSelectComponent {
|
|
|
1586
1800
|
afterNextRender(() => {
|
|
1587
1801
|
this.fetchUsers();
|
|
1588
1802
|
});
|
|
1589
|
-
//
|
|
1590
|
-
|
|
1591
|
-
const selectedId = this.value();
|
|
1592
|
-
const users = this.users();
|
|
1593
|
-
untracked(() => {
|
|
1594
|
-
if (selectedId) {
|
|
1595
|
-
const user = users.find((u) => u.id === selectedId);
|
|
1596
|
-
this.userSelected.emit(user ?? null);
|
|
1597
|
-
}
|
|
1598
|
-
else {
|
|
1599
|
-
this.userSelected.emit(null);
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
});
|
|
1803
|
+
// Setup value change tracking (implemented by subclass)
|
|
1804
|
+
this.setupValueEffect();
|
|
1603
1805
|
}
|
|
1604
1806
|
handleSearch(search) {
|
|
1605
1807
|
this.searchTerm.set(search);
|
|
@@ -1611,6 +1813,12 @@ class UserSelectComponent {
|
|
|
1611
1813
|
this.pagination.set(pagination);
|
|
1612
1814
|
this.fetchUsers(true);
|
|
1613
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
|
+
}
|
|
1614
1822
|
async fetchUsers(append = false) {
|
|
1615
1823
|
if (this.isLoading())
|
|
1616
1824
|
return;
|
|
@@ -1668,14 +1876,63 @@ class UserSelectComponent {
|
|
|
1668
1876
|
})),
|
|
1669
1877
|
})));
|
|
1670
1878
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
+
});
|
|
1676
1933
|
}
|
|
1677
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserSelectComponent, deps:
|
|
1678
|
-
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: `
|
|
1679
1936
|
<lib-lazy-select
|
|
1680
1937
|
[(value)]="value"
|
|
1681
1938
|
[placeHolder]="placeHolder()"
|
|
@@ -1695,7 +1952,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1695
1952
|
type: Component,
|
|
1696
1953
|
args: [{
|
|
1697
1954
|
selector: 'lib-user-select',
|
|
1698
|
-
standalone: true,
|
|
1699
1955
|
imports: [AngularModule, PrimeModule, LazySelectComponent],
|
|
1700
1956
|
template: `
|
|
1701
1957
|
<lib-lazy-select
|
|
@@ -1714,166 +1970,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1714
1970
|
`,
|
|
1715
1971
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1716
1972
|
}]
|
|
1717
|
-
}],
|
|
1973
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], userSelected: [{ type: i0.Output, args: ["userSelected"] }] } });
|
|
1718
1974
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
* -
|
|
1727
|
-
* -
|
|
1728
|
-
* -
|
|
1729
|
-
* -
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1736
|
-
* [
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1742
|
-
* [
|
|
1743
|
-
* [
|
|
1744
|
-
*
|
|
1745
|
-
*
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
// Outputs
|
|
1763
|
-
usersSelected = output();
|
|
1764
|
-
onError = output();
|
|
1765
|
-
// Internal state
|
|
1766
|
-
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
1767
|
-
users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
|
|
1768
|
-
total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
|
|
1769
|
-
pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
|
|
1770
|
-
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
1771
|
-
// Computed dropdown data
|
|
1772
|
-
dropdownUsers = computed(() => this.users().map((user) => ({
|
|
1773
|
-
label: user.name || user.email,
|
|
1774
|
-
value: user.id,
|
|
1775
|
-
})), ...(ngDevMode ? [{ debugName: "dropdownUsers" }] : []));
|
|
1776
|
-
constructor() {
|
|
1777
|
-
// Cleanup on destroy
|
|
1778
|
-
this.destroyRef.onDestroy(() => {
|
|
1779
|
-
this.abortController?.abort();
|
|
1780
|
-
});
|
|
1781
|
-
// Update page size from input
|
|
1782
|
-
effect(() => {
|
|
1783
|
-
const size = this.pageSize();
|
|
1784
|
-
untracked(() => {
|
|
1785
|
-
this.pagination.update((p) => ({ ...p, pageSize: size }));
|
|
1786
|
-
});
|
|
1787
|
-
});
|
|
1788
|
-
// Load initial users after render
|
|
1789
|
-
afterNextRender(() => {
|
|
1790
|
-
this.fetchUsers();
|
|
1791
|
-
});
|
|
1792
|
-
// Emit selected users when value changes
|
|
1793
|
-
effect(() => {
|
|
1794
|
-
const selectedIds = this.value() ?? [];
|
|
1795
|
-
const users = this.users();
|
|
1796
|
-
untracked(() => {
|
|
1797
|
-
const selectedUsers = users.filter((u) => selectedIds.includes(u.id));
|
|
1798
|
-
this.usersSelected.emit(selectedUsers);
|
|
1799
|
-
});
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
handleSearch(search) {
|
|
1803
|
-
this.searchTerm.set(search);
|
|
1804
|
-
this.pagination.update((p) => ({ ...p, currentPage: 0 }));
|
|
1805
|
-
this.users.set([]);
|
|
1806
|
-
this.fetchUsers();
|
|
1807
|
-
}
|
|
1808
|
-
handlePagination(pagination) {
|
|
1809
|
-
this.pagination.set(pagination);
|
|
1810
|
-
this.fetchUsers(true);
|
|
1811
|
-
}
|
|
1812
|
-
async fetchUsers(append = false) {
|
|
1813
|
-
if (this.isLoading())
|
|
1814
|
-
return;
|
|
1815
|
-
// Cancel previous request
|
|
1816
|
-
this.abortController?.abort();
|
|
1817
|
-
this.abortController = new AbortController();
|
|
1818
|
-
this.isLoading.set(true);
|
|
1819
|
-
try {
|
|
1820
|
-
const pag = this.pagination();
|
|
1821
|
-
const filter = {
|
|
1822
|
-
page: pag.currentPage,
|
|
1823
|
-
pageSize: pag.pageSize,
|
|
1824
|
-
search: this.searchTerm(),
|
|
1825
|
-
...this.additionalFilters(),
|
|
1826
|
-
};
|
|
1827
|
-
// Use custom loadUsers if provided, otherwise use USER_PROVIDER
|
|
1828
|
-
const customLoadUsers = this.loadUsers();
|
|
1829
|
-
const response = await firstValueFrom(customLoadUsers
|
|
1830
|
-
? customLoadUsers(filter)
|
|
1831
|
-
: this.loadUsersFromProvider(filter));
|
|
1832
|
-
if (response.success && response.data) {
|
|
1833
|
-
if (append) {
|
|
1834
|
-
this.users.update((current) => [...current, ...response.data]);
|
|
1835
|
-
}
|
|
1836
|
-
else {
|
|
1837
|
-
this.users.set(response.data);
|
|
1838
|
-
}
|
|
1839
|
-
this.total.set(response.meta?.total);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
catch (error) {
|
|
1843
|
-
if (error.name !== 'AbortError') {
|
|
1844
|
-
this.onError.emit(error);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
finally {
|
|
1848
|
-
this.isLoading.set(false);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
/** Load users from USER_PROVIDER with active filter */
|
|
1852
|
-
loadUsersFromProvider(filter) {
|
|
1853
|
-
return this.userProvider
|
|
1854
|
-
.getUsers({
|
|
1855
|
-
page: filter.page,
|
|
1856
|
-
pageSize: filter.pageSize,
|
|
1857
|
-
search: filter.search,
|
|
1858
|
-
isActive: this.filterActive() ? true : undefined,
|
|
1859
|
-
})
|
|
1860
|
-
.pipe(map$1((res) => ({
|
|
1861
|
-
...res,
|
|
1862
|
-
data: res.data?.map((u) => ({
|
|
1863
|
-
id: u.id,
|
|
1864
|
-
name: u.name,
|
|
1865
|
-
email: u.email,
|
|
1866
|
-
})),
|
|
1867
|
-
})));
|
|
1868
|
-
}
|
|
1869
|
-
/** Reload users (useful when filters change externally) */
|
|
1870
|
-
reload() {
|
|
1871
|
-
this.pagination.update((p) => ({ ...p, currentPage: 0 }));
|
|
1872
|
-
this.users.set([]);
|
|
1873
|
-
this.fetchUsers();
|
|
1975
|
+
/**
|
|
1976
|
+
* User Multi-Select Component - Multiple user selection with lazy loading.
|
|
1977
|
+
*
|
|
1978
|
+
* Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
|
|
1979
|
+
*
|
|
1980
|
+
* Features:
|
|
1981
|
+
* - Search with debouncing (handled by lazy-multi-select)
|
|
1982
|
+
* - Infinite scroll pagination
|
|
1983
|
+
* - Select all / deselect all
|
|
1984
|
+
* - Filter active users by default (configurable)
|
|
1985
|
+
* - Supports additional filters via `additionalFilters` input
|
|
1986
|
+
*
|
|
1987
|
+
* @example
|
|
1988
|
+
* ```html
|
|
1989
|
+
* <!-- Simple usage - uses USER_PROVIDER internally -->
|
|
1990
|
+
* <lib-user-multi-select
|
|
1991
|
+
* [(value)]="selectedUserIds"
|
|
1992
|
+
* [isEditMode]="true"
|
|
1993
|
+
* />
|
|
1994
|
+
*
|
|
1995
|
+
* <!-- With custom loadUsers function -->
|
|
1996
|
+
* <lib-user-multi-select
|
|
1997
|
+
* [(value)]="selectedUserIds"
|
|
1998
|
+
* [isEditMode]="true"
|
|
1999
|
+
* [loadUsers]="customLoadUsers"
|
|
2000
|
+
* />
|
|
2001
|
+
* ```
|
|
2002
|
+
*/
|
|
2003
|
+
class UserMultiSelectComponent extends BaseUserSelectComponent {
|
|
2004
|
+
// Two-way bound value
|
|
2005
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
2006
|
+
// Outputs
|
|
2007
|
+
usersSelected = output();
|
|
2008
|
+
setupValueEffect() {
|
|
2009
|
+
// Emit selected users when value changes
|
|
2010
|
+
effect(() => {
|
|
2011
|
+
const selectedIds = this.value() ?? [];
|
|
2012
|
+
const users = this.users();
|
|
2013
|
+
untracked(() => {
|
|
2014
|
+
const selectedUsers = users.filter((u) => selectedIds.includes(u.id));
|
|
2015
|
+
this.usersSelected.emit(selectedUsers);
|
|
2016
|
+
});
|
|
2017
|
+
});
|
|
1874
2018
|
}
|
|
1875
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserMultiSelectComponent, deps:
|
|
1876
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserMultiSelectComponent, isStandalone: true, selector: "lib-user-multi-select", inputs: {
|
|
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: `
|
|
1877
2021
|
<lib-lazy-multi-select
|
|
1878
2022
|
[(value)]="value"
|
|
1879
2023
|
[placeHolder]="placeHolder()"
|
|
@@ -1891,7 +2035,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1891
2035
|
type: Component,
|
|
1892
2036
|
args: [{
|
|
1893
2037
|
selector: 'lib-user-multi-select',
|
|
1894
|
-
standalone: true,
|
|
1895
2038
|
imports: [AngularModule, PrimeModule, LazyMultiSelectComponent],
|
|
1896
2039
|
template: `
|
|
1897
2040
|
<lib-lazy-multi-select
|
|
@@ -1908,47 +2051,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1908
2051
|
`,
|
|
1909
2052
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1910
2053
|
}]
|
|
1911
|
-
}],
|
|
2054
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], usersSelected: [{ type: i0.Output, args: ["usersSelected"] }] } });
|
|
1912
2055
|
|
|
1913
2056
|
/**
|
|
1914
2057
|
* File Uploader Component - Drag & drop file upload with type filtering.
|
|
1915
2058
|
*
|
|
1916
2059
|
* Pass your own `uploadFile` function - works with any storage API.
|
|
1917
|
-
*
|
|
1918
|
-
* Features:
|
|
1919
|
-
* - Drag & drop support
|
|
1920
|
-
* - File type filtering (images, documents, etc.)
|
|
1921
|
-
* - Upload progress indication
|
|
1922
|
-
* - Multiple file support (optional)
|
|
1923
|
-
* - Image compression options
|
|
1924
|
-
*
|
|
1925
|
-
* @example
|
|
1926
|
-
* ```typescript
|
|
1927
|
-
* // In component
|
|
1928
|
-
* readonly uploadService = inject(UploadService);
|
|
1929
|
-
*
|
|
1930
|
-
* readonly uploadFile: UploadFileFn = (file, options) =>
|
|
1931
|
-
* this.uploadService.uploadSingleFile(file, options);
|
|
1932
|
-
* ```
|
|
1933
|
-
*
|
|
1934
|
-
* ```html
|
|
1935
|
-
* <!-- Single image upload -->
|
|
1936
|
-
* <lib-file-uploader
|
|
1937
|
-
* [uploadFile]="uploadFile"
|
|
1938
|
-
* [acceptTypes]="['image/*']"
|
|
1939
|
-
* [multiple]="false"
|
|
1940
|
-
* (fileUploaded)="onFileUploaded($event)"
|
|
1941
|
-
* />
|
|
1942
|
-
*
|
|
1943
|
-
* <!-- Multiple document upload -->
|
|
1944
|
-
* <lib-file-uploader
|
|
1945
|
-
* [uploadFile]="uploadFile"
|
|
1946
|
-
* [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
|
|
1947
|
-
* [multiple]="true"
|
|
1948
|
-
* [maxFiles]="5"
|
|
1949
|
-
* (filesUploaded)="onFilesUploaded($event)"
|
|
1950
|
-
* />
|
|
1951
|
-
* ```
|
|
1952
2060
|
*/
|
|
1953
2061
|
class FileUploaderComponent {
|
|
1954
2062
|
messageService = inject(MessageService);
|
|
@@ -2094,11 +2202,7 @@ class FileUploaderComponent {
|
|
|
2094
2202
|
});
|
|
2095
2203
|
}
|
|
2096
2204
|
catch (error) {
|
|
2097
|
-
|
|
2098
|
-
severity: 'error',
|
|
2099
|
-
summary: 'Upload Failed',
|
|
2100
|
-
detail: error.message || 'Failed to upload file',
|
|
2101
|
-
});
|
|
2205
|
+
// Error toast handled by global interceptor
|
|
2102
2206
|
this.onError.emit(error);
|
|
2103
2207
|
}
|
|
2104
2208
|
finally {
|
|
@@ -2120,37 +2224,41 @@ class FileUploaderComponent {
|
|
|
2120
2224
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2121
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: `
|
|
2122
2226
|
<div
|
|
2123
|
-
class="
|
|
2124
|
-
[class.
|
|
2125
|
-
[class.disabled]="disabled()"
|
|
2227
|
+
class="w-full"
|
|
2228
|
+
[class.opacity-60]="disabled()"
|
|
2126
2229
|
(dragover)="onDragOver($event)"
|
|
2127
2230
|
(dragleave)="onDragLeave($event)"
|
|
2128
2231
|
(drop)="onDrop($event)"
|
|
2129
2232
|
>
|
|
2130
|
-
<!-- Upload Area -->
|
|
2131
|
-
<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
|
+
>
|
|
2132
2240
|
@if (isUploading()) {
|
|
2133
|
-
<div class="
|
|
2134
|
-
<i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
|
|
2135
|
-
<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>
|
|
2136
2244
|
@if (uploadProgress() > 0) {
|
|
2137
|
-
<p-progressBar [value]="uploadProgress()" [showValue]="true" />
|
|
2245
|
+
<p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
|
|
2138
2246
|
}
|
|
2139
2247
|
</div>
|
|
2140
2248
|
} @else {
|
|
2141
|
-
<div class="
|
|
2142
|
-
<i class="pi pi-cloud-upload text-4xl text-primary"></i>
|
|
2143
|
-
<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">
|
|
2144
2252
|
{{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
|
|
2145
2253
|
</p>
|
|
2146
|
-
<p class="text-sm text-color-secondary">
|
|
2254
|
+
<p class="text-xs sm:text-sm text-color-secondary px-2">
|
|
2147
2255
|
@if (acceptTypesDisplay()) {
|
|
2148
2256
|
Allowed: {{ acceptTypesDisplay() }}
|
|
2149
2257
|
} @else {
|
|
2150
2258
|
All file types allowed
|
|
2151
2259
|
}
|
|
2152
2260
|
@if (maxSizeMb()) {
|
|
2153
|
-
(Max {{ maxSizeMb() }}MB)
|
|
2261
|
+
<span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
|
|
2154
2262
|
}
|
|
2155
2263
|
</p>
|
|
2156
2264
|
</div>
|
|
@@ -2161,71 +2269,76 @@ class FileUploaderComponent {
|
|
|
2161
2269
|
<input
|
|
2162
2270
|
#fileInput
|
|
2163
2271
|
type="file"
|
|
2272
|
+
class="hidden"
|
|
2164
2273
|
[accept]="acceptString()"
|
|
2165
2274
|
[multiple]="multiple()"
|
|
2166
2275
|
[disabled]="disabled() || isUploading()"
|
|
2167
2276
|
(change)="onFileSelected($event)"
|
|
2168
|
-
class="hidden"
|
|
2169
2277
|
/>
|
|
2170
2278
|
|
|
2171
|
-
<!-- Selected Files Preview -->
|
|
2279
|
+
<!-- Selected Files Preview - Responsive layout -->
|
|
2172
2280
|
@if (selectedFiles().length > 0 && showPreview()) {
|
|
2173
|
-
<div class="
|
|
2281
|
+
<div class="mt-3 space-y-2">
|
|
2174
2282
|
@for (file of selectedFiles(); track file.name) {
|
|
2175
|
-
<div class="file-item flex
|
|
2176
|
-
<i [class]="getFileIcon(file)"></i>
|
|
2177
|
-
<span class="flex-1 text-
|
|
2178
|
-
<span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
|
|
2179
|
-
<button
|
|
2180
|
-
pButton
|
|
2181
|
-
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
|
|
2182
2288
|
icon="pi pi-times"
|
|
2183
|
-
|
|
2184
|
-
|
|
2289
|
+
[text]="true"
|
|
2290
|
+
[rounded]="true"
|
|
2291
|
+
size="small"
|
|
2292
|
+
severity="secondary"
|
|
2185
2293
|
[disabled]="isUploading()"
|
|
2186
|
-
|
|
2294
|
+
(onClick)="removeFile(file)"
|
|
2295
|
+
/>
|
|
2187
2296
|
</div>
|
|
2188
2297
|
}
|
|
2189
2298
|
</div>
|
|
2190
2299
|
}
|
|
2191
2300
|
</div>
|
|
2192
|
-
`, 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 });
|
|
2193
2302
|
}
|
|
2194
2303
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, decorators: [{
|
|
2195
2304
|
type: Component,
|
|
2196
|
-
args: [{ selector: 'lib-file-uploader',
|
|
2305
|
+
args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule], template: `
|
|
2197
2306
|
<div
|
|
2198
|
-
class="
|
|
2199
|
-
[class.
|
|
2200
|
-
[class.disabled]="disabled()"
|
|
2307
|
+
class="w-full"
|
|
2308
|
+
[class.opacity-60]="disabled()"
|
|
2201
2309
|
(dragover)="onDragOver($event)"
|
|
2202
2310
|
(dragleave)="onDragLeave($event)"
|
|
2203
2311
|
(drop)="onDrop($event)"
|
|
2204
2312
|
>
|
|
2205
|
-
<!-- Upload Area -->
|
|
2206
|
-
<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
|
+
>
|
|
2207
2320
|
@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>
|
|
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>
|
|
2211
2324
|
@if (uploadProgress() > 0) {
|
|
2212
|
-
<p-progressBar [value]="uploadProgress()" [showValue]="true" />
|
|
2325
|
+
<p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
|
|
2213
2326
|
}
|
|
2214
2327
|
</div>
|
|
2215
2328
|
} @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">
|
|
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">
|
|
2219
2332
|
{{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
|
|
2220
2333
|
</p>
|
|
2221
|
-
<p class="text-sm text-color-secondary">
|
|
2334
|
+
<p class="text-xs sm:text-sm text-color-secondary px-2">
|
|
2222
2335
|
@if (acceptTypesDisplay()) {
|
|
2223
2336
|
Allowed: {{ acceptTypesDisplay() }}
|
|
2224
2337
|
} @else {
|
|
2225
2338
|
All file types allowed
|
|
2226
2339
|
}
|
|
2227
2340
|
@if (maxSizeMb()) {
|
|
2228
|
-
(Max {{ maxSizeMb() }}MB)
|
|
2341
|
+
<span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
|
|
2229
2342
|
}
|
|
2230
2343
|
</p>
|
|
2231
2344
|
</div>
|
|
@@ -2236,35 +2349,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2236
2349
|
<input
|
|
2237
2350
|
#fileInput
|
|
2238
2351
|
type="file"
|
|
2352
|
+
class="hidden"
|
|
2239
2353
|
[accept]="acceptString()"
|
|
2240
2354
|
[multiple]="multiple()"
|
|
2241
2355
|
[disabled]="disabled() || isUploading()"
|
|
2242
2356
|
(change)="onFileSelected($event)"
|
|
2243
|
-
class="hidden"
|
|
2244
2357
|
/>
|
|
2245
2358
|
|
|
2246
|
-
<!-- Selected Files Preview -->
|
|
2359
|
+
<!-- Selected Files Preview - Responsive layout -->
|
|
2247
2360
|
@if (selectedFiles().length > 0 && showPreview()) {
|
|
2248
|
-
<div class="
|
|
2361
|
+
<div class="mt-3 space-y-2">
|
|
2249
2362
|
@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"
|
|
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
|
|
2257
2368
|
icon="pi pi-times"
|
|
2258
|
-
|
|
2259
|
-
|
|
2369
|
+
[text]="true"
|
|
2370
|
+
[rounded]="true"
|
|
2371
|
+
size="small"
|
|
2372
|
+
severity="secondary"
|
|
2260
2373
|
[disabled]="isUploading()"
|
|
2261
|
-
|
|
2374
|
+
(onClick)="removeFile(file)"
|
|
2375
|
+
/>
|
|
2262
2376
|
</div>
|
|
2263
2377
|
}
|
|
2264
2378
|
</div>
|
|
2265
2379
|
}
|
|
2266
2380
|
</div>
|
|
2267
|
-
`, 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"] }]
|
|
2268
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"] }] } });
|
|
2269
2383
|
|
|
2270
2384
|
const DEFAULT_PAGE_SIZE = 20;
|
|
@@ -2379,16 +2493,14 @@ class FileSelectorDialogComponent {
|
|
|
2379
2493
|
}, 500);
|
|
2380
2494
|
}
|
|
2381
2495
|
onScroll(event) {
|
|
2382
|
-
const
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
this.fetchFiles(true);
|
|
2391
|
-
}
|
|
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);
|
|
2392
2504
|
}
|
|
2393
2505
|
}
|
|
2394
2506
|
toggleSelection(file) {
|
|
@@ -2499,11 +2611,13 @@ class FileSelectorDialogComponent {
|
|
|
2499
2611
|
[closable]="true"
|
|
2500
2612
|
[draggable]="false"
|
|
2501
2613
|
[resizable]="false"
|
|
2502
|
-
[
|
|
2614
|
+
[breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
|
|
2615
|
+
[style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
|
|
2616
|
+
styleClass="file-selector-dialog"
|
|
2503
2617
|
(onHide)="onDialogHide()"
|
|
2504
2618
|
>
|
|
2505
2619
|
<!-- Search Bar -->
|
|
2506
|
-
<div class="flex gap-2 mb-3">
|
|
2620
|
+
<div class="flex flex-col sm:flex-row gap-2 mb-3">
|
|
2507
2621
|
<span class="p-input-icon-left flex-1">
|
|
2508
2622
|
<i class="pi pi-search"></i>
|
|
2509
2623
|
<input
|
|
@@ -2516,25 +2630,25 @@ class FileSelectorDialogComponent {
|
|
|
2516
2630
|
/>
|
|
2517
2631
|
</span>
|
|
2518
2632
|
@if (multiple()) {
|
|
2519
|
-
<span class="text-sm text-color-secondary
|
|
2633
|
+
<span class="text-sm text-color-secondary self-center whitespace-nowrap">
|
|
2520
2634
|
{{ selectedFiles().length }} selected
|
|
2521
2635
|
</span>
|
|
2522
2636
|
}
|
|
2523
2637
|
</div>
|
|
2524
2638
|
|
|
2525
|
-
<!-- File Grid -->
|
|
2639
|
+
<!-- File Grid - Responsive columns -->
|
|
2526
2640
|
<div
|
|
2527
2641
|
class="file-grid"
|
|
2528
2642
|
#scrollContainer
|
|
2529
2643
|
(scroll)="onScroll($event)"
|
|
2530
2644
|
>
|
|
2531
2645
|
@if (isLoading() && files().length === 0) {
|
|
2532
|
-
<div class="flex justify-
|
|
2533
|
-
<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>
|
|
2534
2648
|
</div>
|
|
2535
2649
|
} @else if (files().length === 0) {
|
|
2536
|
-
<div class="text-center p-4 text-color-secondary">
|
|
2537
|
-
<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>
|
|
2538
2652
|
<p>No files found</p>
|
|
2539
2653
|
</div>
|
|
2540
2654
|
} @else {
|
|
@@ -2548,54 +2662,59 @@ class FileSelectorDialogComponent {
|
|
|
2548
2662
|
<!-- File Preview -->
|
|
2549
2663
|
<div class="file-preview">
|
|
2550
2664
|
@if (isImage(file) && file.url) {
|
|
2551
|
-
<img [src]="file.url" [alt]="file.name" class="
|
|
2665
|
+
<img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
|
|
2552
2666
|
} @else {
|
|
2553
|
-
<i [class]="getFileIcon(file)" class="
|
|
2667
|
+
<i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
|
|
2554
2668
|
}
|
|
2555
2669
|
@if (isSelected(file)) {
|
|
2556
2670
|
<div class="selected-overlay">
|
|
2557
|
-
<i class="pi pi-check"></i>
|
|
2671
|
+
<i class="pi pi-check text-xl sm:text-2xl"></i>
|
|
2558
2672
|
</div>
|
|
2559
2673
|
}
|
|
2560
2674
|
</div>
|
|
2561
2675
|
|
|
2562
2676
|
<!-- File Info -->
|
|
2563
|
-
<div class="
|
|
2564
|
-
<span class="
|
|
2565
|
-
|
|
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>
|
|
2566
2682
|
</div>
|
|
2567
2683
|
</div>
|
|
2568
2684
|
}
|
|
2569
2685
|
|
|
2570
2686
|
@if (isLoading()) {
|
|
2571
|
-
<div class="flex justify-
|
|
2572
|
-
<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>
|
|
2573
2689
|
</div>
|
|
2574
2690
|
}
|
|
2575
2691
|
}
|
|
2576
2692
|
</div>
|
|
2577
2693
|
|
|
2578
2694
|
<!-- Footer -->
|
|
2579
|
-
<ng-template
|
|
2580
|
-
<
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
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>
|
|
2592
2711
|
</ng-template>
|
|
2593
2712
|
</p-dialog>
|
|
2594
|
-
`, 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 });
|
|
2595
2714
|
}
|
|
2596
2715
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
|
|
2597
2716
|
type: Component,
|
|
2598
|
-
args: [{ selector: 'lib-file-selector-dialog',
|
|
2717
|
+
args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule], template: `
|
|
2599
2718
|
<p-dialog
|
|
2600
2719
|
[header]="header()"
|
|
2601
2720
|
[(visible)]="visible"
|
|
@@ -2603,11 +2722,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2603
2722
|
[closable]="true"
|
|
2604
2723
|
[draggable]="false"
|
|
2605
2724
|
[resizable]="false"
|
|
2606
|
-
[
|
|
2725
|
+
[breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
|
|
2726
|
+
[style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
|
|
2727
|
+
styleClass="file-selector-dialog"
|
|
2607
2728
|
(onHide)="onDialogHide()"
|
|
2608
2729
|
>
|
|
2609
2730
|
<!-- Search Bar -->
|
|
2610
|
-
<div class="flex gap-2 mb-3">
|
|
2731
|
+
<div class="flex flex-col sm:flex-row gap-2 mb-3">
|
|
2611
2732
|
<span class="p-input-icon-left flex-1">
|
|
2612
2733
|
<i class="pi pi-search"></i>
|
|
2613
2734
|
<input
|
|
@@ -2620,25 +2741,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2620
2741
|
/>
|
|
2621
2742
|
</span>
|
|
2622
2743
|
@if (multiple()) {
|
|
2623
|
-
<span class="text-sm text-color-secondary
|
|
2744
|
+
<span class="text-sm text-color-secondary self-center whitespace-nowrap">
|
|
2624
2745
|
{{ selectedFiles().length }} selected
|
|
2625
2746
|
</span>
|
|
2626
2747
|
}
|
|
2627
2748
|
</div>
|
|
2628
2749
|
|
|
2629
|
-
<!-- File Grid -->
|
|
2750
|
+
<!-- File Grid - Responsive columns -->
|
|
2630
2751
|
<div
|
|
2631
2752
|
class="file-grid"
|
|
2632
2753
|
#scrollContainer
|
|
2633
2754
|
(scroll)="onScroll($event)"
|
|
2634
2755
|
>
|
|
2635
2756
|
@if (isLoading() && files().length === 0) {
|
|
2636
|
-
<div class="flex justify-
|
|
2637
|
-
<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>
|
|
2638
2759
|
</div>
|
|
2639
2760
|
} @else if (files().length === 0) {
|
|
2640
|
-
<div class="text-center p-4 text-color-secondary">
|
|
2641
|
-
<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>
|
|
2642
2763
|
<p>No files found</p>
|
|
2643
2764
|
</div>
|
|
2644
2765
|
} @else {
|
|
@@ -2652,182 +2773,484 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2652
2773
|
<!-- File Preview -->
|
|
2653
2774
|
<div class="file-preview">
|
|
2654
2775
|
@if (isImage(file) && file.url) {
|
|
2655
|
-
<img [src]="file.url" [alt]="file.name" class="
|
|
2776
|
+
<img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
|
|
2656
2777
|
} @else {
|
|
2657
|
-
<i [class]="getFileIcon(file)" class="
|
|
2778
|
+
<i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
|
|
2658
2779
|
}
|
|
2659
2780
|
@if (isSelected(file)) {
|
|
2660
2781
|
<div class="selected-overlay">
|
|
2661
|
-
<i class="pi pi-check"></i>
|
|
2782
|
+
<i class="pi pi-check text-xl sm:text-2xl"></i>
|
|
2662
2783
|
</div>
|
|
2663
2784
|
}
|
|
2664
2785
|
</div>
|
|
2665
2786
|
|
|
2666
2787
|
<!-- File Info -->
|
|
2667
|
-
<div class="
|
|
2668
|
-
<span class="
|
|
2669
|
-
|
|
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>
|
|
2670
2793
|
</div>
|
|
2671
2794
|
</div>
|
|
2672
2795
|
}
|
|
2673
2796
|
|
|
2674
2797
|
@if (isLoading()) {
|
|
2675
|
-
<div class="flex justify-
|
|
2676
|
-
<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>
|
|
2677
2800
|
</div>
|
|
2678
2801
|
}
|
|
2679
2802
|
}
|
|
2680
2803
|
</div>
|
|
2681
2804
|
|
|
2682
2805
|
<!-- Footer -->
|
|
2683
|
-
<ng-template
|
|
2684
|
-
<
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
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>
|
|
2696
2822
|
</ng-template>
|
|
2697
2823
|
</p-dialog>
|
|
2698
|
-
`, 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"] }]
|
|
2699
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"] }] } });
|
|
2700
2826
|
|
|
2701
|
-
|
|
2702
|
-
const devLog = (message) => {
|
|
2703
|
-
if (isDevMode())
|
|
2704
|
-
console.log(message);
|
|
2705
|
-
};
|
|
2706
|
-
/**
|
|
2707
|
-
* Permission Guard
|
|
2708
|
-
*
|
|
2709
|
-
* Route-level guard for permission-based access control.
|
|
2710
|
-
* Validates permissions before allowing navigation.
|
|
2711
|
-
*
|
|
2712
|
-
* Features:
|
|
2713
|
-
* - Single permission check
|
|
2714
|
-
* - Complex ILogicNode logic trees
|
|
2715
|
-
* - Configurable redirect URL
|
|
2716
|
-
* - Debug logging for denied access
|
|
2717
|
-
*
|
|
2718
|
-
* @example
|
|
2719
|
-
* ```typescript
|
|
2720
|
-
* // Simple permission check
|
|
2721
|
-
* { path: 'users', canActivate: [permissionGuard('user.view')] }
|
|
2722
|
-
*
|
|
2723
|
-
* // Complex logic
|
|
2724
|
-
* { path: 'admin', canActivate: [permissionGuard({
|
|
2725
|
-
* id: 'root',
|
|
2726
|
-
* type: 'group',
|
|
2727
|
-
* operator: 'AND',
|
|
2728
|
-
* children: [
|
|
2729
|
-
* { id: '1', type: 'action', actionId: 'admin.view' },
|
|
2730
|
-
* { id: '2', type: 'action', actionId: 'admin.manage' }
|
|
2731
|
-
* ]
|
|
2732
|
-
* })] }
|
|
2733
|
-
*
|
|
2734
|
-
* // With custom redirect
|
|
2735
|
-
* { path: 'users', canActivate: [permissionGuard('user.view', '/access-denied')] }
|
|
2736
|
-
* ```
|
|
2737
|
-
*/
|
|
2738
|
-
function permissionGuard(permission, redirectTo = '/') {
|
|
2827
|
+
function createGuard(guardName, redirectTo, evaluate, getDenialMessage) {
|
|
2739
2828
|
return () => {
|
|
2740
2829
|
const permissionValidator = inject(PermissionValidatorService);
|
|
2741
2830
|
const router = inject(Router);
|
|
2742
|
-
// Check if permissions are loaded
|
|
2743
2831
|
if (!permissionValidator.isPermissionsLoaded()) {
|
|
2744
|
-
|
|
2832
|
+
if (isDevMode()) {
|
|
2833
|
+
console.log(`[${guardName}] Permissions not loaded, denying access`);
|
|
2834
|
+
}
|
|
2745
2835
|
return router.createUrlTree([redirectTo]);
|
|
2746
2836
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
devLog(`[permissionGuard] Access denied - missing permission: ${permissionCode}`);
|
|
2837
|
+
if (!evaluate(permissionValidator.permissions())) {
|
|
2838
|
+
if (isDevMode()) {
|
|
2839
|
+
console.log(`[${guardName}] ${getDenialMessage()}`);
|
|
2840
|
+
}
|
|
2752
2841
|
return router.createUrlTree([redirectTo]);
|
|
2753
2842
|
}
|
|
2754
2843
|
return true;
|
|
2755
2844
|
};
|
|
2756
2845
|
}
|
|
2757
2846
|
/**
|
|
2758
|
-
*
|
|
2847
|
+
* Permission Guard - Single permission or ILogicNode check.
|
|
2759
2848
|
*
|
|
2760
|
-
*
|
|
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.
|
|
2761
2861
|
*
|
|
2762
2862
|
* @example
|
|
2763
2863
|
* ```typescript
|
|
2764
|
-
* // Allow if user has view OR create permission
|
|
2765
2864
|
* { path: 'users', canActivate: [anyPermissionGuard(['user.view', 'user.create'])] }
|
|
2766
2865
|
* ```
|
|
2767
2866
|
*/
|
|
2768
2867
|
function anyPermissionGuard(permissions, redirectTo = '/') {
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
if (!permissionValidator.isPermissionsLoaded()) {
|
|
2779
|
-
devLog('[anyPermissionGuard] Permissions not loaded, denying access to route');
|
|
2780
|
-
return router.createUrlTree([redirectTo]);
|
|
2781
|
-
}
|
|
2782
|
-
const userPermissions = permissionValidator.permissions();
|
|
2783
|
-
const hasPermission = hasAnyPermission(permissions, userPermissions);
|
|
2784
|
-
if (!hasPermission) {
|
|
2785
|
-
devLog(`[anyPermissionGuard] Access denied - missing any of: ${permissions.join(', ')}`);
|
|
2786
|
-
return router.createUrlTree([redirectTo]);
|
|
2787
|
-
}
|
|
2788
|
-
return true;
|
|
2789
|
-
};
|
|
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(', ')}`);
|
|
2790
2877
|
}
|
|
2791
2878
|
/**
|
|
2792
|
-
* All Permissions Guard (AND logic)
|
|
2793
|
-
*
|
|
2794
|
-
* Allows access only if user has ALL of the specified permissions.
|
|
2879
|
+
* All Permissions Guard (AND logic) - Access only if user has ALL permissions.
|
|
2795
2880
|
*
|
|
2796
2881
|
* @example
|
|
2797
2882
|
* ```typescript
|
|
2798
|
-
* // Allow only if user has BOTH view AND create permissions
|
|
2799
2883
|
* { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
|
|
2800
2884
|
* ```
|
|
2801
2885
|
*/
|
|
2802
2886
|
function allPermissionsGuard(permissions, redirectTo = '/') {
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
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;
|
|
2821
3018
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
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 });
|
|
2824
3244
|
}
|
|
3245
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, decorators: [{
|
|
3246
|
+
type: Directive
|
|
3247
|
+
}] });
|
|
2825
3248
|
|
|
2826
|
-
//
|
|
3249
|
+
// Constants
|
|
2827
3250
|
|
|
2828
3251
|
/**
|
|
2829
3252
|
* Generated bundle index. Do not edit.
|
|
2830
3253
|
*/
|
|
2831
3254
|
|
|
2832
|
-
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, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, 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 };
|
|
2833
3256
|
//# sourceMappingURL=flusys-ng-shared.mjs.map
|