This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-8752-e848f945ca8b315b3bc3419e1354bf5928cd71fc in repository https://gitbox.apache.org/repos/asf/texera.git
commit e2e0225263bf5400b5f6d2d6c99475b9e836cc04 Author: Xinyuan Lin <[email protected]> AuthorDate: Mon Oct 5 21:53:13 2026 +0000 fix(frontend): restore the hub workflow view-count toggle (#8752) ### What changes were proposed in this PR? Clicking the eye ("View") button on the hub workflow detail page switches the view count between the compact form (`1.2k`) and the exact count (`1,234`) again. The button has done nothing since #3028. **Root cause.** Only one method read `displayPreciseViewCount`: `formatViewCount`. #3028 deleted it as a duplicate of the like-count formatter, but the two were not the same. `formatViewCount` skipped the `k` abbreviation while the flag was set. The button, its `(click)` handler and the flag were all kept, so each click flipped a flag that nothing read. ``` Before: click eye -> "1.2k"; click again -> "1.2k" After: click eye -> "1,234"; click again -> "1.2k" ``` `formatViewCount` comes back as a thin wrapper over the shared `formatCount`, and the view button's label uses it again. The exact count is grouped by thousands with the app's `LOCALE_ID`, the same formatting as the `number` pipe used elsewhere in the UI. The app sets no `LOCALE_ID`, so it shows `1,234`. The like and clone counts still go through `formatCount` and stay compact. | | Default | After one click | | --- | --- | --- | | **Before** |  |  | | **After** |  |  | ### History | | | | --- | --- | | **Introduced by** | #2930 (2024-10-28), "Add view count to workflow": the eye button, `changeViewDisplayStyle()`, `displayPreciseViewCount`, and `formatViewCount`, which read the flag. The PR's demo shows the exact count after a click | | **Usage removed by** | #3028 (2024-11-11), "Remove duplicate and unnecessary code": deleted `formatViewCount` and switched the label to `formatCount` | The toggle worked for two weeks and has done nothing for almost two years since. The dataset detail page's copy of the button is a different case: #3269 added it after #3028, so it never worked there, and #8746 removes it. ### Any related issues, documentation, discussions? Closes #8751 ### How was this PR tested? The old spec case only checked that the flag flipped, and it passed while the button did nothing. It is replaced by: | Surface | Pinned | | --- | --- | | `formatViewCount` | Compact by default. Exact (`1,234`) after one toggle, compact again after a second. `1000` shows as `1.0k` / `1,000`. `0`, `1` and `999` look the same in both styles. The compact style of `1234567` equals `formatCount`, and the exact style is `1,234,567`. With `LOCALE_ID` set to `de`, the exact style is `1.234` | | Rendered view button (real template) | Shows `1.2k` for 1234 views. A click shows `1,234`, a second click `1.2k`. Same when the page opens as a modal. The like count stays `2.5k` after the view button is clicked | The rendered-button cases were run against `main`'s component first and fail there with `expected '1.2k' to be '1,234'`. From `frontend/`: - `npx ng test --watch=false --include='**/hub-workflow-detail.component.spec.ts'`: 52 tests, all pass. - `yarn format:ci`: clean. - `npx ng build`: success. Screenshots: `ng serve` against a stub API that reports 1234 views, 56 likes and 12 clones. Headless Chromium at 1280×720 via Playwright, with the button row captured before and after one click on the eye. **Before** is the same page running `main`'s component. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 5.5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../detail/hub-workflow-detail.component.html | 2 +- .../detail/hub-workflow-detail.component.spec.ts | 101 +++++++++++++++++++-- .../detail/hub-workflow-detail.component.ts | 11 ++- 3 files changed, 101 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html b/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html index aed7848aac..d3c41ab3c9 100644 --- a/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html +++ b/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html @@ -48,7 +48,7 @@ [nzType]="'eye'" class="like-icon"> </i> - <span>{{ formatCount(viewCount) }}</span> + <span>{{ formatViewCount(viewCount) }}</span> </button> <button diff --git a/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.spec.ts b/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.spec.ts index 25e91bbc82..a97678eb51 100644 --- a/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.spec.ts +++ b/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.spec.ts @@ -17,7 +17,9 @@ * under the License. */ -import { Component, Input } from "@angular/core"; +import { Component, Input, LOCALE_ID } from "@angular/core"; +import { registerLocaleData } from "@angular/common"; +import localeDe from "@angular/common/locales/de"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { NzIconModule } from "ng-zorro-antd/icon"; @@ -524,14 +526,52 @@ describe("HubWorkflowDetailComponent", () => { }); }); - describe("changeViewDisplayStyle", () => { - it("toggles displayPreciseViewCount", () => { + // Asserted on the formatted string rather than on displayPreciseViewCount: the flag flipped on + // every click even while nothing read it, so a flag assertion passed with the toggle inert. + describe("formatViewCount", () => { + it("returns the compact count before the view style is changed", () => { build({ modalData: { wid: 1 } }); - expect(component.displayPreciseViewCount).toBe(false); + expect(component.formatViewCount(1234)).toBe("1.2k"); + }); + + it("returns the grouped exact count after one change and the compact count after a second", () => { + build({ modalData: { wid: 1 } }); + component.changeViewDisplayStyle(); + expect(component.formatViewCount(1234)).toBe("1,234"); + component.changeViewDisplayStyle(); + expect(component.formatViewCount(1234)).toBe("1.2k"); + }); + + it("abbreviates 1000 only in the compact style", () => { + build({ modalData: { wid: 1 } }); + expect(component.formatViewCount(1000)).toBe("1.0k"); + component.changeViewDisplayStyle(); + expect(component.formatViewCount(1000)).toBe("1,000"); + }); + + it("renders counts below 1000 the same way in both styles", () => { + build({ modalData: { wid: 1 } }); + expect([0, 1, 999].map(count => component.formatViewCount(count))).toEqual(["0", "1", "999"]); component.changeViewDisplayStyle(); - expect(component.displayPreciseViewCount).toBe(true); + expect([0, 1, 999].map(count => component.formatViewCount(count))).toEqual(["0", "1", "999"]); + }); + + it("keeps every digit of a large count in the exact style", () => { + build({ modalData: { wid: 1 } }); + expect(component.formatViewCount(1234567)).toBe(component.formatCount(1234567)); + component.changeViewDisplayStyle(); + expect(component.formatViewCount(1234567)).toBe("1,234,567"); + }); + + it("groups the exact count by the app locale", () => { + // The separator comes from LOCALE_ID, like the `number` pipe the rest of the UI uses, rather + // than a hard-coded "en-US"; the app sets no LOCALE_ID, so it renders "1,234" today. + registerLocaleData(localeDe); + configure({ modalData: { wid: 1 } }); + TestBed.overrideProvider(LOCALE_ID, { useValue: "de" }); + component = TestBed.createComponent(HubWorkflowDetailComponent).componentInstance; component.changeViewDisplayStyle(); - expect(component.displayPreciseViewCount).toBe(false); + expect(component.formatViewCount(1234)).toBe("1.234"); }); }); }); @@ -553,7 +593,7 @@ describe("HubWorkflowDetailComponent rendered with its real children", () => { // goBack() chains .catch() onto the navigation result, so this has to be a real promise. let renderedRouter: { navigateByUrl: ReturnType<typeof vi.fn>; navigate: ReturnType<typeof vi.fn> }; - function render(opts: { isHub: boolean }): void { + function render(opts: { isHub: boolean; viewCount?: number; likeCount?: number }): void { renderedRouter = { navigateByUrl: vi.fn().mockResolvedValue(true), navigate: vi.fn().mockResolvedValue(true) }; TestBed.resetTestingModule(); TestBed.configureTestingModule({ @@ -579,8 +619,8 @@ describe("HubWorkflowDetailComponent rendered with its real children", () => { { provide: HubService, useValue: { - getCounts: () => of([{ entityId: 5, entityType: EntityType.Workflow, counts: {} }]), - postView: () => of(7), + getCounts: () => of([{ entityId: 5, entityType: EntityType.Workflow, counts: { like: opts.likeCount } }]), + postView: () => of(opts.viewCount ?? 7), isLiked: () => of([]), postLike: () => of(true), postUnlike: () => of(true), @@ -647,4 +687,47 @@ describe("HubWorkflowDetailComponent rendered with its real children", () => { expect((fixture.nativeElement as HTMLElement).querySelector(".go-back-button")).toBeNull(); }); + + describe("the view button", () => { + function countShownBy(title: string): string { + const button = (fixture.nativeElement as HTMLElement).querySelector(`button[title='${title}']`)!; + return button.querySelector(":scope > span")!.textContent!.trim(); + } + + function clickView(): void { + (fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>("button[title='View']")!.click(); + fixture.detectChanges(); + } + + it("shows the compact view count until it is clicked", () => { + render({ isHub: true, viewCount: 1234 }); + + expect(countShownBy("View")).toBe("1.2k"); + }); + + it("shows the exact view count on a click and the compact count on a second click", () => { + render({ isHub: true, viewCount: 1234 }); + + clickView(); + expect(countShownBy("View")).toBe("1,234"); + clickView(); + expect(countShownBy("View")).toBe("1.2k"); + }); + + it("works the same when the page is opened as a modal", () => { + render({ isHub: false, viewCount: 1234 }); + + clickView(); + expect(countShownBy("View")).toBe("1,234"); + }); + + it("leaves the like count compact", () => { + // The toggle belongs to the view count alone; the like count shares the formatter, so a + // style change that leaked into formatCount would show up here. + render({ isHub: true, viewCount: 1234, likeCount: 2500 }); + + clickView(); + expect(countShownBy("Like")).toBe("2.5k"); + }); + }); }); diff --git a/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts b/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts index 83e82cf288..16bf16668e 100644 --- a/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts +++ b/frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts @@ -17,7 +17,7 @@ * under the License. */ -import { AfterViewInit, Component, HostListener, Inject, OnDestroy, OnInit, Optional } from "@angular/core"; +import { AfterViewInit, Component, HostListener, Inject, LOCALE_ID, OnDestroy, OnInit, Optional } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ActivatedRoute, Router } from "@angular/router"; import { UserService } from "../../../../common/service/user/user.service"; @@ -31,7 +31,7 @@ import { NotificationService } from "../../../../common/service/notification/not import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { HUB_WORKFLOW_RESULT, USER_WORKSPACE } from "../../../../app-routing.constant"; -import { NgIf, NgClass } from "@angular/common"; +import { formatNumber, NgIf, NgClass } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @@ -85,7 +85,8 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI private notificationService: NotificationService, private hubService: HubService, private workflowPersistService: WorkflowPersistService, - @Optional() @Inject(NZ_MODAL_DATA) public input: { wid: number } | undefined + @Optional() @Inject(NZ_MODAL_DATA) public input: { wid: number } | undefined, + @Inject(LOCALE_ID) private locale: string ) { this.wid = input?.wid; //Accessing from the pop up. getting wid from the @Input if (!isDefined(this.wid)) { @@ -269,6 +270,10 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI formatCount = formatCount; + formatViewCount(count: number): string { + return this.displayPreciseViewCount ? formatNumber(count, this.locale) : formatCount(count); + } + changeViewDisplayStyle() { this.displayPreciseViewCount = !this.displayPreciseViewCount; }
