@fhss-web-team/frontend-utils 1.7.3 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,24 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Component, InjectionToken, inject, Injector, signal, afterNextRender, effect, computed, Injectable, input, untracked, ViewChild, model, linkedSignal } from '@angular/core';
2
+ import { Component, InjectionToken, inject, Injector, signal, afterNextRender, effect, computed, Injectable, input, model, linkedSignal, untracked } from '@angular/core';
3
3
  import * as i1 from '@angular/router';
4
4
  import { Router, RouterModule, RedirectCommand } from '@angular/router';
5
5
  import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType, typeEventArgs } from 'keycloak-angular';
6
6
  import Keycloak from 'keycloak-js';
7
+ import { isTRPCClientError } from '@trpc/client';
7
8
  import * as i1$1 from '@angular/material/table';
8
9
  import { MatTableModule } from '@angular/material/table';
10
+ import * as i2 from '@angular/material/progress-spinner';
11
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
9
12
  import * as i3 from '@angular/material/paginator';
10
- import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator';
11
- import * as i2 from '@angular/material/sort';
12
- import { MatSortModule, MatSort } from '@angular/material/sort';
13
- import { DatePipe } from '@angular/common';
14
- import * as i4 from '@angular/forms';
13
+ import { MatPaginatorModule } from '@angular/material/paginator';
14
+ import * as i4 from '@angular/material/input';
15
+ import { MatInputModule } from '@angular/material/input';
16
+ import * as i5 from '@angular/forms';
15
17
  import { FormsModule } from '@angular/forms';
16
- import * as i5 from '@angular/material/input';
17
- import { MatInputModule, MatFormField } from '@angular/material/input';
18
- import * as i6 from '@angular/material/select';
19
- import { MatSelectModule } from '@angular/material/select';
20
- import { isTRPCClientError } from '@trpc/client';
21
- import * as i2$1 from '@angular/material/progress-spinner';
22
- import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
23
18
  import * as i1$2 from '@angular/material/button';
24
19
  import { MatButtonModule } from '@angular/material/button';
20
+ import * as i7 from '@angular/material/sort';
21
+ import { MatSortModule } from '@angular/material/sort';
25
22
  import * as i8 from '@angular/material/icon';
26
23
  import { MatIconModule } from '@angular/material/icon';
27
24
  import * as i9 from '@angular/material/core';
@@ -263,290 +260,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
263
260
  args: [{ selector: 'byu-header', imports: [RouterModule], template: "<header>\n <div class=\"top\" >\n <img class=\"logo\" src=\"/BYU_monogram_white@2x.png\" alt=\"BYU\">\n <div class=\"titles\"> \n <div class=\"breadcrumbs\">\n @for (breadcrumb of config()?.breadcrumbs; track breadcrumb.text){\n <a [href]=\"breadcrumb.path\" >{{ breadcrumb.text }}</a>\n }\n </div>\n <a [routerLink]=\"config()?.title?.path\" class=\"title\">{{ config()?.title?.text }}</a>\n <a [routerLink]=\"config()?.subtitle?.path\" class=\"subtitle\">{{ config()?.subtitle?.text }}</a>\n </div>\n <div class=\"signin\">\n <p>{{ this.auth.tokenParsed()?.['given_name'] ?? '' }}</p>\n <svg class=\"signin-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">\n <path fill=\"currentcolor\" d=\"M50 95c-26 0-34-18-34-18 3-12 8-18 17-18 5 5 10 7 17 7s12-2 17-7c9 0 14 6 17 18 0 0-7 18-34 18z\"></path>\n <circle cx=\"50\" cy=\"50\" r=\"45\" fill=\"none\" stroke=\"currentcolor\" stroke-width=\"10\"></circle>\n <circle fill=\"currentcolor\" cx=\"50\" cy=\"40\" r=\"20\"></circle>\n </svg>\n @if (auth.authenticated()) {\n <a class=\"signin-link\" (click)=\"auth.logout()\">Sign Out</a>\n } @else {\n <a class=\"signin-link\" (click)=\"auth.login()\">Sign In</a>\n }\n </div>\n </div>\n <div class=\"bottom\">\n <nav>\n @for (menuItem of config()?.menu; track menuItem.text){\n @if (isHeaderLink(menuItem)) {\n <li class = \"nav-item\">\n <a class=\"nav-item-content\" [routerLink]=\"menuItem.path\">{{ menuItem.text }}</a>\n </li>\n } @else {\n <li class=\"nav-item dropdown\" (click)=\"toggleDropdown(menuItem.text)\">\n <div class=\"nav-item-content\" >{{ menuItem.text }}</div>\n @if (openDropdownText === menuItem.text) {\n <ul class = \"dropdown-item-menu\"> \n @for(dropItem of menuItem.items; track dropItem.text){\n <li class = \"dropdown-item\">\n <a class=\"dropdown-item-content\" [routerLink]=\"dropItem.path\">{{ dropItem.text }}</a>\n </li>\n }\n </ul>\n }\n </li>\n }\n }\n </nav>\n </div>\n</header>\n", styles: ["header{font-family:HCo Ringside Narrow SSm,Open Sans,Helvetica,Arial,sans-serif;color:#fff}header .top{background-color:#002e5d;display:flex;align-items:center;padding:13px 16px;gap:16px}header .top .logo{width:100px}header .top .titles{display:flex;flex-direction:column;gap:8px;padding-left:30px;border-left:1px solid rgba(255,255,255,.25)}header .top .titles .breadcrumbs{display:flex;flex-direction:row}header .top .titles .breadcrumbs a{color:#a6abb1;text-decoration:none;font-size:16px}header .top .titles .breadcrumbs a:not(:first-child){padding-left:10px}header .top .titles .breadcrumbs a:not(:last-child){padding-right:10px;border-right:1px solid rgba(255,255,255,.25)}header .top .titles .breadcrumbs a:hover{color:#fff}header .top .titles .title{color:inherit;font-size:24px;font-weight:500;text-decoration:none}header .top .titles .subtitle{color:inherit;font-size:16px;font-weight:500;text-decoration:none}header .top .signin{display:flex;align-items:center;gap:10px;font-size:16px;margin-left:auto}header .top .signin .signin-icon{display:flex;margin-left:auto;margin-right:0;align-items:center;height:20px;width:20px}header .top .signin .signin-link{color:#fff;text-decoration:none;cursor:pointer}header .bottom{box-shadow:0 3px 10px #ccc5c580}header .bottom nav{display:flex;flex-direction:row;padding-left:124px}header .bottom nav .nav-item{display:block;list-style:none;transition:all .25s;text-decoration:none}header .bottom nav .nav-item:hover{box-shadow:inset 0 -5px #002e5d;background-color:#f1f1f1}header .bottom nav .nav-item .nav-item-content{margin:5px;display:inline-block;padding:11px 22px;text-decoration:none;color:#002e5d}header .bottom nav .nav-item.dropdown{position:relative}header .bottom nav .nav-item.dropdown .dropdown-item-menu{position:absolute;background:#fff;z-index:1000;top:100%;width:min-content;margin:-5px;padding:0;box-shadow:0 3px 3px #ccc5c5bf}header .bottom nav .nav-item.dropdown .dropdown-item-menu .dropdown-item:hover{background-color:#f1f1f1}header .bottom nav .nav-item.dropdown .dropdown-item-menu .dropdown-item .dropdown-item-content{margin:10px;display:inline-block;text-decoration:none;text-wrap:nowrap;color:#002e5d;padding:11px 22px}\n"] }]
