From af854e6aa67f9471907dea3d8d5f24810020b513 Mon Sep 17 00:00:00 2001 From: lberrymage Date: Fri, 23 Oct 2026 20:04:57 +0000 Subject: [PATCH] Remove reloading states from pages Allowing reloads complicates our page states and is actively harmful to offer when it's unlikely to help, such as in the case of internal server errors. We should add this functionality back later in the cases it's helpful, but for now, it's probably better to remove in the general case. --- .../home-container/home-container.html | 2 +- .../home-container/home-container.spec.ts | 48 +++-------------- .../home-container/home-container.ts | 20 ++----- src/app/components/home-page/home-page.html | 17 +++--- .../components/home-page/home-page.spec.ts | 53 +------------------ .../components/home-page/home-page.stories.ts | 6 --- src/app/components/home-page/home-page.ts | 16 ++---- .../org-dashboard-container.html | 2 +- .../org-dashboard-container.spec.ts | 42 ++------------- .../org-dashboard-container.ts | 11 +--- .../org-dashboard-page.html | 17 +++--- .../org-dashboard-page.spec.ts | 44 +++------------ .../org-dashboard-page.stories.ts | 6 --- .../org-dashboard-page/org-dashboard-page.ts | 18 +------ 14 files changed, 45 insertions(+), 257 deletions(-) diff --git a/src/app/components/home-container/home-container.html b/src/app/components/home-container/home-container.html index 65602f06..2963eb54 100644 --- a/src/app/components/home-container/home-container.html +++ b/src/app/components/home-container/home-container.html @@ -4,4 +4,4 @@ SPDX-License-Identifier: AGPL-3.0-only --> - + diff --git a/src/app/components/home-container/home-container.spec.ts b/src/app/components/home-container/home-container.spec.ts index 2fef0e44..4a0aa152 100644 --- a/src/app/components/home-container/home-container.spec.ts +++ b/src/app/components/home-container/home-container.spec.ts @@ -71,50 +71,14 @@ describe("HomeContainer", () => { expect(TestBed.inject(Router).url).toBe("/login"); }); - describe("when another error occurs", () => { - let fixture: ComponentFixture; - let page: HomePage; - - beforeEach(async () => { - consoleApi.myOrganizationResult = err({ kind: "unknown" }); - fixture = await render(); - page = fixture.debugElement.query(By.directive(HomePage)).injector.get(HomePage); - }); - - function pageState(): HomePageState { - fixture.detectChanges(); - return page.state(); - } - - function clickReload(): void { - const button: HTMLButtonElement = fixture.nativeElement.querySelector("button"); - button.click(); - } - - it("should show an error", () => { - expect(pageState()).toBe(HomePageState.LoadingFailed); - }); - - describe("when reloading", () => { - it("should show progress for the minimum duration even if reloading fails quickly", async () => { - clickReload(); - - await vi.advanceTimersByTimeAsync(DELAY_MS - 1); - expect(pageState()).toBe(HomePageState.Reloading); - - await vi.advanceTimersByTimeAsync(1); - expect(pageState()).toBe(HomePageState.LoadingFailed); - }); - - it("should redirect to the organization dashboard when reloading succeeds", async () => { - consoleApi.myOrganizationResult = ok({ organizationId: ORGANIZATION_ID }); + it("should show an error when any other error occurs", async () => { + consoleApi.myOrganizationResult = err({ kind: "unknown" }); - clickReload(); - await vi.advanceTimersByTimeAsync(DELAY_MS); + const fixture = await render(); - expect(TestBed.inject(Router).url).toBe(`/orgs/${ORGANIZATION_ID}/dashboard`); - }); - }); + fixture.detectChanges(); + const page = fixture.debugElement.query(By.directive(HomePage)).injector.get(HomePage); + expect(page.state()).toBe(HomePageState.LoadingFailed); }); }); diff --git a/src/app/components/home-container/home-container.ts b/src/app/components/home-container/home-container.ts index 4ac77028..79264e4c 100644 --- a/src/app/components/home-container/home-container.ts +++ b/src/app/components/home-container/home-container.ts @@ -25,27 +25,15 @@ export class HomeContainer { protected readonly state = signal(HomePageState.Loading); constructor() { - this.redirect(); - } - - /** - * Reloads the page, trying to determine where to redirect to after previously failing to do so. - */ - protected reload(): void { - this.state.set(HomePageState.Reloading); - this.redirect(); + // Run as a pending task so that the app isn't considered stable until we've redirected or + // displayed an error + this.pendingTasks.run(() => this.redirect()); } /** * Determines where to send the user and redirects them there, displaying an error on failure. */ - private redirect(): void { - // Run as a pending task so that the app isn't considered stable until we've redirected or - // displayed an error - this.pendingTasks.run(() => this.tryRedirect()); - } - - private async tryRedirect(): Promise { + private async redirect(): Promise { const result = await withDelay(this.consoleApi.getMyOrganization()); if (result.ok) { diff --git a/src/app/components/home-page/home-page.html b/src/app/components/home-page/home-page.html index 2385f071..5317987a 100644 --- a/src/app/components/home-page/home-page.html +++ b/src/app/components/home-page/home-page.html @@ -10,23 +10,18 @@ @case (HomePageState.Loading) { } - @case (HomePageState.LoadingFailed) - @case (HomePageState.Reloading) { - @let reloading = currentState === HomePageState.Reloading; + @case (HomePageState.LoadingFailed) {
- - Try again - + + Read the docs +
} @default never; diff --git a/src/app/components/home-page/home-page.spec.ts b/src/app/components/home-page/home-page.spec.ts index 7d26683f..877acae6 100644 --- a/src/app/components/home-page/home-page.spec.ts +++ b/src/app/components/home-page/home-page.spec.ts @@ -2,19 +2,15 @@ // // SPDX-License-Identifier: AGPL-3.0-only -import { inputBinding, outputBinding, signal } from "@angular/core"; +import { inputBinding, signal } from "@angular/core"; import { type ComponentFixture, TestBed } from "@angular/core/testing"; import { HomePage, HomePageState } from "./home-page"; describe("HomePage", () => { - let reloadCount: number; - beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HomePage], }).compileComponents(); - - reloadCount = 0; }); /** @@ -22,10 +18,7 @@ describe("HomePage", () => { */ async function render(state: HomePageState): Promise> { const fixture = TestBed.createComponent(HomePage, { - bindings: [ - inputBinding("state", signal(state)), - outputBinding("reload", () => reloadCount++), - ], + bindings: [inputBinding("state", signal(state))], }); await fixture.whenStable(); return fixture; @@ -54,47 +47,5 @@ describe("HomePage", () => { const alert: HTMLElement = fixture.nativeElement.querySelector("[role=alert]"); expect(alert.textContent).toContain("Something went wrong"); }); - - it("should emit reload when the reload button is clicked", () => { - fixture.nativeElement.querySelector("button").click(); - - expect(reloadCount).toBe(1); - }); - }); - - describe("when reloading", () => { - let fixture: ComponentFixture; - - beforeEach(async () => { - fixture = await render(HomePageState.Reloading); - }); - - it("should show an error", () => { - const alert: HTMLElement = fixture.nativeElement.querySelector("[role=alert]"); - expect(alert.textContent).toContain("Something went wrong"); - }); - - it("should show a progress indicator in the reload button", () => { - const button: HTMLButtonElement = fixture.nativeElement.querySelector("button"); - expect(button.querySelector("mat-progress-spinner")).not.toBeNull(); - }); - - it("should disable the reload button but keep it focusable", () => { - const button: HTMLButtonElement = fixture.nativeElement.querySelector("button"); - - expect(button.disabled).toBe(false); - expect(button.getAttribute("aria-disabled")).toBe("true"); - }); - - it("should give the reload button an accessible name describing the reload in progress", () => { - const button: HTMLButtonElement = fixture.nativeElement.querySelector("button"); - expect(button.getAttribute("aria-label")).toBe("Trying again"); - }); - - it("should not emit reload when the reload button is clicked", () => { - fixture.nativeElement.querySelector("button").click(); - - expect(reloadCount).toBe(0); - }); }); }); diff --git a/src/app/components/home-page/home-page.stories.ts b/src/app/components/home-page/home-page.stories.ts index f89219eb..e75ac9c1 100644 --- a/src/app/components/home-page/home-page.stories.ts +++ b/src/app/components/home-page/home-page.stories.ts @@ -27,9 +27,3 @@ export const LoadingFailed: Story = { state: HomePageState.LoadingFailed, }, }; - -export const Reloading: Story = { - args: { - state: HomePageState.Reloading, - }, -}; diff --git a/src/app/components/home-page/home-page.ts b/src/app/components/home-page/home-page.ts index 16a3b7ba..435260b7 100644 --- a/src/app/components/home-page/home-page.ts +++ b/src/app/components/home-page/home-page.ts @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: AGPL-3.0-only -import { Component, input, output } from "@angular/core"; +import { Component, input } from "@angular/core"; +import { MatButton } from "@angular/material/button"; import { MatProgressSpinner } from "@angular/material/progress-spinner"; -import { LoadingButton } from "../loading-button/loading-button"; /** * The state of the {@link HomePage}. @@ -19,18 +19,13 @@ export enum HomePageState { * Determining where to send the user failed. */ LoadingFailed, - - /** - * Determining where to send the user after a previous attempt failed. - */ - Reloading, } /** * The page shown while determining where to send the user on entering the console. */ @Component({ - imports: [LoadingButton, MatProgressSpinner], + imports: [MatButton, MatProgressSpinner], selector: "acc-home-page", templateUrl: "./home-page.html", }) @@ -41,9 +36,4 @@ export class HomePage { * The state of determining where to send the user. */ readonly state = input.required(); - - /** - * Emits when the user clicks the reload button. - */ - readonly reload = output(); } diff --git a/src/app/components/org-dashboard-container/org-dashboard-container.html b/src/app/components/org-dashboard-container/org-dashboard-container.html index 23159704..8eeabcdc 100644 --- a/src/app/components/org-dashboard-container/org-dashboard-container.html +++ b/src/app/components/org-dashboard-container/org-dashboard-container.html @@ -4,4 +4,4 @@ SPDX-License-Identifier: AGPL-3.0-only --> - + diff --git a/src/app/components/org-dashboard-container/org-dashboard-container.spec.ts b/src/app/components/org-dashboard-container/org-dashboard-container.spec.ts index d713e65f..57b6767c 100644 --- a/src/app/components/org-dashboard-container/org-dashboard-container.spec.ts +++ b/src/app/components/org-dashboard-container/org-dashboard-container.spec.ts @@ -105,46 +105,12 @@ describe("OrgDashboardContainer", () => { expect(TestBed.inject(Router).url).toBe("/login"); }); - describe("when another error occurs", () => { - let fixture: ComponentFixture; + it("should show an error when any other error occurs", async () => { + consoleApi.organizationResult = err({ kind: "unknown" }); - beforeEach(async () => { - consoleApi.organizationResult = err({ kind: "unknown" }); - fixture = await render(); - }); - - function clickReload(): void { - const button: HTMLButtonElement = fixture.nativeElement.querySelector("button"); - button.click(); - } - - it("should show an error", () => { - expect(pageState(fixture)).toEqual({ kind: "loadingFailed" }); - }); - - describe("when reloading", () => { - it("should show progress for the minimum duration even if reloading fails quickly", async () => { - clickReload(); - - await vi.advanceTimersByTimeAsync(DELAY_MS - 1); - expect(pageState(fixture)).toEqual({ kind: "reloading" }); - - await vi.advanceTimersByTimeAsync(1); - expect(pageState(fixture)).toEqual({ kind: "loadingFailed" }); - }); - - it("should show the organization when reloading succeeds", async () => { - consoleApi.organizationResult = ok({ organization: ORGANIZATION }); - - clickReload(); - await vi.advanceTimersByTimeAsync(DELAY_MS); + const fixture = await render(); - expect(pageState(fixture)).toEqual({ - kind: "loaded", - organization: ORGANIZATION, - }); - }); - }); + expect(pageState(fixture)).toEqual({ kind: "loadingFailed" }); }); }); diff --git a/src/app/components/org-dashboard-container/org-dashboard-container.ts b/src/app/components/org-dashboard-container/org-dashboard-container.ts index 00bd14f7..908373ee 100644 --- a/src/app/components/org-dashboard-container/org-dashboard-container.ts +++ b/src/app/components/org-dashboard-container/org-dashboard-container.ts @@ -38,11 +38,8 @@ export class OrgDashboardContainer { withDelay(this.consoleApi.getOrganization(params, abortSignal)), }); protected readonly state = computed((): OrgDashboardPageState => { - switch (this.organization.status()) { - case "reloading": - return { kind: "reloading" }; - case "error": - return { kind: "loadingFailed" }; + if (this.organization.status() === "error") { + return { kind: "loadingFailed" }; } return this.organization.hasValue() ? toPageState(this.organization.value()) @@ -61,10 +58,6 @@ export class OrgDashboardContainer { } }); } - - protected reload(): void { - this.organization.reload(); - } } function toPageState( diff --git a/src/app/components/org-dashboard-page/org-dashboard-page.html b/src/app/components/org-dashboard-page/org-dashboard-page.html index 31c24b8a..6af67f7e 100644 --- a/src/app/components/org-dashboard-page/org-dashboard-page.html +++ b/src/app/components/org-dashboard-page/org-dashboard-page.html @@ -13,22 +13,19 @@ } - @case ("loadingFailed") - @case ("reloading") { + @case ("loadingFailed") {
- - Try again - + + Read the docs +
} @case ("permissionDenied") { diff --git a/src/app/components/org-dashboard-page/org-dashboard-page.spec.ts b/src/app/components/org-dashboard-page/org-dashboard-page.spec.ts index c6d85aaf..460cdefe 100644 --- a/src/app/components/org-dashboard-page/org-dashboard-page.spec.ts +++ b/src/app/components/org-dashboard-page/org-dashboard-page.spec.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-only -import { inputBinding, outputBinding, signal } from "@angular/core"; +import { inputBinding, signal } from "@angular/core"; import { type ComponentFixture, TestBed } from "@angular/core/testing"; import { provideRouter } from "@angular/router"; import type { Organization } from "../../../domain/ports/console/organization"; @@ -17,11 +17,8 @@ const ORGANIZATION: Organization = { }; describe("OrgDashboardPage", () => { - let reloadCount: number; - beforeEach(() => { TestBed.configureTestingModule({ providers: [provideRouter([])] }); - reloadCount = 0; }); /** @@ -31,10 +28,7 @@ describe("OrgDashboardPage", () => { state: OrgDashboardPageState, ): Promise> { const fixture = TestBed.createComponent(OrgDashboardPage, { - bindings: [ - inputBinding("state", signal(state)), - outputBinding("reload", () => reloadCount++), - ], + bindings: [inputBinding("state", signal(state))], }); await fixture.whenStable(); return fixture; @@ -46,40 +40,16 @@ describe("OrgDashboardPage", () => { expect(fixture.nativeElement.querySelector("mat-progress-spinner")).not.toBeNull(); }); - it.each(["loadingFailed", "reloading"] as const)( - "should show an error when %s", - async (kind) => { - const fixture = await render({ kind }); - - const alert: HTMLElement = fixture.nativeElement.querySelector("[role=alert]"); - expect(alert.textContent).toContain("Something went wrong"); - }, - ); - - it("should emit reload when the reload button is clicked after loading failed", async () => { - const fixture = await render({ kind: "loadingFailed" }); - - fixture.nativeElement.querySelector("button").click(); - - expect(reloadCount).toBe(1); - }); - - describe("when reloading", () => { + describe("when loading failed", () => { let fixture: ComponentFixture; beforeEach(async () => { - fixture = await render({ kind: "reloading" }); - }); - - it("should show a progress indicator in the reload button", () => { - const button: HTMLButtonElement = fixture.nativeElement.querySelector("button"); - expect(button.querySelector("mat-progress-spinner")).not.toBeNull(); + fixture = await render({ kind: "loadingFailed" }); }); - it("should not emit reload when the reload button is clicked", () => { - fixture.nativeElement.querySelector("button").click(); - - expect(reloadCount).toBe(0); + it("should show an error", () => { + const alert: HTMLElement = fixture.nativeElement.querySelector("[role=alert]"); + expect(alert.textContent).toContain("Something went wrong"); }); }); diff --git a/src/app/components/org-dashboard-page/org-dashboard-page.stories.ts b/src/app/components/org-dashboard-page/org-dashboard-page.stories.ts index e3828c02..44194eb5 100644 --- a/src/app/components/org-dashboard-page/org-dashboard-page.stories.ts +++ b/src/app/components/org-dashboard-page/org-dashboard-page.stories.ts @@ -56,12 +56,6 @@ export const LoadingFailed: Story = { }, }; -export const Reloading: Story = { - args: { - state: { kind: "reloading" }, - }, -}; - export const PermissionDenied: Story = { args: { state: { kind: "permissionDenied" }, diff --git a/src/app/components/org-dashboard-page/org-dashboard-page.ts b/src/app/components/org-dashboard-page/org-dashboard-page.ts index 61764d1c..34407e24 100644 --- a/src/app/components/org-dashboard-page/org-dashboard-page.ts +++ b/src/app/components/org-dashboard-page/org-dashboard-page.ts @@ -2,20 +2,19 @@ // // SPDX-License-Identifier: AGPL-3.0-only -import { Component, input, output } from "@angular/core"; +import { Component, input } from "@angular/core"; import { MatButton } from "@angular/material/button"; import { MatIcon } from "@angular/material/icon"; import { MatProgressSpinner } from "@angular/material/progress-spinner"; import { RouterLink } from "@angular/router"; import type { Organization } from "../../../domain/ports/console/organization"; -import { LoadingButton } from "../loading-button/loading-button"; import { QuotaCard } from "../quota-card/quota-card"; /** * The dashboard page for a given organization. */ @Component({ - imports: [LoadingButton, MatButton, MatIcon, MatProgressSpinner, QuotaCard, RouterLink], + imports: [MatButton, MatIcon, MatProgressSpinner, QuotaCard, RouterLink], selector: "acc-org-dashboard-page", templateUrl: "./org-dashboard-page.html", }) @@ -24,11 +23,6 @@ export class OrgDashboardPage { * The UI state of the organization dashboard page. */ readonly state = input.required(); - - /** - * Emits when the user clicks the reload button. - */ - readonly reload = output(); } /** @@ -37,7 +31,6 @@ export class OrgDashboardPage { export type OrgDashboardPageState = | OrgDashboardPageLoading | OrgDashboardPageLoadingFailed - | OrgDashboardPageReloading | OrgDashboardPagePermissionDenied | OrgDashboardPageLoaded; @@ -55,13 +48,6 @@ export interface OrgDashboardPageLoadingFailed { readonly kind: "loadingFailed"; } -/** - * Loading the organization is in progress after a previous attempt failed. - */ -export interface OrgDashboardPageReloading { - readonly kind: "reloading"; -} - /** * The user isn't allowed to view the organization. */