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** | ![before,
    
default](https://raw.githubusercontent.com/aglinxinyuan/texera/630604d0570603be76e7caac68436e82575d6d1c/before-default.png)
    | ![before,
    
clicked](https://raw.githubusercontent.com/aglinxinyuan/texera/630604d0570603be76e7caac68436e82575d6d1c/before-clicked.png)
    |
    | **After** | ![after,
    
default](https://raw.githubusercontent.com/aglinxinyuan/texera/630604d0570603be76e7caac68436e82575d6d1c/after-default.png)
    | ![after,
    
clicked](https://raw.githubusercontent.com/aglinxinyuan/texera/630604d0570603be76e7caac68436e82575d6d1c/after-clicked.png)
    |
    
    ### 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;
   }

Reply via email to