@fhss-web-team/frontend-utils 1.6.0 → 1.6.2

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,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Component, InjectionToken, inject, computed, Injectable, input, signal, effect, 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
- import { KEYCLOAK_EVENT_SIGNAL } from 'keycloak-angular';
5
+ import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType, typeEventArgs } from 'keycloak-angular';
6
6
  import Keycloak from 'keycloak-js';
7
7
  import * as i1$1 from '@angular/material/table';
8
8
  import { MatTableModule } from '@angular/material/table';
@@ -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
@@ -42,6 +43,25 @@ class AuthService {
42
43
  router = inject(Router);
43
44
  config = inject(FHSS_CONFIG);
44
45
  nextUriKey = 'nextUri';
46
+ injector = inject(Injector);
47
+ /**
48
+ * Signal indicating whether the user is authenticated.
49
+ * Returns `true` if authenticated, `false` otherwise.
50
+ */
51
+ authenticated = signal(false);
52
+ constructor() {
53
+ afterNextRender(() => {
54
+ effect(() => {
55
+ const kcEvent = this.keycloakSignal();
56
+ if (kcEvent.type === KeycloakEventType.Ready) {
57
+ this.authenticated.set(typeEventArgs(kcEvent.args));
58
+ }
59
+ if (kcEvent.type === KeycloakEventType.AuthLogout) {
60
+ this.authenticated.set(false);
61
+ }
62
+ }, { injector: this.injector });
63
+ });
64
+ }
45
65
  /**
46
66
  * Initiates the login process. Optionally stores a route to redirect to after login.
47
67
  * @param nextUri - The route to navigate to after successful login.
@@ -84,7 +104,7 @@ class AuthService {
84
104
  this.router.navigate(['/']);
85
105
  return;
86
106
  }
87
- const res = await fetch('/api/auth/callback', {
107
+ const res = await fetch(window.location.origin + '/api/auth/callback', {
88
108
  headers: { Authorization: this.bearerToken() ?? '' },
89
109
  });
90
110
  if (!res.ok)
@@ -105,14 +125,6 @@ class AuthService {
105
125
  logout = (nextUri) => this.keycloak.logout({
106
126
  redirectUri: window.location.origin + (nextUri ?? '/'),
107
127
  });
108
- /**
109
- * Signal indicating whether the user is authenticated.
110
- * Returns `true` if authenticated, `false` otherwise.
111
- */
112
- authenticated = computed(() => {
113
- this.keycloakSignal();
114
- return this.keycloak.authenticated;
115
- });
116
128
  /**
117
129
  * Signal for the base64-encoded token used in the `Authorization` header.
118
130
  */
@@ -216,7 +228,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImpo
216
228
  args: [{
217
229
  providedIn: 'root',
218
230
  }]
219
- }] });
231
+ }], ctorParameters: () => [] });
220
232
 
221
233
  class ByuHeaderComponent {
222
234
  auth = inject(AuthService);
@@ -260,42 +272,43 @@ const debounce = (callback, wait) => {
260
272
  };
261
273
  };
262
274
 
263
- /**
264
- * Reads the underlying value from a MaybeSignal.
265
- *
266
- * This function is designed for use with reactive APIs (like fetchSignal) where it's important
267
- * that Angular's computed methods register the dependency. If the input is a Signal, invoking it
268
- * not only returns its current value but also tracks the signal for reactivity. If the input is
269
- * a plain value, it simply returns that value.
270
- *
271
- * @template T The type of the value.
272
- * @param value A plain value or a Signal that yields the value.
273
- * @returns The current value, with dependency tracking enabled if the input is a Signal.
274
- */
275
- function readMaybeSignal(value) {
276
- return typeof value === 'function' ? value() : value;
277
- }
278
275
  /**
279
276
  * Creates a reactive fetch signal.
280
277
  *
281
278
  * The request function can include Signals for properties. These are unwrapped in the refresh function.
282
279
  */
