@fhss-web-team/frontend-utils 1.6.1 → 1.6.3

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Component, InjectionToken, inject, Injector, signal, afterNextRender, effect, computed, Injectable, input, ViewChild } from '@angular/core';
2
+ import { Component, InjectionToken, inject, Injector, signal, afterNextRender, effect, computed, Injectable, input, untracked, ViewChild } 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';
@@ -19,6 +19,7 @@ import * as i6 from '@angular/material/select';
19
19
  import { MatSelectModule } from '@angular/material/select';
20
20
  import * as i1$2 from '@angular/material/button';
21
21
  import { MatButtonModule } from '@angular/material/button';
22
+ import { isTRPCClientError } from '@trpc/client';
22
23
 
23
24
  class ByuFooterComponent {
24
25
  currentYear = new Date().getFullYear(); // Automatically updates the year
@@ -271,42 +272,43 @@ const debounce = (callback, wait) => {
271
272
  };
272
273
  };
273
274
 
274
- /**
275
- * Reads the underlying value from a MaybeSignal.
276
- *
277
- * This function is designed for use with reactive APIs (like fetchSignal) where it's important
278
- * that Angular's computed methods register the dependency. If the input is a Signal, invoking it
279
- * not only returns its current value but also tracks the signal for reactivity. If the input is
280
- * a plain value, it simply returns that value.
281
- *
282
- * @template T The type of the value.
283
- * @param value A plain value or a Signal that yields the value.
284
- * @returns The current value, with dependency tracking enabled if the input is a Signal.
285
- */
286
- function readMaybeSignal(value) {
287
- return typeof value === 'function' ? value() : value;
288
- }
289
275
  /**
290
276
  * Creates a reactive fetch signal.
291
277
  *
292
278
  * The request function can include Signals for properties. These are unwrapped in the refresh function.
293
279
  */
