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