264
261
  }] });
265
262
 
266
- const debounced = (inputSignal, wait = 400) => {
267
- const debouncedSignal = signal(inputSignal());
268
- const setSignal = debounce((value) => debouncedSignal.set(value), wait);
269
- effect(() => {
270
- setSignal(inputSignal());
271
- });
272
- return debouncedSignal;
273
- };
274
- const debounce = (callback, wait) => {
275
- let timeoutId;
276
- return (...args) => {
277
- window.clearTimeout(timeoutId);
278
- timeoutId = window.setTimeout(() => {
279
- callback(...args);
280
- }, wait);
281
- };
282
- };
283
-
284
- /**
285
- * Creates a reactive fetch signal.
286
- *
287
- * The request function can include Signals for properties. These are unwrapped in the refresh function.
288
- */
289
- function createFetchSignal(request, method, transform, options) {
290
- // Use a computed signal so that any changes to Signals in the request object trigger updates.
291
- const currentRequest = computed(request);
292
- const value = signal(options?.defaultValue, { equal: options?.equal });
293
- const errorResponse = signal(undefined);
294
- const isLoading = signal(false);
295
- const statusCode = signal(undefined);
296
- const headers = signal(undefined);
297
- const status = signal('idle');
298
- const error = signal(undefined);
299
- const injector = inject(Injector);
300
- let effectRef = undefined;
301
- if (options?.autoRefresh) {
302
- effectRef = effect((onCleanup) => {
303
- // pass abort signal to refresh on cleanup of effect
304
- const controller = new AbortController();
305
- onCleanup(() => controller.abort());
306
- // call refresh with this abort controller
307
- refresh(controller.signal, true);
308
- }, { injector: options?.injector || injector });
309
- }
310
- const refresh = async (abortSignal, keepLoadingThroughAbort) => {
311
- // if the fetchSignal has been destroyed, do nothing
312
- if (untracked(status) === 'destroyed')
313
- return;
314
- // Reset signals for a fresh request.
315
- isLoading.set(true);
316
- errorResponse.set(undefined);
317
- statusCode.set(undefined);
318
- headers.set(undefined);
319
- status.set('loading');
320
- error.set(undefined);
321
- // Unwrap the current request.
322
- const req = currentRequest();
323
- const url = req.url;
324
- const params = req.params;
325
- const requestHeaders = req.headers;
326
- const body = req.body;
327
- // Build URL with query parameters.
328
- let uri = url;
329
- if (params) {
330
- const searchParams = new URLSearchParams();
331
- for (const key in params) {
332
- if (params[key] !== undefined)
333
- searchParams.append(key, String(params[key]));
334
- }
335
- uri += (uri.includes('?') ? '&' : '?') + searchParams.toString();
336
- }
337
- // Filter out undefined header values
338
- const filteredHeaders = requestHeaders
339
- ? Object.fromEntries(Object.entries(requestHeaders).filter(([, value]) => value !== undefined))
340
- : undefined;
341
- try {
342
- // send the request
343
- const response = await fetch(uri, {
344
- method,
345
- headers: filteredHeaders,
346
- // Only include a body if one is provided.
347
- body: body,
348
- signal: abortSignal,
349
- });
350
- // set the status code
351
- statusCode.set(response.status);
352
- // Extract response headers.
353
- const headersObj = {};
354
- response.headers.forEach((val, key) => {
355
- headersObj[key] = val;
356
- });
357
- headers.set(headersObj);
358
- // if the response is ok, transform the body
359
- if (response.ok) {
360
- value.set(await transform(response));
361
- status.set('resolved');
362
- }
363
- else {
364
- // try to parse the error as json
365
- try {
366
- errorResponse.set(await response.json());
367
- value.set(undefined);
368
- status.set('resolved');
369
- }
370
- catch {
371
- throw new Error('Unable to parse error response.');
372
- }
373
- }
374
- }
375
- catch (err) {
376
- if (err instanceof Error) {
377
- // if the fetch request was aborted
378
- // we check if we would like to continue loading (the next request)
379
- // if so then we just leave this refresh in an undefined state
380
- // else we error as usual
381
- if (err.name === 'AbortError' && keepLoadingThroughAbort) {
382
- return;
383
- }
384
- error.set(err);
385
- }
386
- else {
387
- // Fallback for non-Error values
388
- error.set(new Error(String(err)));
389
- }
390
- value.set(undefined);
391
- status.set('error');
392
- }
393
- isLoading.set(false);
394
- };
395
- const destroy = () => {
396
- // if the fetchSignal has been destroyed, do nothing
397
- if (status() === 'destroyed')
398
- return;
399
- if (effectRef) {
400
- effectRef.destroy();
401
- }
402
- status.set('destroyed');
403
- value.set(undefined);
404
- errorResponse.set(undefined);
405
- isLoading.set(false);
406
- statusCode.set(undefined);
407
- headers.set(undefined);
408
- error.set(undefined);
409
- };
410
- return {
411
- value,
412
- errorResponse,
413
- isLoading,
414
- statusCode,
415
- headers,
416
- status,
417
- error,
418
- refresh,
419
- destroy
420
- };
421
- }
422
- //
423
- // Helpers for attaching response transforms.
424
- //
425
- const createHelper = (method, transform) => (request, options) => createFetchSignal(request, method, transform, options);
426
- // Transforms
427
- const jsonTransformer = (response) => response.json();
428
- const textTransformer = (response) => response.text();
429
- const blobTransformer = (response) => response.blob();
430
- const arrayBufferTransformer = (response) => response.arrayBuffer();
431
- //
432
- // Build the defaults - GET chain
433
- //
434
- const fetchSignal = createHelper('GET', jsonTransformer);
435
- fetchSignal.json = createHelper('GET', jsonTransformer);
436
- fetchSignal.text = createHelper('GET', textTransformer);
437
- fetchSignal.blob = createHelper('GET', blobTransformer);
438
- fetchSignal.arrayBuffer = createHelper('GET', arrayBufferTransformer);
439
- //
440
- // Build the GET chain
441
- //
442
- fetchSignal.get = createHelper('GET', jsonTransformer);
443
- fetchSignal.get.json = createHelper('GET', jsonTransformer);
444
- fetchSignal.get.text = createHelper('GET', textTransformer);
445
- fetchSignal.get.blob = createHelper('GET', blobTransformer);
446
- fetchSignal.get.arrayBuffer = createHelper('GET', arrayBufferTransformer);
447
- //
448
- // Build the POST chain.
449
- //
450
- fetchSignal.post = createHelper('POST', jsonTransformer);
451
- fetchSignal.post.json = createHelper('POST', jsonTransformer);
452
- fetchSignal.post.text = createHelper('POST', textTransformer);
453
- fetchSignal.post.blob = createHelper('POST', blobTransformer);
454
- fetchSignal.post.arrayBuffer = createHelper('POST', arrayBufferTransformer);
455
- //
456
- // Build the PUT chain.
457
- //
458
- fetchSignal.put = createHelper('PUT', jsonTransformer);
459
- fetchSignal.put.json = createHelper('PUT', jsonTransformer);
460
- fetchSignal.put.text = createHelper('PUT', textTransformer);
461
- fetchSignal.put.blob = createHelper('PUT', blobTransformer);
462
- fetchSignal.put.arrayBuffer = createHelper('PUT', arrayBufferTransformer);
463
- //
464
- // Build the PATCH chain.
465
- //
466
- fetchSignal.patch = createHelper('PATCH', jsonTransformer);
467
- fetchSignal.patch.json = createHelper('PATCH', jsonTransformer);
468
- fetchSignal.patch.text = createHelper('PATCH', textTransformer);
469
- fetchSignal.patch.blob = createHelper('PATCH', blobTransformer);
470
- fetchSignal.patch.arrayBuffer = createHelper('PATCH', arrayBufferTransformer);
471
- //
472
- // Build the DELETE chain
473
- //
474
- fetchSignal.delete = createHelper('DELETE', jsonTransformer);
475
- fetchSignal.delete.json = createHelper('DELETE', jsonTransformer);
476
- fetchSignal.delete.text = createHelper('DELETE', textTransformer);
477
- fetchSignal.delete.blob = createHelper('DELETE', blobTransformer);
478
- fetchSignal.delete.arrayBuffer = createHelper('DELETE', arrayBufferTransformer);
479
-
480
- class UserManagementService {
481
- auth = inject(AuthService);
482
- getUsers(search, accountTypes, sortBy, sortDirection, pageCount, pageOffset, createdAfter, createdBefore) {
483
- return fetchSignal(() => ({
484
- url: '/api/user-management',
485
- params: {
486
- search: search(),
487
- account_types: accountTypes().join(','),
488
- sort_by: sortBy(),
489
- sort_direction: sortDirection(),
490
- page_count: pageCount(),
491
- page_offset: pageOffset(),
492
- created_after: createdAfter && createdAfter().toISOString(),
493
- created_before: createdBefore && createdBefore().toISOString(),
494
- },
495
- headers: {
496
- Authorization: this.auth.bearerToken()
497
- }
498
- }), { autoRefresh: true });
499
- }
500
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UserManagementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
501
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UserManagementService, providedIn: 'root' });
502
- }
503
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UserManagementService, decorators: [{
504
- type: Injectable,
505
- args: [{
506
- providedIn: 'root'
507
- }]
508
- }] });
509
-
510
- class UserManagementComponent {
511
- api = inject(UserManagementService);
512
- displayedColumns = ['netId', 'preferredFirstName', 'preferredLastName', 'roles', 'accountType', 'created'];
513
- accountTypeOptions = ['NonBYU', 'Student', 'Employee'];
514
- paginator;
515
- sort;
516
- search = signal('');
517
- accountTypes = signal(['NonBYU', 'Student', 'Employee']);
518
- sortBy = signal('netId');
519
- sortDirection = signal('asc');
520
- pageCount = signal(5);
521
- pageOffset = signal(0);
522
- getUsers = this.api.getUsers(debounced(this.search), this.accountTypes, this.sortBy, this.sortDirection, this.pageCount, this.pageOffset);
523
- ngAfterViewInit() {
524
- // write the observables from the sort and the paging to the signals
525
- this.sort.sortChange.subscribe(({ active, direction }) => {
526
- this.sortBy.set(active);
527
- this.sortDirection.set(direction);
528
- this.pageOffset.set(0);
529
- this.paginator.pageIndex = 0;
530
- });
531
- this.paginator.page.subscribe(({ pageIndex, pageSize }) => {
532
- this.pageOffset.set(pageIndex * pageSize);
533
- this.pageCount.set(pageSize);
534
- });
535
- }
536
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UserManagementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
537
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: UserManagementComponent, isStandalone: true, selector: "app-user-management", viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], ngImport: i0, template: "<mat-form-field>\n <mat-label>Search for User</mat-label>\n <input matInput [(ngModel)]=\"search\">\n</mat-form-field>\n<mat-form-field>\n <mat-label>Account Type</mat-label>\n <mat-select multiple [(ngModel)]=\"accountTypes\">\n @for (accountType of accountTypeOptions; track accountType) {\n <mat-option [value]=\"accountType\">{{accountType}}</mat-option>\n }\n </mat-select>\n</mat-form-field>\n\n<table mat-table [dataSource]=\"getUsers.value()?.data ?? []\" matSort matSortActive=\"netId\" matSortDisableClear matSortDirection=\"asc\">\n <!-- NetID Column -->\n <ng-container matColumnDef=\"netId\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>NetID</th>\n <td mat-cell *matCellDef=\"let user\">{{user.netId}}</td>\n </ng-container>\n\n <!-- First Name Column -->\n <ng-container matColumnDef=\"preferredFirstName\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th>\n <td mat-cell *matCellDef=\"let user\">{{user.preferredFirstName}}</td>\n </ng-container>\n\n <!-- Last Name Column -->\n <ng-container matColumnDef=\"preferredLastName\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>\n <td mat-cell *matCellDef=\"let user\">{{user.preferredLastName}}</td>\n </ng-container>\n\n <!-- Roles Column -->\n <ng-container matColumnDef=\"roles\">\n <th mat-header-cell *matHeaderCellDef>Roles</th>\n <td mat-cell *matCellDef=\"let user\">{{user.roles.join(', ')}}</td>\n </ng-container>\n\n <!-- Account Type Column -->\n <ng-container matColumnDef=\"accountType\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>Account Type</th>\n <td mat-cell *matCellDef=\"let user\">{{user.accountType}}</td>\n </ng-container>\n\n <!-- Created Column -->\n <ng-container matColumnDef=\"created\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>\n Created\n </th>\n <td mat-cell *matCellDef=\"let user\">{{user.created | date}}</td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n</table>\n\n<mat-paginator [length]=\"getUsers.value()?.totalCount\" [pageSize]=\"pageCount()\" [pageSizeOptions]=\"[5, 10, 20]\" showFirstLastButtons></mat-paginator>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i1$1.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i1$1.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i1$1.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i1$1.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i1$1.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i1$1.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i1$1.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i1$1.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i1$1.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i1$1.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "ngmodule", type: MatSortModule }, { kind: "directive", type: i2.MatSort, selector: "[matSort]", inputs: ["matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear", "matSortDisabled"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i2.MatSortHeader, selector: "[mat-sort-header]", inputs: ["mat-sort-header", "arrowPosition", "start", "disabled", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "component", type: i3.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }] });
538
- }
539
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UserManagementComponent, decorators: [{
540
- type: Component,
541
- args: [{ selector: 'app-user-management', imports: [MatTableModule, MatSortModule, MatPaginatorModule, DatePipe, FormsModule, MatInputModule, MatSelectModule], template: "<mat-form-field>\n <mat-label>Search for User</mat-label>\n <input matInput [(ngModel)]=\"search\">\n</mat-form-field>\n<mat-form-field>\n <mat-label>Account Type</mat-label>\n <mat-select multiple [(ngModel)]=\"accountTypes\">\n @for (accountType of accountTypeOptions; track accountType) {\n <mat-option [value]=\"accountType\">{{accountType}}</mat-option>\n }\n </mat-select>\n</mat-form-field>\n\n<table mat-table [dataSource]=\"getUsers.value()?.data ?? []\" matSort matSortActive=\"netId\" matSortDisableClear matSortDirection=\"asc\">\n <!-- NetID Column -->\n <ng-container matColumnDef=\"netId\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>NetID</th>\n <td mat-cell *matCellDef=\"let user\">{{user.netId}}</td>\n </ng-container>\n\n <!-- First Name Column -->\n <ng-container matColumnDef=\"preferredFirstName\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th>\n <td mat-cell *matCellDef=\"let user\">{{user.preferredFirstName}}</td>\n </ng-container>\n\n <!-- Last Name Column -->\n <ng-container matColumnDef=\"preferredLastName\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>\n <td mat-cell *matCellDef=\"let user\">{{user.preferredLastName}}</td>\n </ng-container>\n\n <!-- Roles Column -->\n <ng-container matColumnDef=\"roles\">\n <th mat-header-cell *matHeaderCellDef>Roles</th>\n <td mat-cell *matCellDef=\"let user\">{{user.roles.join(', ')}}</td>\n </ng-container>\n\n <!-- Account Type Column -->\n <ng-container matColumnDef=\"accountType\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>Account Type</th>\n <td mat-cell *matCellDef=\"let user\">{{user.accountType}}</td>\n </ng-container>\n\n <!-- Created Column -->\n <ng-container matColumnDef=\"created\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header>\n Created\n </th>\n <td mat-cell *matCellDef=\"let user\">{{user.created | date}}</td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n</table>\n\n<mat-paginator [length]=\"getUsers.value()?.totalCount\" [pageSize]=\"pageCount()\" [pageSizeOptions]=\"[5, 10, 20]\" showFirstLastButtons></mat-paginator>\n" }]
542
- }], propDecorators: { paginator: [{
543
- type: ViewChild,
544
- args: [MatPaginator]
545
- }], sort: [{
546
- type: ViewChild,
547
- args: [MatSort]
548
- }] } });
549
-
550
263
  function trpcResource(procedure, input, options) {
551
264
  const currentInput = computed(input);
552
265
  const value = signal(options?.defaultValue, { equal: options?.equal });
@@ -606,26 +319,44 @@ function debugTrpcResource(_trpcResource) {
606
319
  };
607
320
  }