283
- function createFetchSignal(request, method, transform, autoRefresh) {
280
+ function createFetchSignal(request, method, transform, options) {
284
281
  // Use a computed signal so that any changes to Signals in the request object trigger updates.
285
282
  const currentRequest = computed(request);
286
- const value = signal(undefined);
287
- const persistentValue = signal(undefined);
283
+ const value = signal(options?.defaultValue, { equal: options?.equal });
284
+ const errorResponse = signal(undefined);
288
285
  const isLoading = signal(false);
289
- const error = signal(undefined);
290
286
  const statusCode = signal(undefined);
291
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
+ }
292
301
  const refresh = async (abortSignal, keepLoadingThroughAbort) => {
302
+ // if the fetchSignal has been destroyed, do nothing
303
+ if (untracked(status) === 'destroyed')
304
+ return;
293
305
  // Reset signals for a fresh request.
294
- value.set(undefined);
295
306
  isLoading.set(true);
296
- error.set(undefined);
307
+ errorResponse.set(undefined);
297
308
  statusCode.set(undefined);
298
309
  headers.set(undefined);
310
+ status.set('loading');
311
+ error.set(undefined);
299
312
  // Unwrap the current request.
300
313
  const req = currentRequest();
301
314
  const url = req.url;
@@ -312,14 +325,20 @@ function createFetchSignal(request, method, transform, autoRefresh) {
312
325
  }
313
326
  uri += (uri.includes('?') ? '&' : '?') + searchParams.toString();
314
327
  }
328
+ // Filter out undefined header values
329
+ const filteredHeaders = requestHeaders
330
+ ? Object.fromEntries(Object.entries(requestHeaders).filter(([, value]) => value !== undefined))
331
+ : undefined;
315
332
  try {
333
+ // send the request
316
334
  const response = await fetch(uri, {
317
335
  method,
318
- headers: requestHeaders,
336
+ headers: filteredHeaders,
319
337
  // Only include a body if one is provided.
320
- body: body !== undefined ? JSON.stringify(body) : undefined,
338
+ body: body,
321
339
  signal: abortSignal,
322
340
  });
341
+ // set the status code
323
342
  statusCode.set(response.status);
324
343
  // Extract response headers.
325
344
  const headersObj = {};
@@ -330,57 +349,71 @@ function createFetchSignal(request, method, transform, autoRefresh) {
330
349
  // if the response is ok, transform the body
331
350
  if (response.ok) {
332
351
  value.set(await transform(response));
333
- error.set(null);
352
+ status.set('resolved');
334
353
  }
335
354
  else {
336
355
  // try to parse the error as json
337
- // set the error to null if this fails
338
356
  try {
339
- error.set(await response.json());
357
+ errorResponse.set(await response.json());
358
+ value.set(undefined);
359
+ status.set('resolved');
340
360
  }
341
361
  catch {
342
- error.set(null);
343
- }
344
- finally {
345
- value.set(null);
362
+ throw new Error('Unable to parse error response.');
346
363
  }
347
364
  }
348
365
  }
349
366
  catch (err) {
350
- if (err.name === 'AbortError' && keepLoadingThroughAbort) {
351
- 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)));
352
380
  }
353
- error.set(null);
354
- value.set(null);
381
+ value.set(undefined);
382
+ status.set('error');
355
383
  }
356
- persistentValue.set(value());
357
384
  isLoading.set(false);
358
385
  };
359
- if (autoRefresh) {
360
- effect((onCleanup) => {
361
- // read the current request to trigger re-run of this effect
362
- currentRequest();
363
- // pass abort signal to refresh on cleanup of effect
364
- const controller = new AbortController();
365
- onCleanup(() => controller.abort());
366
- // call refresh with this abort controller
367
- refresh(controller.signal, true);
368
- });
369
- }
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
+ };
370
401
  return {
371
402
  value,
372
- persistentValue,
403
+ errorResponse,
373
404
  isLoading,
374
- error,
375
405
  statusCode,
376
406
  headers,
407
+ status,
408
+ error,
377
409
  refresh,
410
+ destroy
378
411
  };
379
412
  }
380
413
  //
381
- // Helpers for attaching response transforms for GET/DELETE (which don’t include a body).
414
+ // Helpers for attaching response transforms.
382
415
  //
383
- const createHelper = (method, transform) => (request, autoRefresh = false) => createFetchSignal(request, method, transform, autoRefresh);
416
+ const createHelper = (method, transform) => (request, options) => createFetchSignal(request, method, transform, options);
384
417
  // Transforms
385
418
  const jsonTransformer = (response) => response.json();
386
419
  const textTransformer = (response) => response.text();
@@ -451,9 +484,9 @@ class UserManagementService {
451
484
  created_before: createdBefore && createdBefore().toISOString(),
452
485
  },
453
486
  headers: {
454
- Authorization: this.auth.bearerToken() ?? ''
487
+ Authorization: this.auth.bearerToken()
455
488
  }
456
- }), true);
489
+ }), { autoRefresh: true });
457
490
  }
458
491
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
459
492
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementService, providedIn: 'root' });
@@ -492,11 +525,11 @@ class UserManagementComponent {
492
525
  });
493
526
  }
494
527
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
495
- 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"] }] });
496
529
  }
497
530
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: UserManagementComponent, decorators: [{
498
531
  type: Component,
499
- 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" }]
500
533
  }], propDecorators: { paginator: [{
501
534
  type: ViewChild,
502
535
  args: [MatPaginator]
@@ -594,6 +627,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImpo
594
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"] }]
595
628
  }] });
596
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
+
597
698
  /**
598
699
  * Components
599
700
  */
@@ -602,5 +703,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImpo
602
703
  * Generated bundle index. Do not edit.
603
704
  */
604
705
 
605
- 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 };
606
707
  //# sourceMappingURL=fhss-web-team-frontend-utils.mjs.map