294
- function createFetchSignal(request, method, transform, autoRefresh) {
280
+ function createFetchSignal(request, method, transform, options) {
295
281
  // Use a computed signal so that any changes to Signals in the request object trigger updates.
296
282
  const currentRequest = computed(request);
297
- const value = signal(undefined);
298
- const persistentValue = signal(undefined);
283
+ const value = signal(options?.defaultValue, { equal: options?.equal });
284
+ const errorResponse = signal(undefined);
299
285
  const isLoading = signal(false);
300
- const error = signal(undefined);
301
286
  const statusCode = signal(undefined);
302
287
  const headers = signal(undefined);
288
+ const status = signal('idle');
289
+ const error = signal(undefined);
290
+ const injector = inject(Injector);
291
+ let effectRef = undefined;
292
+ if (options?.autoRefresh) {
293
+ effectRef = effect((onCleanup) => {
294
+ // pass abort signal to refresh on cleanup of effect
295
+ const controller = new AbortController();
296
+ onCleanup(() => controller.abort());
297
+ // call refresh with this abort controller
298
+ refresh(controller.signal, true);
299
+ }, { injector: options?.injector || injector });
300
+ }
303
301
  const refresh = async (abortSignal, keepLoadingThroughAbort) => {
302
+ // if the fetchSignal has been destroyed, do nothing
303
+ if (untracked(status) === 'destroyed')
304
+ return;
304
305
  // Reset signals for a fresh request.
305
- value.set(undefined);
306
306
  isLoading.set(true);
307
- error.set(undefined);
307
+ errorResponse.set(undefined);
308
308
  statusCode.set(undefined);
309
309
  headers.set(undefined);
310
+ status.set('loading');
311
+ error.set(undefined);
310
312
  // Unwrap the current request.
311
313
  const req = currentRequest();
312
314
  const url = req.url;
@@ -323,14 +325,20 @@ function createFetchSignal(request, method, transform, autoRefresh) {
323
325
  }
324
326
  uri += (uri.includes('?') ? '&' : '?') + searchParams.toString();
325
327
  }
328
+ // Filter out undefined header values
329
+ const filteredHeaders = requestHeaders
330
+ ? Object.fromEntries(Object.entries(requestHeaders).filter(([, value]) => value !== undefined))
331
+ : undefined;
326
332
  try {
333
+ // send the request
327
334
  const response = await fetch(uri, {
328
335
  method,
329
- headers: requestHeaders,
336
+ headers: filteredHeaders,
330
337
  // Only include a body if one is provided.
331
- body: body !== undefined ? JSON.stringify(body) : undefined,
338
+ body: body,
332
339
  signal: abortSignal,
333
340
  });
341
+ // set the status code
334
342
  statusCode.set(response.status);
335
343
  // Extract response headers.
336
344
  const headersObj = {};
@@ -341,57 +349,71 @@ function createFetchSignal(request, method, transform, autoRefresh) {
341
349
  // if the response is ok, transform the body
342
350
  if (response.ok) {
343
351
  value.set(await transform(response));
344
- error.set(null);
352
+ status.set('resolved');
345
353
  }
346
354
  else {
347
355
  // try to parse the error as json
348
- // set the error to null if this fails
349
356
  try {
350
- error.set(await response.json());
357
+ errorResponse.set(await response.json());
358
+ value.set(undefined);
359
+ status.set('resolved');
351
360
  }
352
361
  catch {
353
- error.set(null);
354
- }
355
- finally {
356
- value.set(null);
362
+ throw new Error('Unable to parse error response.');
357
363
  }
358
364
  }
359
365
  }
360
366
  catch (err) {
361
- if (err.name === 'AbortError' && keepLoadingThroughAbort) {
362
- return;
367
+ if (err instanceof Error) {
368
+ // if the fetch request was aborted
369
+ // we check if we would like to continue loading (the next request)
370
+ // if so then we just leave this refresh in an undefined state
371
+ // else we error as usual
372
+ if (err.name === 'AbortError' && keepLoadingThroughAbort) {
373
+ return;
374
+ }
375
+ error.set(err);
376
+ }
377
+ else {
378
+ // Fallback for non-Error values
379
+ error.set(new Error(String(err)));
363
380
  }
364
- error.set(null);
365
- value.set(null);
381
+ value.set(undefined);
382
+ status.set('error');
366
383
  }
367
- persistentValue.set(value());
368
384
  isLoading.set(false);
369
385
  };
370
- if (autoRefresh) {
371
- effect((onCleanup) => {
372
- // read the current request to trigger re-run of this effect
373
- currentRequest();
374
- // pass abort signal to refresh on cleanup of effect
375
- const controller = new AbortController();
376
- onCleanup(() => controller.abort());
377
- // call refresh with this abort controller
378
- refresh(controller.signal, true);
379
- });
380
- }
386
+ const destroy = () => {
387
+ // if the fetchSignal has been destroyed, do nothing
388
+ if (status() === 'destroyed')
389
+ return;
390
+ if (effectRef) {
391
+ effectRef.destroy();
392
+ }
393
+ status.set('destroyed');
394
+ value.set(undefined);
395
+ errorResponse.set(undefined);
396
+ isLoading.set(false);
397
+ statusCode.set(undefined);
398
+ headers.set(undefined);
399
+ error.set(undefined);
400
+ };
381
401
  return {
382
402
  value,
383
- persistentValue,
403
+ errorResponse,
384
404
  isLoading,
385
- error,
386
405
  statusCode,
387
406
  headers,
407
+ status,
408
+ error,
388
409
  refresh,
410
+ destroy
389
411
  };
390
412
  }
391
413
  //
392
- // Helpers for attaching response transforms for GET/DELETE (which don’t include a body).
414
+ // Helpers for attaching response transforms.
393
415
  //
394
- const createHelper = (method, transform) => (request, autoRefresh = false) => createFetchSignal(request, method, transform, autoRefresh);
416
+ const createHelper = (method, transform) => (request, options) => createFetchSignal(request, method, transform, options);
395
417
  // Transforms
396
418
  const jsonTransformer = (response) => response.json();
397
419
  const textTransformer = (response) => response.text();
@@ -462,9 +484,9 @@ class UserManagementService {
462
484
  created_before: createdBefore && createdBefore().toISOString(),
463
485
  },
464
486
  headers: {
465
- Authorization: this.auth.bearerToken() ?? ''
487
+ Authorization: this.auth.bearerToken()
466
488
  }
467
- }), true);
489
+ }), { autoRefresh: true });
468
490
  }
469
491
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
470
492
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementService, providedIn: 'root' });
@@ -503,11 +525,11 @@ class UserManagementComponent {
503
525
  });