608
321
 
322
+ const debounced = (inputSignal, wait = 400) => {
323
+ const debouncedSignal = signal(inputSignal());
324
+ const setSignal = debounce((value) => debouncedSignal.set(value), wait);
325
+ effect(() => {
326
+ setSignal(inputSignal());
327
+ });
328
+ return debouncedSignal;
329
+ };
330
+ const debounce = (callback, wait) => {
331
+ let timeoutId;
332
+ return (...args) => {
333
+ window.clearTimeout(timeoutId);
334
+ timeoutId = window.setTimeout(() => {
335
+ callback(...args);
336
+ }, wait);
337
+ };
338
+ };
339
+
609
340
  const makeTableConfig = (config) => config;
610
341
 
611
342
  class FhssTableComponent {
612
343
  injector = inject(Injector);
613
344
  config = input.required();
614
345
  miscParams = input({});
615
- columnKeys = computed(() => {
616
- const keys = Object.keys(this.config().columns);
617
- if (this.config().displayIdColumn) {
618
- return keys;
619
- }
620
- return keys.filter((k) => k !== 'id');
621
- });
346
+ columnKeys = [];
347
+ columnsToDisplay = [];
622
348
  selection;
623
349
  selectedValues = model([]);
624
350
  dataResource;
625
351
  ngOnInit() {
626
- const interaction = this.config().interaction;
627
- if (interaction?.type === 'select') {
628
- this.selection = new SelectionModel(interaction.multi, undefined, undefined, (o1, o2) => o1.id === o2.id);
352
+ const cfg = this.config();
353
+ this.columnKeys = Object.keys(cfg.columns);
354
+ this.columnsToDisplay = cfg.displayIdColumn
355
+ ? this.columnKeys
356
+ : this.columnKeys.filter((k) => k !== 'id');
357
+ if (cfg.interaction?.type === 'select') {
358
+ this.columnsToDisplay.unshift('table-checkbox-column');
359
+ this.selection = new SelectionModel(cfg.interaction.multi, undefined, undefined, (o1, o2) => o1.id === o2.id);
629
360
  }
630
361
  this.dataResource = trpcResource(this.config().procedure, () => ({
631
362
  search: this.debouncedSearch(),
@@ -649,7 +380,7 @@ class FhssTableComponent {
649
380
  filters = signal({});
650
381
  debouncedFilters = debounced(this.filters);
651
382
  sortBy = linkedSignal(() => this.config().sorting.defaultSortBy);
652
- sortDirection = linkedSignal(() => this.config().sorting.defaultSortDirection);
383
+ sortDirection = linkedSignal(() => this.config().sorting.defaultSortDirection ?? 'asc');
653
384
  pageIndex = linkedSignal(() => {
654
385
  this.search();
655
386
  return 0;
@@ -662,7 +393,7 @@ class FhssTableComponent {
662
393
  }
663
394
  onSortChange(sort) {
664
395
  this.sortBy.set(sort.active || this.config().sorting.defaultSortBy);
665
- this.sortDirection.set(sort.direction || this.config().sorting.defaultSortDirection);
396
+ this.sortDirection.set(sort.direction || this.config().sorting.defaultSortDirection || 'asc');
666
397
  this.pageIndex.set(0);
667
398
  }
668
399
  onSelectionChange() {
@@ -707,7 +438,7 @@ class FhssTableComponent {
707
438
  return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`;
708
439
  }
709
440
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FhssTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
710
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FhssTableComponent, isStandalone: true, selector: "fhss-table", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, miscParams: { classPropertyName: "miscParams", publicName: "miscParams", isSignal: true, isRequired: false, transformFunction: null }, selectedValues: { classPropertyName: "selectedValues", publicName: "selectedValues", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedValues: "selectedValuesChange" }, ngImport: i0, template: "@if (dataResource.isLoading()) {\n <div class=\"shade\">\n <mat-spinner />\n </div>\n}\n@if (dataResource.error()) {\n <div class=\"shade\">\n <div class=\"error-msg\">There was an error retrieving this data</div>\n <button mat-button (click)=\"this.dataResource.refresh()\">Retry</button>\n </div>\n}\n\n@if (config().showSearch) {\n <mat-form-field>\n <input matInput [(ngModel)]=\"search\" placeholder=\"Find...\" />\n @if (search()) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"search.set('')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n}\n\n<table\n mat-table\n [dataSource]=\"dataResource.value()?.data ?? []\"\n matSort\n [matSortActive]=\"sortBy()\"\n [matSortDirection]=\"sortDirection()\"\n (matSortChange)=\"onSortChange($event)\"\n matSortDisableClear\n>\n @if (selection) {\n <ng-container matColumnDef=\"select\">\n <th mat-header-cell *matHeaderCellDef>\n @if (selection.isMultipleSelection()) {\n <mat-checkbox\n (change)=\"$event ? toggleAllRows() : null\"\n [checked]=\"selection.hasValue() && isAllSelected()\"\n [indeterminate]=\"selection.hasValue() && !isAllSelected()\"\n [aria-label]=\"checkboxLabel()\"\n />\n }\n </th>\n <td mat-cell *matCellDef=\"let row\">\n <mat-checkbox\n (click)=\"$event.stopPropagation()\"\n (change)=\"$event ? onRowClick(row) : null\"\n [checked]=\"selection.isSelected(row)\"\n [aria-label]=\"checkboxLabel(row)\"\n />\n </td>\n </ng-container>\n }\n\n @for (key of columnKeys(); track key) {\n <ng-container [matColumnDef]=\"key\">\n <mat-header-cell\n *matHeaderCellDef\n mat-sort-header\n [disabled]=\"!config().columns[key].allowSort\"\n >\n {{ config().columns[key].header }}\n @if (config().columns[key].individualFilter) {\n <mat-form-field>\n <input\n matInput\n [ngModel]=\"filters()[key] || ''\"\n (ngModelChange)=\"onFilterChange(key, $event)\"\n placeholder=\"Filter...\"\n />\n @if (filters()[key]) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"onFilterChange(key, '')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n }\n </mat-header-cell>\n <mat-cell *matCellDef=\"let row\">\n {{ row[key] }}\n </mat-cell>\n </ng-container>\n }\n\n <mat-header-row *matHeaderRowDef=\"columnKeys()\"></mat-header-row>\n @if (config().interaction) {\n <mat-row\n matRipple\n (click)=\"onRowClick(row)\"\n *matRowDef=\"let row; columns: columnKeys()\"\n ></mat-row>\n } @else {\n <mat-row *matRowDef=\"let row; columns: columnKeys()\"></mat-row>\n }\n\n <mat-row *matNoDataRow>\n <mat-cell colspan=\"4\">No data found</mat-cell>\n <button mat-button (click)=\"this.dataResource.refresh()\">Retry</button>\n </mat-row>\n</table>\n\n@if (!config().pagination.hideControls) {\n <mat-paginator\n [length]=\"dataResource.value()?.totalCount\"\n [showFirstLastButtons]=\"true\"\n [pageSize]=\"config().pagination.pageSize\"\n [hidePageSize]=\"true\"\n [pageIndex]=\"pageIndex()\"\n [disabled]=\"dataResource.isLoading()\"\n (page)=\"onPaginationChange($event)\"\n />\n}\n", styles: [".shade{position:absolute;inset:0 0 56px;background:#00000026;z-index:1;display:flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i1$1.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i1$1.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i1$1.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i1$1.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i1$1.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i1$1.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i1$1.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i1$1.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i1$1.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i1$1.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: i1$1.MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i2$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "component", type: i3.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatSortModule }, { kind: "directive", type: i2.MatSort, selector: "[matSort]", inputs: ["matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear", "matSortDisabled"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i2.MatSortHeader, selector: "[mat-sort-header]", inputs: ["mat-sort-header", "arrowPosition", "start", "disabled", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i9.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i10.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }] });
441
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FhssTableComponent, isStandalone: true, selector: "fhss-table", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, miscParams: { classPropertyName: "miscParams", publicName: "miscParams", isSignal: true, isRequired: false, transformFunction: null }, selectedValues: { classPropertyName: "selectedValues", publicName: "selectedValues", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedValues: "selectedValuesChange" }, ngImport: i0, template: "<div class=\"table-container\">\n @if (dataResource.isLoading() || dataResource.error()) {\n <div class=\"shade\">\n @if (dataResource.isLoading()) {\n <mat-spinner />\n }\n @if (dataResource.error()) {\n <div class=\"msg-bkgd\">\n <div class=\"error-msg\">There was an error retrieving this data</div>\n <button mat-button (click)=\"this.dataResource.refresh()\">\n Retry\n </button>\n </div>\n }\n </div>\n }\n\n @if (config().showSearch) {\n <mat-form-field class=\"search\">\n <input matInput [(ngModel)]=\"search\" placeholder=\"Find...\" />\n @if (search()) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"search.set('')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n }\n\n <div class=\"other-controls\">\n <ng-content />\n </div>\n\n <table\n mat-table\n [dataSource]=\"dataResource.value()?.data ?? []\"\n matSort\n [matSortActive]=\"sortBy()\"\n [matSortDirection]=\"sortDirection()\"\n (matSortChange)=\"onSortChange($event)\"\n matSortDisableClear\n >\n @if (selection) {\n <ng-container matColumnDef=\"table-checkbox-column\">\n <mat-header-cell *matHeaderCellDef>\n @if (selection.isMultipleSelection()) {\n <mat-checkbox\n (change)=\"$event ? toggleAllRows() : null\"\n [checked]=\"selection.hasValue() && isAllSelected()\"\n [indeterminate]=\"selection.hasValue() && !isAllSelected()\"\n [aria-label]=\"checkboxLabel()\"\n />\n }\n </mat-header-cell>\n <mat-cell *matCellDef=\"let row\">\n <mat-checkbox\n (click)=\"$event.stopPropagation()\"\n (change)=\"$event ? onRowClick(row) : null\"\n [checked]=\"selection.isSelected(row)\"\n [aria-label]=\"checkboxLabel(row)\"\n />\n </mat-cell>\n </ng-container>\n }\n\n @for (key of columnKeys; track key) {\n <ng-container [matColumnDef]=\"key\">\n <mat-header-cell\n *matHeaderCellDef\n mat-sort-header\n [disabled]=\"!config().columns[key].allowSort\"\n >\n <div class=\"header\">\n {{ config().columns[key].header }}\n @if (config().columns[key].individualFilter) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n matInput\n (click)=\"$event.stopPropagation()\"\n [ngModel]=\"filters()[key] || ''\"\n (ngModelChange)=\"onFilterChange(key, $event)\"\n placeholder=\"Filter...\"\n />\n @if (filters()[key]) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"onFilterChange(key, '')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n }\n </div>\n </mat-header-cell>\n <mat-cell *matCellDef=\"let row\">\n {{ row[key] }}\n </mat-cell>\n </ng-container>\n }\n\n <mat-header-row *matHeaderRowDef=\"columnsToDisplay\"></mat-header-row>\n @if (config().interaction) {\n <mat-row\n matRipple\n (click)=\"onRowClick(row)\"\n *matRowDef=\"let row; columns: columnsToDisplay\"\n ></mat-row>\n } @else {\n <mat-row *matRowDef=\"let row; columns: columnsToDisplay\"></mat-row>\n }\n\n <tr class=\"mat-row\" *matNoDataRow>\n <td class=\"mat-cell\">\n <div class=\"no-data\">\n No data found\n <button mat-button (click)=\"this.dataResource.refresh()\">\n Retry\n </button>\n </div>\n </td>\n </tr>\n </table>\n\n @if (!config().pagination.hideControls) {\n <mat-paginator\n [length]=\"dataResource.value()?.totalCount\"\n [showFirstLastButtons]=\"true\"\n [pageSize]=\"config().pagination.pageSize\"\n [hidePageSize]=\"true\"\n [pageIndex]=\"pageIndex()\"\n [disabled]=\"dataResource.isLoading()\"\n (page)=\"onPaginationChange($event)\"\n />\n }\n</div>\n", styles: [".table-container{position:relative}.shade{position:absolute;inset:0;background:#00000026;z-index:99;display:flex;align-items:center;justify-content:center}.msg-bkgd{background:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:15px 15px 5px}.search{background:#faf9fd;width:100%}.other-controls:not(:empty){background:#faf9fd}.mat-column-table-checkbox-column{background:#686868}.no-data{width:fit-content;margin:auto}\n"], dependencies: [{ kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i1$1.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i1$1.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i1$1.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i1$1.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i1$1.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i1$1.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i1$1.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i1$1.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i1$1.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i1$1.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: i1$1.MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i2.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "component", type: i3.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.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: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatSortModule }, { kind: "directive", type: i7.MatSort, selector: "[matSort]", inputs: ["matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear", "matSortDisabled"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i7.MatSortHeader, selector: "[mat-sort-header]", inputs: ["mat-sort-header", "arrowPosition", "start", "disabled", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i9.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i10.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }] });
711
442
  }
712
443
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FhssTableComponent, decorators: [{
713
444
  type: Component,
@@ -721,9 +452,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
721
452
  MatSortModule,
722
453
  MatIconModule,
723
454
  MatRippleModule,
724
- MatFormField,
725
455
  MatCheckboxModule,
726
- ], template: "@if (dataResource.isLoading()) {\n <div class=\"shade\">\n <mat-spinner />\n </div>\n}\n@if (dataResource.error()) {\n <div class=\"shade\">\n <div class=\"error-msg\">There was an error retrieving this data</div>\n <button mat-button (click)=\"this.dataResource.refresh()\">Retry</button>\n </div>\n}\n\n@if (config().showSearch) {\n <mat-form-field>\n <input matInput [(ngModel)]=\"search\" placeholder=\"Find...\" />\n @if (search()) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"search.set('')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n}\n\n<table\n mat-table\n [dataSource]=\"dataResource.value()?.data ?? []\"\n matSort\n [matSortActive]=\"sortBy()\"\n [matSortDirection]=\"sortDirection()\"\n (matSortChange)=\"onSortChange($event)\"\n matSortDisableClear\n>\n @if (selection) {\n <ng-container matColumnDef=\"select\">\n <th mat-header-cell *matHeaderCellDef>\n @if (selection.isMultipleSelection()) {\n <mat-checkbox\n (change)=\"$event ? toggleAllRows() : null\"\n [checked]=\"selection.hasValue() && isAllSelected()\"\n [indeterminate]=\"selection.hasValue() && !isAllSelected()\"\n [aria-label]=\"checkboxLabel()\"\n />\n }\n </th>\n <td mat-cell *matCellDef=\"let row\">\n <mat-checkbox\n (click)=\"$event.stopPropagation()\"\n (change)=\"$event ? onRowClick(row) : null\"\n [checked]=\"selection.isSelected(row)\"\n [aria-label]=\"checkboxLabel(row)\"\n />\n </td>\n </ng-container>\n }\n\n @for (key of columnKeys(); track key) {\n <ng-container [matColumnDef]=\"key\">\n <mat-header-cell\n *matHeaderCellDef\n mat-sort-header\n [disabled]=\"!config().columns[key].allowSort\"\n >\n {{ config().columns[key].header }}\n @if (config().columns[key].individualFilter) {\n <mat-form-field>\n <input\n matInput\n [ngModel]=\"filters()[key] || ''\"\n (ngModelChange)=\"onFilterChange(key, $event)\"\n placeholder=\"Filter...\"\n />\n @if (filters()[key]) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"onFilterChange(key, '')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n }\n </mat-header-cell>\n <mat-cell *matCellDef=\"let row\">\n {{ row[key] }}\n </mat-cell>\n </ng-container>\n }\n\n <mat-header-row *matHeaderRowDef=\"columnKeys()\"></mat-header-row>\n @if (config().interaction) {\n <mat-row\n matRipple\n (click)=\"onRowClick(row)\"\n *matRowDef=\"let row; columns: columnKeys()\"\n ></mat-row>\n } @else {\n <mat-row *matRowDef=\"let row; columns: columnKeys()\"></mat-row>\n }\n\n <mat-row *matNoDataRow>\n <mat-cell colspan=\"4\">No data found</mat-cell>\n <button mat-button (click)=\"this.dataResource.refresh()\">Retry</button>\n </mat-row>\n</table>\n\n@if (!config().pagination.hideControls) {\n <mat-paginator\n [length]=\"dataResource.value()?.totalCount\"\n [showFirstLastButtons]=\"true\"\n [pageSize]=\"config().pagination.pageSize\"\n [hidePageSize]=\"true\"\n [pageIndex]=\"pageIndex()\"\n [disabled]=\"dataResource.isLoading()\"\n (page)=\"onPaginationChange($event)\"\n />\n}\n", styles: [".shade{position:absolute;inset:0 0 56px;background:#00000026;z-index:1;display:flex;align-items:center;justify-content:center}\n"] }]
456
+ ], template: "<div class=\"table-container\">\n @if (dataResource.isLoading() || dataResource.error()) {\n <div class=\"shade\">\n @if (dataResource.isLoading()) {\n <mat-spinner />\n }\n @if (dataResource.error()) {\n <div class=\"msg-bkgd\">\n <div class=\"error-msg\">There was an error retrieving this data</div>\n <button mat-button (click)=\"this.dataResource.refresh()\">\n Retry\n </button>\n </div>\n }\n </div>\n }\n\n @if (config().showSearch) {\n <mat-form-field class=\"search\">\n <input matInput [(ngModel)]=\"search\" placeholder=\"Find...\" />\n @if (search()) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"search.set('')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n }\n\n <div class=\"other-controls\">\n <ng-content />\n </div>\n\n <table\n mat-table\n [dataSource]=\"dataResource.value()?.data ?? []\"\n matSort\n [matSortActive]=\"sortBy()\"\n [matSortDirection]=\"sortDirection()\"\n (matSortChange)=\"onSortChange($event)\"\n matSortDisableClear\n >\n @if (selection) {\n <ng-container matColumnDef=\"table-checkbox-column\">\n <mat-header-cell *matHeaderCellDef>\n @if (selection.isMultipleSelection()) {\n <mat-checkbox\n (change)=\"$event ? toggleAllRows() : null\"\n [checked]=\"selection.hasValue() && isAllSelected()\"\n [indeterminate]=\"selection.hasValue() && !isAllSelected()\"\n [aria-label]=\"checkboxLabel()\"\n />\n }\n </mat-header-cell>\n <mat-cell *matCellDef=\"let row\">\n <mat-checkbox\n (click)=\"$event.stopPropagation()\"\n (change)=\"$event ? onRowClick(row) : null\"\n [checked]=\"selection.isSelected(row)\"\n [aria-label]=\"checkboxLabel(row)\"\n />\n </mat-cell>\n </ng-container>\n }\n\n @for (key of columnKeys; track key) {\n <ng-container [matColumnDef]=\"key\">\n <mat-header-cell\n *matHeaderCellDef\n mat-sort-header\n [disabled]=\"!config().columns[key].allowSort\"\n >\n <div class=\"header\">\n {{ config().columns[key].header }}\n @if (config().columns[key].individualFilter) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n matInput\n (click)=\"$event.stopPropagation()\"\n [ngModel]=\"filters()[key] || ''\"\n (ngModelChange)=\"onFilterChange(key, $event)\"\n placeholder=\"Filter...\"\n />\n @if (filters()[key]) {\n <button\n matSuffix\n mat-icon-button\n aria-label=\"Clear\"\n (click)=\"onFilterChange(key, '')\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-form-field>\n }\n </div>\n </mat-header-cell>\n <mat-cell *matCellDef=\"let row\">\n {{ row[key] }}\n </mat-cell>\n </ng-container>\n }\n\n <mat-header-row *matHeaderRowDef=\"columnsToDisplay\"></mat-header-row>\n @if (config().interaction) {\n <mat-row\n matRipple\n (click)=\"onRowClick(row)\"\n *matRowDef=\"let row; columns: columnsToDisplay\"\n ></mat-row>\n } @else {\n <mat-row *matRowDef=\"let row; columns: columnsToDisplay\"></mat-row>\n }\n\n <tr class=\"mat-row\" *matNoDataRow>\n <td class=\"mat-cell\">\n <div class=\"no-data\">\n No data found\n <button mat-button (click)=\"this.dataResource.refresh()\">\n Retry\n </button>\n </div>\n </td>\n </tr>\n </table>\n\n @if (!config().pagination.hideControls) {\n <mat-paginator\n [length]=\"dataResource.value()?.totalCount\"\n [showFirstLastButtons]=\"true\"\n [pageSize]=\"config().pagination.pageSize\"\n [hidePageSize]=\"true\"\n [pageIndex]=\"pageIndex()\"\n [disabled]=\"dataResource.isLoading()\"\n (page)=\"onPaginationChange($event)\"\n />\n }\n</div>\n", styles: [".table-container{position:relative}.shade{position:absolute;inset:0;background:#00000026;z-index:99;display:flex;align-items:center;justify-content:center}.msg-bkgd{background:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:15px 15px 5px}.search{background:#faf9fd;width:100%}.other-controls:not(:empty){background:#faf9fd}.mat-column-table-checkbox-column{background:#686868}.no-data{width:fit-content;margin:auto}\n"] }]
727
457
  }] });
728
458
 
729
459
  const provideFhss = (config) => ({
@@ -811,6 +541,202 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
811
541
  args: [{ selector: 'fhss-not-found', imports: [MatButtonModule, ByuHeaderComponent, ByuFooterComponent], template: "<byu-header />\n\n<div class=\"container\">\n <div class=\"not-found\">\n <img src=\"/confused-duck.png\" alt=\"\" />\n <h1>404 - Page not found</h1>\n <p>The page you are looking for doesn't exist or it may have moved.</p>\n <a mat-flat-button href=\"/\">Go back to home</a>\n </div>\n</div>\n\n<byu-footer />\n", styles: [":host{height:100vh;display:flex;flex-direction:column}.container{flex:1;display:flex;align-items:center;justify-content:center}.container .not-found{text-align:center;padding-bottom:3em}\n"] }]
812
542
  }] });
813
543
 
544
+ /**
545
+ * Creates a reactive fetch signal.
546
+ *
547
+ * The request function can include Signals for properties. These are unwrapped in the refresh function.
548
+ */
549
+ function createFetchSignal(request, method, transform, options) {
550
+ // Use a computed signal so that any changes to Signals in the request object trigger updates.
551
+ const currentRequest = computed(request);
552
+ const value = signal(options?.defaultValue, { equal: options?.equal });
553
+ const errorResponse = signal(undefined);
554
+ const isLoading = signal(false);
555
+ const statusCode = signal(undefined);
556
+ const headers = signal(undefined);
557
+ const status = signal('idle');
558
+ const error = signal(undefined);
559
+ const injector = inject(Injector);
560
+ let effectRef = undefined;
561
+ if (options?.autoRefresh) {
562
+ effectRef = effect((onCleanup) => {
563
+ // pass abort signal to refresh on cleanup of effect
564
+ const controller = new AbortController();
565
+ onCleanup(() => controller.abort());
566
+ // call refresh with this abort controller
567
+ refresh(controller.signal, true);
568
+ }, { injector: options?.injector || injector });
569
+ }
570
+ const refresh = async (abortSignal, keepLoadingThroughAbort) => {
571
+ // if the fetchSignal has been destroyed, do nothing
572
+ if (untracked(status) === 'destroyed')
573
+ return;
574
+ // Reset signals for a fresh request.
575
+ isLoading.set(true);
576
+ errorResponse.set(undefined);
577
+ statusCode.set(undefined);
578
+ headers.set(undefined);
579
+ status.set('loading');
580
+ error.set(undefined);
581
+ // Unwrap the current request.
582
+ const req = currentRequest();
583
+ const url = req.url;
584
+ const params = req.params;
585
+ const requestHeaders = req.headers;
586
+ const body = req.body;
587
+ // Build URL with query parameters.
588
+ let uri = url;
589
+ if (params) {
590
+ const searchParams = new URLSearchParams();
591
+ for (const key in params) {
592
+ if (params[key] !== undefined)
593
+ searchParams.append(key, String(params[key]));
594
+ }
595
+ uri += (uri.includes('?') ? '&' : '?') + searchParams.toString();
596
+ }
597
+ // Filter out undefined header values
598
+ const filteredHeaders = requestHeaders
599
+ ? Object.fromEntries(Object.entries(requestHeaders).filter(([, value]) => value !== undefined))
600
+ : undefined;
601
+ try {
602
+ // send the request
603
+ const response = await fetch(uri, {
604
+ method,
605
+ headers: filteredHeaders,
606
+ // Only include a body if one is provided.
607
+ body: body,
608
+ signal: abortSignal,
609
+ });
610
+ // set the status code
611
+ statusCode.set(response.status);
612
+ // Extract response headers.
613
+ const headersObj = {};
614
+ response.headers.forEach((val, key) => {
615
+ headersObj[key] = val;
616
+ });
617
+ headers.set(headersObj);
618
+ // if the response is ok, transform the body
619
+ if (response.ok) {
620
+ value.set(await transform(response));
621
+ status.set('resolved');
622
+ }
623
+ else {
624
+ // try to parse the error as json
625
+ try {
626
+ errorResponse.set(await response.json());
627
+ value.set(undefined);
628
+ status.set('resolved');
629
+ }
630
+ catch {
631
+ throw new Error('Unable to parse error response.');
632
+ }
633
+ }
634
+ }
635
+ catch (err) {
636
+ if (err instanceof Error) {
637
+ // if the fetch request was aborted
638
+ // we check if we would like to continue loading (the next request)
639
+ // if so then we just leave this refresh in an undefined state
640
+ // else we error as usual
641
+ if (err.name === 'AbortError' && keepLoadingThroughAbort) {
642
+ return;
643
+ }
644
+ error.set(err);
645
+ }
646
+ else {
647
+ // Fallback for non-Error values
648
+ error.set(new Error(String(err)));
649
+ }
650
+ value.set(undefined);
651
+ status.set('error');
652
+ }
653
+ isLoading.set(false);
654
+ };
655
+ const destroy = () => {
656
+ // if the fetchSignal has been destroyed, do nothing
657
+ if (status() === 'destroyed')
658
+ return;
659
+ if (effectRef) {
660
+ effectRef.destroy();
661
+ }
662
+ status.set('destroyed');
663
+ value.set(undefined);
664
+ errorResponse.set(undefined);
665
+ isLoading.set(false);
666
+ statusCode.set(undefined);
667
+ headers.set(undefined);
668
+ error.set(undefined);
669
+ };
670
+ return {
671
+ value,
672
+ errorResponse,
673
+ isLoading,
674
+ statusCode,
675
+ headers,
676
+ status,
677
+ error,
678
+ refresh,
679
+ destroy
680
+ };
681
+ }
682
+ //
683
+ // Helpers for attaching response transforms.
684
+ //
685
+ const createHelper = (method, transform) => (request, options) => createFetchSignal(request, method, transform, options);
686
+ // Transforms
687
+ const jsonTransformer = (response) => response.json();
688
+ const textTransformer = (response) => response.text();
689
+ const blobTransformer = (response) => response.blob();
690
+ const arrayBufferTransformer = (response) => response.arrayBuffer();
691
+ //
692
+ // Build the defaults - GET chain
693
+ //
694
+ const fetchSignal = createHelper('GET', jsonTransformer);
695
+ fetchSignal.json = createHelper('GET', jsonTransformer);
696
+ fetchSignal.text = createHelper('GET', textTransformer);
697
+ fetchSignal.blob = createHelper('GET', blobTransformer);
698
+ fetchSignal.arrayBuffer = createHelper('GET', arrayBufferTransformer);
699
+ //
700
+ // Build the GET chain
701
+ //
702
+ fetchSignal.get = createHelper('GET', jsonTransformer);
703
+ fetchSignal.get.json = createHelper('GET', jsonTransformer);
704
+ fetchSignal.get.text = createHelper('GET', textTransformer);
705
+ fetchSignal.get.blob = createHelper('GET', blobTransformer);
706
+ fetchSignal.get.arrayBuffer = createHelper('GET', arrayBufferTransformer);
707
+ //
708
+ // Build the POST chain.
709
+ //
710
+ fetchSignal.post = createHelper('POST', jsonTransformer);
711
+ fetchSignal.post.json = createHelper('POST', jsonTransformer);
712
+ fetchSignal.post.text = createHelper('POST', textTransformer);
713
+ fetchSignal.post.blob = createHelper('POST', blobTransformer);
714
+ fetchSignal.post.arrayBuffer = createHelper('POST', arrayBufferTransformer);
715
+ //
716
+ // Build the PUT chain.
717
+ //
718
+ fetchSignal.put = createHelper('PUT', jsonTransformer);
719
+ fetchSignal.put.json = createHelper('PUT', jsonTransformer);
720
+ fetchSignal.put.text = createHelper('PUT', textTransformer);
721
+ fetchSignal.put.blob = createHelper('PUT', blobTransformer);
722
+ fetchSignal.put.arrayBuffer = createHelper('PUT', arrayBufferTransformer);
723
+ //
724
+ // Build the PATCH chain.
725
+ //
726
+ fetchSignal.patch = createHelper('PATCH', jsonTransformer);
727
+ fetchSignal.patch.json = createHelper('PATCH', jsonTransformer);
728
+ fetchSignal.patch.text = createHelper('PATCH', textTransformer);
729
+ fetchSignal.patch.blob = createHelper('PATCH', blobTransformer);
730
+ fetchSignal.patch.arrayBuffer = createHelper('PATCH', arrayBufferTransformer);
731
+ //
732
+ // Build the DELETE chain
733
+ //
734
+ fetchSignal.delete = createHelper('DELETE', jsonTransformer);
735
+ fetchSignal.delete.json = createHelper('DELETE', jsonTransformer);
736
+ fetchSignal.delete.text = createHelper('DELETE', textTransformer);
737
+ fetchSignal.delete.blob = createHelper('DELETE', blobTransformer);
738
+ fetchSignal.delete.arrayBuffer = createHelper('DELETE', arrayBufferTransformer);
739
+
814
740
  /**
815
741
  * Components
816
742
  */
@@ -819,5 +745,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
819
745
  * Generated bundle index. Do not edit.
820
746
  */
821
747
 
822
- export { AuthCallbackPage, AuthErrorPage, AuthService, ByuFooterComponent, ByuHeaderComponent, FHSS_CONFIG, FhssTableComponent, ForbiddenPage, NotFoundPage, UserManagementComponent, authGuard, debounced, debugTrpcResource, fetchSignal, makeTableConfig, provideFhss, roleGuardFactory, trpcResource };
748
+ export { AuthCallbackPage, AuthErrorPage, AuthService, ByuFooterComponent, ByuHeaderComponent, FHSS_CONFIG, FhssTableComponent, ForbiddenPage, NotFoundPage, authGuard, debounced, debugTrpcResource, fetchSignal, makeTableConfig, provideFhss, roleGuardFactory, trpcResource };
823
749
  //# sourceMappingURL=fhss-web-team-frontend-utils.mjs.map