refactor: add successOr, sweep remaining inline unwraps (RD-17)

Eight sites hand-rolled `rd.tag === 'Success' ? rd.value : fallback`. Six
take the new `successOr(rd, fallback)`, one takes the existing `successOf`,
and one (`big-profile.store.ts`) uses the existing `map`, since it returns a
RemoteData rather than an unwrapped value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 20:33:56 +02:00
co-authored by Claude Sonnet 5
parent c36d9e3ff0
commit e221834f6e
13 changed files with 212 additions and 37 deletions
+2 -4
View File
@@ -5,6 +5,7 @@ import { AlertComponent } from '@shared/ui/alert/alert.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { ASYNC } from '@shared/ui/async/async.component';
import { AccessStore } from '@shared/application/access.store';
import { successOr } from '@shared/application/remote-data';
import { AuditStore } from '@beheer/application/audit.store';
/**
@@ -94,10 +95,7 @@ export class AuditPage {
protected access = inject(AccessStore);
protected canRead = computed(() => this.access.can('cases:manage'));
protected entries = computed(() => {
const rd = this.store.entries();
return rd.tag === 'Success' ? rd.value : [];
});
protected entries = computed(() => successOr(this.store.entries(), []));
protected heading = $localize`:@@audit.heading:Auditlog`;
protected intro = $localize`:@@audit.intro:Toegangs- en inzagebeslissingen (autorisatie en het tonen van afgeschermde gegevens). Vastgelegd zonder persoonsgegevens.`;
+6 -1
View File
@@ -20,7 +20,7 @@ tested where._
Every bullet below is a real test name from the suite — an `it()` title (frontend) or a test
method name (backend), read as a sentence. Nothing here is hand-written prose: this page
**is** the suite, reshaped for a business reader. 530 frontend behaviours across
**is** the suite, reshaped for a business reader. 532 frontend behaviours across
9 contexts; 261 backend behaviours across 42 test
classes.
@@ -968,6 +968,11 @@ classes.
- unwraps a Success value
- is undefined for every other state
#### successOr
- unwraps a Success value
- is the fallback for every other state
#### upload lifecycle messages
- queued → progress → complete
@@ -1,5 +1,5 @@
import { Injectable, computed, inject, signal } from '@angular/core';
import { RemoteData } from '@shared/application/remote-data';
import { RemoteData, successOr } from '@shared/application/remote-data';
import { runSubmit } from '@shared/application/submit';
import { Result, ok, err } from '@shared/kernel/fp';
import { FeatureFlag } from '@shared/domain/feature-flag';
@@ -22,10 +22,7 @@ export class FeatureFlagStore {
readonly flags = this.state.asReadonly();
/** The resolved list (empty until loaded) — for the admin toggle UI. */
readonly all = computed(() => {
const rd = this.state();
return rd.tag === 'Success' ? rd.value : [];
});
readonly all = computed(() => successOr(this.state(), []));
constructor() {
void this.load();
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { RemoteData, fromLoadLifecycle, map2, map, successOf } from './remote-data';
import { RemoteData, fromLoadLifecycle, map2, map, successOf, successOr } from './remote-data';
import { loading, failure, empty, success } from '../testing/remote-data';
const loadingRd: RemoteData<string, number> = loading();
@@ -34,6 +34,18 @@ describe('successOf', () => {
});
});
describe('successOr', () => {
it('unwraps a Success value', () => {
expect(successOr(ok(2), 0)).toBe(2);
});
it('is the fallback for every other state', () => {
expect(successOr(loadingRd, [])).toEqual([]);
expect(successOr(failureRd, [])).toEqual([]);
expect(successOr(empty(), null)).toBeNull();
});
});
describe('fromLoadLifecycle', () => {
it('maps Loading → Loading', () => {
expect(fromLoadLifecycle({ tag: 'Loading' })).toEqual(loading());
@@ -109,3 +109,13 @@ export function andThen<E, A, B>(
export function successOf<E, T>(rd: RemoteData<E, T>): T | undefined {
return rd.tag === 'Success' ? rd.value : undefined;
}
/** Unwrap a Success value, or a caller-supplied `fallback` for every other state.
Reach for this over `successOf` when `undefined` is not a usable value at the
call site (e.g. a list the template iterates, which wants `[]`). Reach for
`map` instead when the site needs to stay a `RemoteData`, not an unwrapped
value. Three type parameters: `fallback` need not be assignable to `T`
(an empty array is not a `T[]` at the type level, only at the value level). */
export function successOr<E, T, F>(rd: RemoteData<E, T>, fallback: F): T | F {
return rd.tag === 'Success' ? rd.value : fallback;
}