504
526
  }
505
527
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
506
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.10", 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.persistentValue()?.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.persistentValue()?.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"] }] });
528
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.10", 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"] }] });
507
529
  }
508
530
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementComponent, decorators: [{
509
531
  type: Component,
510
- 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.persistentValue()?.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.persistentValue()?.totalCount\" [pageSize]=\"pageCount()\" [pageSizeOptions]=\"[5, 10, 20]\" showFirstLastButtons></mat-paginator>\n" }]
532
+ 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" }]
511
533
  }], propDecorators: { paginator: [{
512
534
  type: ViewChild,
513
535
  args: [MatPaginator]
@@ -605,6 +627,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImpo
605
627
  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"] }]
606
628
  }] });
607
629
 
630
+ function trpcResource(procedure, input, options) {
631
+ const currentInput = computed(input);
632
+ const value = signal(options?.defaultValue, { equal: options?.equal });
633
+ const error = signal(undefined);
634
+ const isLoading = signal(false);
635
+ const resourceError = signal(undefined);
636
+ const injector = inject(Injector);
637
+ let effectRef = undefined;
638
+ if (options?.autoRefresh) {
639
+ effectRef = effect((onCleanup) => {
640
+ // pass abort signal to refresh on cleanup of effect
641
+ const controller = new AbortController();
642
+ onCleanup(() => controller.abort());
643
+ // call refresh with this abort controller
644
+ // refresh reads currentInput which triggers the effect
645
+ refresh(controller.signal, true);
646
+ }, { injector: options?.injector || injector });
647
+ }
648
+ const refresh = async (abortSignal, keepLoadingThroughAbort = true) => {
649
+ // Reset signals for a fresh request.
650
+ isLoading.set(true);
651
+ error.set(undefined);
652
+ resourceError.set(undefined);
653
+ try {
654
+ value.set(await procedure(currentInput(), {
655
+ signal: abortSignal
656
+ }));
657
+ error.set(undefined);
658
+ }
659
+ catch (err) {
660
+ if (isTRPCClientError(err)) {
661
+ // if the trpc request was aborted
662
+ // we check if we would like to continue loading (the next request)
663
+ // if so then we just leave this refresh in an undefined state
664
+ // else we error as usual
665
+ if (err.cause?.name === 'AbortError' && keepLoadingThroughAbort) {
666
+ return;
667
+ }
668
+ error.set(err);
669
+ }
670
+ else if (err instanceof Error) {
671
+ resourceError.set(err);
672
+ }
673
+ else {
674
+ // Fallback for non-Error values
675
+ resourceError.set(new Error(String(err)));
676
+ }
677
+ value.set(options?.defaultValue);
678
+ }
679
+ isLoading.set(false);
680
+ };
681
+ return {
682
+ value,
683
+ error,
684
+ isLoading,
685
+ resourceError,
686
+ refresh
687
+ };
688
+ }
689
+ function debugTrpcResource(_trpcResource) {
690
+ return {
691
+ value: _trpcResource.value(),
692
+ error: _trpcResource.error(),
693
+ isLoading: _trpcResource.isLoading(),
694
+ resourceError: _trpcResource.resourceError(),
695
+ };
696
+ }
697
+
608
698
  /**
609
699
  * Components
610
700
  */
@@ -613,5 +703,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImpo
613
703
  * Generated bundle index. Do not edit.
614
704
  */
615
705
 
616
- export { AuthCallbackPage, AuthErrorPage, AuthService, ByuFooterComponent, ByuHeaderComponent, FHSS_CONFIG, ForbiddenPage, NotFoundPage, UserManagementComponent, authGuard, debounced, enumToRecord, fetchSignal, provideFhss, readMaybeSignal, roleGuardFactory };
706
+ export { AuthCallbackPage, AuthErrorPage, AuthService, ByuFooterComponent, ByuHeaderComponent, FHSS_CONFIG, ForbiddenPage, NotFoundPage, UserManagementComponent, authGuard, debounced, debugTrpcResource, enumToRecord, fetchSignal, provideFhss, roleGuardFactory, trpcResource };
617
707
  //# sourceMappingURL=fhss-web-team-frontend-utils.mjs.map