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-5151-eb287f3fc2cbba5416d851078affc10537123e68 in repository https://gitbox.apache.org/repos/asf/texera.git
commit d26bff669e0e253da451be3e64567a353036e00d Author: Matthew B. <[email protected]> AuthorDate: Mon Jun 1 16:31:36 2026 -0700 refactor: Drop /dashboard prefix from user-facing URLs (#5151) ### What changes were proposed in this PR? - Shortens user-facing URLs by dropping the `/dashboard` segment: `/dashboard/user/workflow/2358` becomes `/user/workflow/2358`. - Mounts `DashboardComponent` at `path: ""` (with a child `redirectTo: "about"`) instead of `"dashboard"`, and renames every route constant in `app-routing.constant.ts` to drop the `DASHBOARD`/`DASHBOARD_*` prefix (e.g. `DASHBOARD_USER_WORKFLOW` → `USER_WORKFLOW`). The standalone `DASHBOARD` prefix constant is removed; each constant is now an absolute path that already excludes the segment. - Removes a dead top-level `WorkspaceComponent` root route whose guard always redirected to `/about`; `WorkspaceComponent` stays reachable via `/user/workflow/:id`. - Updates the hardcoded `/dashboard/...` references (admin execution link, share-access message, navbar-hiding regex) and the Scala email-link path. Backend API endpoints under `@Path("/dashboard")` (`DashboardResource.scala`) and their corresponding `dashboard/...` service URLs are intentionally left unchanged — they are REST endpoints, not user-facing routes. > [!NOTE] > This only changes front-end routes. Pre-existing `/dashboard/...` deep links (bookmarks, the example links in `docs/tutorials/migrate-jupyter-notebook.md`) no longer resolve and fall through the `**` wildcard to `/user/workflow`. ### Any related issues, documentation, or discussions? Closes: #4407 ### How was this PR tested? - Ran `yarn format:fix` (clean) and `sbt scalafmtAll` (clean). - Ran `tsc --noEmit` against the branch merged with `main` (no type errors). - Added/updated unit tests covering the new paths: `app-routing.constant` values, the navbar-hiding regex, sidebar `routerLink`s (`dashboard.component`), admin execution link, `list-item`/`browse-section` entry routing, logout redirect (`user-icon`), new-workflow navigation (`user-workflow`), share-access message paths, owner-revoked-access redirect (`menu`), and the `location.go` URL update (`workspace`). - Ran the affected suites locally via `ng test` — all pass. #### Before <img width="2560" height="245" alt="Screenshot From 2026-05-23 04-11-22" src="https://github.com/user-attachments/assets/937c92bf-45e8-43de-ba17-482f63ee7c9d" /> #### After <img width="2560" height="245" alt="Screenshot From 2026-05-23 04-11-04" src="https://github.com/user-attachments/assets/ca53ba20-e794-472e-a8cf-fd1e24ae544a" /> ### Was this PR authored or co-authored using generative AI tooling? Co-authored with Claude Opus 4.7 and Claude Opus 4.8 in compliance with ASF --------- Signed-off-by: Matthew B. <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> --- .../texera/web/service/WorkflowEmailNotifier.scala | 2 +- docs/tutorials/migrate-jupyter-notebook.md | 2 +- frontend/src/app/app-routing.constant.ts | 49 ++++++++------- frontend/src/app/app-routing.module.ts | 35 ++++------- .../app/common/service/user/auth-guard.service.ts | 4 +- .../admin/execution/admin-execution.component.html | 2 +- .../execution/admin-execution.component.spec.ts | 21 +++++++ .../dashboard/component/dashboard.component.html | 22 +++---- .../component/dashboard.component.spec.ts | 63 ++++++++++++++++++- .../app/dashboard/component/dashboard.component.ts | 51 ++++++++-------- .../user/list-item/list-item.component.spec.ts | 70 ++++++++++++++++++++++ .../user/list-item/list-item.component.ts | 20 +++---- .../user/search-bar/search-bar.component.spec.ts | 6 +- .../user/search-bar/search-bar.component.ts | 4 +- .../share-access/share-access.component.spec.ts | 8 +-- .../user/share-access/share-access.component.ts | 12 ++-- .../user-dataset-list-item.component.html | 2 +- .../user-dataset-list-item.component.ts | 4 +- .../user-dataset/user-dataset.component.spec.ts | 4 +- .../user/user-dataset/user-dataset.component.ts | 4 +- .../user/user-icon/user-icon.component.spec.ts | 32 ++++++++++ .../user/user-icon/user-icon.component.ts | 4 +- .../user-project-list-item.component.ts | 4 +- .../user-workflow-list-item.component.html | 4 +- .../user-workflow-list-item.component.ts | 6 +- .../user-workflow/user-workflow.component.spec.ts | 19 ++++++ .../user/user-workflow/user-workflow.component.ts | 4 +- .../service/admin/guard/admin-guard.service.ts | 4 +- .../about/local-login/local-login.component.ts | 6 +- .../browse-section.component.spec.ts | 37 ++++++++++++ .../browse-section/browse-section.component.ts | 24 ++++---- frontend/src/app/hub/component/hub.component.html | 6 +- frontend/src/app/hub/component/hub.component.ts | 12 ++-- .../landing-page/landing-page.component.spec.ts | 12 ++-- .../landing-page/landing-page.component.ts | 12 ++-- .../detail/hub-workflow-detail.component.ts | 6 +- .../workspace/component/menu/menu.component.html | 2 +- .../component/menu/menu.component.spec.ts | 27 +++++++++ .../app/workspace/component/menu/menu.component.ts | 6 +- .../component/workspace.component.spec.ts | 46 ++++++++++++++ .../app/workspace/component/workspace.component.ts | 4 +- 41 files changed, 469 insertions(+), 193 deletions(-) diff --git a/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala b/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala index af9a26286d..c49f63e939 100644 --- a/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala +++ b/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala @@ -107,7 +107,7 @@ class WorkflowEmailNotifier( private def createDashboardUrl(): String = { val host = sessionUri.getHost val port = sessionUri.getPort - val path = s"/dashboard/user/workspace/$workflowId" + val path = s"/user/workspace/$workflowId" if (port == -1 || port == 80 || port == 443) { s"http://$host$path" } else { diff --git a/docs/tutorials/migrate-jupyter-notebook.md b/docs/tutorials/migrate-jupyter-notebook.md index 5fc875d78b..eb1b3cea74 100644 --- a/docs/tutorials/migrate-jupyter-notebook.md +++ b/docs/tutorials/migrate-jupyter-notebook.md @@ -16,7 +16,7 @@ Migrating notebook code into Texera operators, then wiring those operators with ## 2. Example: convert a "tweet analysis" notebook into a workflow -> The [notebook](https://hub.texera.io/dashboard/user/dataset/124), [dataset](https://hub.texera.io/dashboard/user/dataset/124) and [workflow](https://hub.texera.io/dashboard/user/workspace/1162) in this example are available on [TexeraHub](https://hub.texera.io/dashboard/about). +> The [notebook](https://hub.texera.io/user/dataset/124), [dataset](https://hub.texera.io/user/dataset/124) and [workflow](https://hub.texera.io/user/workflow/1162) in this example are available on [TexeraHub](https://hub.texera.io/about). ### Notebook Overview We will use a Tweet-Analysis notebook to demonstrate the migration process. The notebook has three cells: diff --git a/frontend/src/app/app-routing.constant.ts b/frontend/src/app/app-routing.constant.ts index 4181df8a95..6e06f72520 100644 --- a/frontend/src/app/app-routing.constant.ts +++ b/frontend/src/app/app-routing.constant.ts @@ -17,32 +17,31 @@ * under the License. */ -export const DASHBOARD = "/dashboard"; -export const DASHBOARD_HOME = `${DASHBOARD}/home`; -export const DASHBOARD_ABOUT = `${DASHBOARD}/about`; +export const HOME = "/home"; +export const ABOUT = "/about"; -export const DASHBOARD_HUB = `${DASHBOARD}/hub`; -export const DASHBOARD_HUB_WORKFLOW = `${DASHBOARD_HUB}/workflow`; -export const DASHBOARD_HUB_WORKFLOW_RESULT = `${DASHBOARD_HUB_WORKFLOW}/result`; -export const DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = `${DASHBOARD_HUB_WORKFLOW_RESULT}/detail`; -export const DASHBOARD_HUB_DATASET = `${DASHBOARD_HUB}/dataset`; -export const DASHBOARD_HUB_DATASET_RESULT = `${DASHBOARD_HUB_DATASET}/result`; -export const DASHBOARD_HUB_DATASET_RESULT_DETAIL = `${DASHBOARD_HUB_DATASET_RESULT}/detail`; +export const HUB = "/hub"; +export const HUB_WORKFLOW = `${HUB}/workflow`; +export const HUB_WORKFLOW_RESULT = `${HUB_WORKFLOW}/result`; +export const HUB_WORKFLOW_RESULT_DETAIL = `${HUB_WORKFLOW_RESULT}/detail`; +export const HUB_DATASET = `${HUB}/dataset`; +export const HUB_DATASET_RESULT = `${HUB_DATASET}/result`; +export const HUB_DATASET_RESULT_DETAIL = `${HUB_DATASET_RESULT}/detail`; -export const DASHBOARD_USER = `${DASHBOARD}/user`; -export const DASHBOARD_USER_PROJECT = `${DASHBOARD_USER}/project`; -export const DASHBOARD_USER_WORKSPACE = `${DASHBOARD_USER}/workflow`; -export const DASHBOARD_USER_WORKFLOW = `${DASHBOARD_USER}/workflow`; -export const DASHBOARD_USER_DATASET = `${DASHBOARD_USER}/dataset`; -export const DASHBOARD_USER_DATASET_CREATE = `${DASHBOARD_USER_DATASET}/create`; -export const DASHBOARD_USER_COMPUTING_UNIT = `${DASHBOARD_USER}/compute`; -export const DASHBOARD_USER_QUOTA = `${DASHBOARD_USER}/quota`; -export const DASHBOARD_USER_DISCUSSION = `${DASHBOARD_USER}/discussion`; +export const USER = "/user"; +export const USER_PROJECT = `${USER}/project`; +export const USER_WORKSPACE = `${USER}/workflow`; +export const USER_WORKFLOW = `${USER}/workflow`; +export const USER_DATASET = `${USER}/dataset`; +export const USER_DATASET_CREATE = `${USER_DATASET}/create`; +export const USER_COMPUTING_UNIT = `${USER}/compute`; +export const USER_QUOTA = `${USER}/quota`; +export const USER_DISCUSSION = `${USER}/discussion`; -export const DASHBOARD_ADMIN = `${DASHBOARD}/admin`; -export const DASHBOARD_ADMIN_USER = `${DASHBOARD_ADMIN}/user`; -export const DASHBOARD_ADMIN_GMAIL = `${DASHBOARD_ADMIN}/gmail`; -export const DASHBOARD_ADMIN_EXECUTION = `${DASHBOARD_ADMIN}/execution`; -export const DASHBOARD_ADMIN_SETTINGS = `${DASHBOARD_ADMIN}/settings`; +export const ADMIN = "/admin"; +export const ADMIN_USER = `${ADMIN}/user`; +export const ADMIN_GMAIL = `${ADMIN}/gmail`; +export const ADMIN_EXECUTION = `${ADMIN}/execution`; +export const ADMIN_SETTINGS = `${ADMIN}/settings`; -export const DASHBOARD_SEARCH = `${DASHBOARD}/search`; +export const SEARCH = "/search"; diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 179caf5c08..8e5a44903e 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -17,8 +17,8 @@ * under the License. */ -import { inject, NgModule } from "@angular/core"; -import { CanActivateFn, Router, RouterModule, Routes } from "@angular/router"; +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; import { DashboardComponent } from "./dashboard/component/dashboard.component"; import { UserWorkflowComponent } from "./dashboard/component/user/user-workflow/user-workflow.component"; import { UserQuotaComponent } from "./dashboard/component/user/user-quota/user-quota.component"; @@ -38,28 +38,21 @@ import { DatasetDetailComponent } from "./dashboard/component/user/user-dataset/ import { UserDatasetComponent } from "./dashboard/component/user/user-dataset/user-dataset.component"; import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component"; import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component"; -import { DASHBOARD_ABOUT, DASHBOARD_USER_WORKFLOW } from "./app-routing.constant"; +import { USER_WORKFLOW } from "./app-routing.constant"; import { HubSearchResultComponent } from "./hub/component/hub-search-result/hub-search-result.component"; import { AdminSettingsComponent } from "./dashboard/component/admin/settings/admin-settings.component"; -import { GuiConfigService } from "./common/service/gui-config.service"; - -const rootRedirectGuard: CanActivateFn = () => { - const config = inject(GuiConfigService); - const router = inject(Router); - try { - return router.parseUrl(DASHBOARD_ABOUT); - } catch { - // config not loaded yet, swallow the error and let the app handle it - } - return true; -}; const routes: Routes = []; routes.push({ - path: "dashboard", + path: "", component: DashboardComponent, children: [ + { + path: "", + redirectTo: "about", + pathMatch: "full", + }, { path: "home", component: LandingPageComponent, @@ -174,18 +167,10 @@ routes.push({ ], }); -// default route renders the workspace editor directly; if userSystem is enabled at runtime, -// AppComponent will navigate to DASHBOARD_ABOUT instead. -routes.push({ - path: "", - component: WorkspaceComponent, - canActivate: [rootRedirectGuard], -}); - // redirect all other paths to index. routes.push({ path: "**", - redirectTo: DASHBOARD_USER_WORKFLOW, + redirectTo: USER_WORKFLOW, }); @NgModule({ diff --git a/frontend/src/app/common/service/user/auth-guard.service.ts b/frontend/src/app/common/service/user/auth-guard.service.ts index 3e6e315871..1a69ad0289 100644 --- a/frontend/src/app/common/service/user/auth-guard.service.ts +++ b/frontend/src/app/common/service/user/auth-guard.service.ts @@ -21,7 +21,7 @@ import { Injectable } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; import { GuiConfigService } from "../gui-config.service"; import { UserService } from "./user.service"; -import { DASHBOARD_ABOUT } from "../../../app-routing.constant"; +import { ABOUT } from "../../../app-routing.constant"; /** * AuthGuardService is a service can tell the router whether @@ -38,7 +38,7 @@ export class AuthGuardService implements CanActivate { if (this.userService.isLogin()) { return true; } else { - this.router.navigate([DASHBOARD_ABOUT], { queryParams: { returnUrl: state.url === "/" ? null : state.url } }); + this.router.navigate([ABOUT], { queryParams: { returnUrl: state.url === "/" ? null : state.url } }); return false; } } diff --git a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html index 41a2ceb965..907ffa8d72 100644 --- a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html +++ b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html @@ -100,7 +100,7 @@ <tr *ngFor="let execution of basicTable.data"> <td> <div *ngIf="execution.access; else normalWorkflowName"> - <a href="/dashboard/user/workflow/{{execution.workflowId}}"> + <a href="/user/workflow/{{execution.workflowId}}"> {{ maxStringLength(execution.workflowName, 16) }} ({{ execution.workflowId }}) </a> </div> diff --git a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts index 466d0a3db0..caaa71a220 100644 --- a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts +++ b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts @@ -18,12 +18,14 @@ */ import { ComponentFixture, inject, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { AdminExecutionComponent } from "./admin-execution.component"; import { AdminExecutionService } from "../../../service/admin/execution/admin-execution.service"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { NzModalModule } from "ng-zorro-antd/modal"; import { commonTestProviders } from "../../../../common/testing/test-utils"; +import { Execution } from "../../../../common/type/execution"; describe("AdminDashboardComponent", () => { let component: AdminExecutionComponent; @@ -45,4 +47,23 @@ describe("AdminDashboardComponent", () => { it("should create", inject([HttpTestingController], () => { expect(component).toBeTruthy(); })); + + it("renders the workflow link to /user/workflow/<id> when the admin has access", () => { + component.listOfExecutions = [ + { + access: true, + workflowId: 42, + workflowName: "demo workflow", + executionId: 1, + executionName: "exec", + userName: "alice", + executionStatus: "COMPLETED", + } as unknown as Execution, + ]; + component.isLoading = false; + fixture.detectChanges(); + + const anchor = fixture.debugElement.query(By.css('a[href="/user/workflow/42"]')); + expect(anchor).toBeTruthy(); + }); }); diff --git a/frontend/src/app/dashboard/component/dashboard.component.html b/frontend/src/app/dashboard/component/dashboard.component.html index 9edea62915..ba3f74fa3a 100644 --- a/frontend/src/app/dashboard/component/dashboard.component.html +++ b/frontend/src/app/dashboard/component/dashboard.component.html @@ -65,7 +65,7 @@ nz-tooltip="Look up the user projects" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_USER_PROJECT"> + [routerLink]="USER_PROJECT"> <span nz-icon nzType="container"></span> @@ -78,7 +78,7 @@ nz-tooltip="Open the saved workflows" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_USER_WORKFLOW"> + [routerLink]="USER_WORKFLOW"> <span nz-icon nzType="project"></span> @@ -91,7 +91,7 @@ nz-tooltip="Look up for datasets" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_USER_DATASET"> + [routerLink]="USER_DATASET"> <span nz-icon nzType="database"></span> @@ -103,7 +103,7 @@ nz-tooltip="Manage computing units" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_USER_COMPUTING_UNIT"> + [routerLink]="USER_COMPUTING_UNIT"> <span nz-icon nzType="deployment-unit"></span> @@ -115,7 +115,7 @@ nz-tooltip="Quota information" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_USER_QUOTA"> + [routerLink]="USER_QUOTA"> <span nz-icon nzType="dashboard"></span> @@ -127,7 +127,7 @@ nz-tooltip="Open the discussion forum" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_USER_DISCUSSION"> + [routerLink]="USER_DISCUSSION"> <span nz-icon nzType="comment"></span> @@ -147,7 +147,7 @@ nz-tooltip="Look up the users" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_ADMIN_USER"> + [routerLink]="ADMIN_USER"> <span nz-icon nzType="user"></span> @@ -158,7 +158,7 @@ nz-tooltip="View statistics" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_ADMIN_EXECUTION"> + [routerLink]="ADMIN_EXECUTION"> <span nz-icon nzType="setting"></span> @@ -169,7 +169,7 @@ nz-tooltip="Setup gmail" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_ADMIN_GMAIL"> + [routerLink]="ADMIN_GMAIL"> <span nz-icon nzType="mail"></span> @@ -180,7 +180,7 @@ nz-tooltip="Settings" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_ADMIN_SETTINGS"> + [routerLink]="ADMIN_SETTINGS"> <span nz-icon nzType="edit"></span> @@ -194,7 +194,7 @@ nz-menu-item nz-tooltip nzTooltipPlacement="right" - [routerLink]="DASHBOARD_ABOUT"> + [routerLink]="ABOUT"> <span nz-icon nzType="info-circle"></span> diff --git a/frontend/src/app/dashboard/component/dashboard.component.spec.ts b/frontend/src/app/dashboard/component/dashboard.component.spec.ts index 6f508ca136..29ed8426fe 100644 --- a/frontend/src/app/dashboard/component/dashboard.component.spec.ts +++ b/frontend/src/app/dashboard/component/dashboard.component.spec.ts @@ -35,12 +35,26 @@ import { NavigationEnd, Params, Router, + RouterLink, UrlSegment, } from "@angular/router"; import type { Mock } from "vitest"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { commonTestProviders } from "../../common/testing/test-utils"; import { GuiConfigService } from "../../common/service/gui-config.service"; +import { + ABOUT, + ADMIN_EXECUTION, + ADMIN_GMAIL, + ADMIN_SETTINGS, + ADMIN_USER, + USER_COMPUTING_UNIT, + USER_DATASET, + USER_DISCUSSION, + USER_PROJECT, + USER_QUOTA, + USER_WORKFLOW, +} from "../../app-routing.constant"; describe("DashboardComponent", () => { let component: DashboardComponent; @@ -77,11 +91,12 @@ describe("DashboardComponent", () => { isAdmin: vi.fn().mockReturnValue(false), isLogin: vi.fn().mockReturnValue(false), userChanged: vi.fn().mockReturnValue(of(null)), + getCurrentUser: vi.fn().mockReturnValue(undefined), }; routerMock = { - events: of(new NavigationEnd(1, "/dashboard", "/dashboard")), - url: "/dashboard", + events: of(new NavigationEnd(1, "/", "/")), + url: "/", navigateByUrl: vi.fn(), }; @@ -162,4 +177,48 @@ describe("DashboardComponent", () => { expect(fixture.debugElement.query(By.css("#powered-by"))).toBeTruthy(); }); + + it("should hide the navbar on workflow workspace routes", () => { + expect(component.isNavbarEnabled("/user/workflow/42")).toBe(false); + expect(component.isNavbarEnabled("/user/workflow")).toBe(true); + expect(component.isNavbarEnabled("/user/project")).toBe(true); + }); + + it("exposes route constants without the legacy /dashboard prefix", () => { + expect(USER_PROJECT).toBe("/user/project"); + expect(USER_WORKFLOW).toBe("/user/workflow"); + expect(USER_DATASET).toBe("/user/dataset"); + expect(USER_COMPUTING_UNIT).toBe("/user/compute"); + expect(USER_QUOTA).toBe("/user/quota"); + expect(USER_DISCUSSION).toBe("/user/discussion"); + expect(ADMIN_USER).toBe("/admin/user"); + expect(ADMIN_EXECUTION).toBe("/admin/execution"); + expect(ADMIN_GMAIL).toBe("/admin/gmail"); + expect(ADMIN_SETTINGS).toBe("/admin/settings"); + expect(ABOUT).toBe("/about"); + }); + + it("renders every sidebar tab's routerLink when fully enabled", () => { + (userServiceMock.isLogin as Mock).mockReturnValue(true); + component.isLogin = true; + component.isAdmin = true; + component.sidebarTabs = { + hub_enabled: false, + home_enabled: true, + workflow_enabled: true, + dataset_enabled: true, + your_work_enabled: true, + projects_enabled: true, + workflows_enabled: true, + datasets_enabled: true, + compute_enabled: true, + quota_enabled: true, + forum_enabled: true, + about_enabled: true, + }; + fixture.detectChanges(); + + // 6 "Your Work" links + 4 admin links + 1 about link = 11 + expect(fixture.debugElement.queryAll(By.directive(RouterLink)).length).toBe(11); + }); }); diff --git a/frontend/src/app/dashboard/component/dashboard.component.ts b/frontend/src/app/dashboard/component/dashboard.component.ts index 57e6e8e284..aadf07d54b 100644 --- a/frontend/src/app/dashboard/component/dashboard.component.ts +++ b/frontend/src/app/dashboard/component/dashboard.component.ts @@ -29,17 +29,17 @@ import { AdminSettingsService } from "../service/admin/settings/admin-settings.s import { GuiConfigService } from "../../common/service/gui-config.service"; import { - DASHBOARD_ABOUT, - DASHBOARD_ADMIN_EXECUTION, - DASHBOARD_ADMIN_GMAIL, - DASHBOARD_ADMIN_SETTINGS, - DASHBOARD_ADMIN_USER, - DASHBOARD_USER_COMPUTING_UNIT, - DASHBOARD_USER_DATASET, - DASHBOARD_USER_DISCUSSION, - DASHBOARD_USER_PROJECT, - DASHBOARD_USER_QUOTA, - DASHBOARD_USER_WORKFLOW, + ABOUT, + ADMIN_EXECUTION, + ADMIN_GMAIL, + ADMIN_SETTINGS, + ADMIN_USER, + USER_COMPUTING_UNIT, + USER_DATASET, + USER_DISCUSSION, + USER_PROJECT, + USER_QUOTA, + USER_WORKFLOW, } from "../../app-routing.constant"; import { Version } from "../../../environments/version"; import { SidebarTabs } from "../../common/type/gui-config"; @@ -105,16 +105,18 @@ export class DashboardComponent implements OnInit { about_enabled: false, }; - protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT; - protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW; - protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET; - protected readonly DASHBOARD_USER_COMPUTING_UNIT = DASHBOARD_USER_COMPUTING_UNIT; - protected readonly DASHBOARD_USER_QUOTA = DASHBOARD_USER_QUOTA; - protected readonly DASHBOARD_USER_DISCUSSION = DASHBOARD_USER_DISCUSSION; - protected readonly DASHBOARD_ADMIN_USER = DASHBOARD_ADMIN_USER; - protected readonly DASHBOARD_ADMIN_GMAIL = DASHBOARD_ADMIN_GMAIL; - protected readonly DASHBOARD_ADMIN_EXECUTION = DASHBOARD_ADMIN_EXECUTION; - protected readonly DASHBOARD_ADMIN_SETTINGS = DASHBOARD_ADMIN_SETTINGS; + protected readonly USER_PROJECT = USER_PROJECT; + protected readonly USER_WORKFLOW = USER_WORKFLOW; + protected readonly USER_DATASET = USER_DATASET; + protected readonly USER_COMPUTING_UNIT = USER_COMPUTING_UNIT; + protected readonly USER_QUOTA = USER_QUOTA; + protected readonly USER_DISCUSSION = USER_DISCUSSION; + protected readonly ADMIN_USER = ADMIN_USER; + protected readonly ADMIN_GMAIL = ADMIN_GMAIL; + protected readonly ADMIN_EXECUTION = ADMIN_EXECUTION; + protected readonly ADMIN_SETTINGS = ADMIN_SETTINGS; + protected readonly ABOUT = ABOUT; + protected readonly String = String; constructor( private userService: UserService, @@ -158,7 +160,7 @@ export class DashboardComponent implements OnInit { .pipe(untilDestroyed(this)) .subscribe(() => { this.ngZone.run(() => { - this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || DASHBOARD_USER_WORKFLOW); + this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || USER_WORKFLOW); }); }); }); @@ -232,7 +234,7 @@ export class DashboardComponent implements OnInit { isNavbarEnabled(currentRoute: string) { // Hide navbar for workflow workspace pages (with numeric ID) - if (currentRoute.match(/\/dashboard\/user\/workflow\/\d+/)) { + if (currentRoute.match(/\/user\/workflow\/\d+/)) { return false; } return true; @@ -248,7 +250,4 @@ export class DashboardComponent implements OnInit { }, 175); } } - - protected readonly DASHBOARD_ABOUT = DASHBOARD_ABOUT; - protected readonly String = String; } diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts index c8840a135b..44bec7e07f 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts @@ -30,6 +30,14 @@ import { UserService } from "../../../../common/service/user/user.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import type { Mocked } from "vitest"; import { DashboardEntry } from "src/app/dashboard/type/dashboard-entry"; +import { + HUB_DATASET_RESULT_DETAIL, + HUB_WORKFLOW_RESULT_DETAIL, + USER_DATASET, + USER_PROJECT, + USER_WORKSPACE, +} from "../../../../app-routing.constant"; + describe("ListItemComponent", () => { let component: ListItemComponent; let fixture: ComponentFixture<ListItemComponent>; @@ -119,4 +127,66 @@ describe("ListItemComponent", () => { expect(component.entry.description).toBe("Old Description"); expect(component.editingDescription).toBe(false); }); + + describe("initializeEntry routes", () => { + const baseStats = { likeCount: 0, viewCount: 0, isLiked: false }; + + it("routes owned workflows to the user workspace", () => { + component.currentUid = 1; + component.entry = { + id: 100, + type: "workflow", + workflow: { isOwner: true }, + accessibleUserIds: [1], + ...baseStats, + } as unknown as DashboardEntry; + component.initializeEntry(); + expect(component.entryLink).toEqual([USER_WORKSPACE, "100"]); + }); + + it("routes non-owned workflows to the hub workflow detail page", () => { + component.currentUid = 1; + component.entry = { + id: 101, + type: "workflow", + workflow: { isOwner: false }, + accessibleUserIds: [2], + ...baseStats, + } as unknown as DashboardEntry; + component.initializeEntry(); + expect(component.entryLink).toEqual([HUB_WORKFLOW_RESULT_DETAIL, "101"]); + }); + + it("routes projects to the user project page", () => { + component.entry = { id: 200, type: "project", ...baseStats } as unknown as DashboardEntry; + component.initializeEntry(); + expect(component.entryLink).toEqual([USER_PROJECT, "200"]); + }); + + it("routes owned datasets to the user dataset page", () => { + component.currentUid = 1; + component.entry = { + id: 300, + type: "dataset", + dataset: { isOwner: true }, + accessibleUserIds: [1], + ...baseStats, + } as unknown as DashboardEntry; + component.initializeEntry(); + expect(component.entryLink).toEqual([USER_DATASET, "300"]); + }); + + it("routes non-owned datasets to the hub dataset detail page", () => { + component.currentUid = 1; + component.entry = { + id: 301, + type: "dataset", + dataset: { isOwner: false }, + accessibleUserIds: [2], + ...baseStats, + } as unknown as DashboardEntry; + component.initializeEntry(); + expect(component.entryLink).toEqual([HUB_DATASET_RESULT_DETAIL, "301"]); + }); + }); }); diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts b/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts index e57cca2013..5cca45ced4 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.ts @@ -46,11 +46,11 @@ import { formatCount, formatRelativeTime } from "src/app/common/util/format.util import { DatasetService, DEFAULT_DATASET_NAME } from "../../../service/user/dataset/dataset.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { - DASHBOARD_HUB_DATASET_RESULT_DETAIL, - DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, - DASHBOARD_USER_DATASET, - DASHBOARD_USER_PROJECT, - DASHBOARD_USER_WORKSPACE, + HUB_DATASET_RESULT_DETAIL, + HUB_WORKFLOW_RESULT_DETAIL, + USER_DATASET, + USER_PROJECT, + USER_WORKSPACE, } from "../../../../app-routing.constant"; import { isDefined } from "../../../../common/util/predicate"; import { NzCardComponent } from "ng-zorro-antd/card"; @@ -145,24 +145,24 @@ export class ListItemComponent implements OnChanges { this.disableDelete = !this.entry.workflow.isOwner; this.owners = this.entry.accessibleUserIds; if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) { - this.entryLink = [DASHBOARD_USER_WORKSPACE, String(this.entry.id)]; + this.entryLink = [USER_WORKSPACE, String(this.entry.id)]; } else { - this.entryLink = [DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, String(this.entry.id)]; + this.entryLink = [HUB_WORKFLOW_RESULT_DETAIL, String(this.entry.id)]; } this.size = this.entry.size; } this.iconType = "project"; } else if (this.entry.type === "project") { - this.entryLink = [DASHBOARD_USER_PROJECT, String(this.entry.id)]; + this.entryLink = [USER_PROJECT, String(this.entry.id)]; this.iconType = "container"; } else if (this.entry.type === "dataset") { if (typeof this.entry.id === "number") { this.disableDelete = !this.entry.dataset.isOwner; this.owners = this.entry.accessibleUserIds; if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) { - this.entryLink = [DASHBOARD_USER_DATASET, String(this.entry.id)]; + this.entryLink = [USER_DATASET, String(this.entry.id)]; } else { - this.entryLink = [DASHBOARD_HUB_DATASET_RESULT_DETAIL, String(this.entry.id)]; + this.entryLink = [HUB_DATASET_RESULT_DETAIL, String(this.entry.id)]; } this.iconType = "database"; this.size = this.entry.size; diff --git a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts index 922a7c4969..2540e89d2b 100644 --- a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts @@ -28,7 +28,7 @@ import { SearchService } from "../../../service/user/search.service"; import { UserService } from "../../../../common/service/user/user.service"; import { SortMethod } from "../../../type/sort-method"; import { SearchResult, SearchResultItem } from "../../../type/search-result"; -import { DASHBOARD_SEARCH } from "../../../../app-routing.constant"; +import { SEARCH } from "../../../../app-routing.constant"; import { commonTestProviders } from "../../../../common/testing/test-utils"; function makeWorkflowItem(name: string, wid: number = 1): SearchResultItem { @@ -237,11 +237,11 @@ describe("SearchBarComponent", () => { }); }); - it("performSearch navigates to DASHBOARD_SEARCH with the keyword as the q query param", () => { + it("performSearch navigates to SEARCH with the keyword as the q query param", () => { const nav = vi.spyOn(router, "navigate").mockResolvedValue(true); component.performSearch("hello world"); - expect(nav).toHaveBeenCalledWith([DASHBOARD_SEARCH], { queryParams: { q: "hello world" } }); + expect(nav).toHaveBeenCalledWith([SEARCH], { queryParams: { q: "hello world" } }); }); }); diff --git a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts index ae6a6c1df5..638a2f7e7b 100644 --- a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts +++ b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts @@ -28,7 +28,7 @@ import { DashboardEntry } from "../../../type/dashboard-entry"; import { Observable, of, Subject } from "rxjs"; import { debounceTime, switchMap } from "rxjs/operators"; import { UserService } from "../../../../common/service/user/user.service"; -import { DASHBOARD_SEARCH } from "../../../../app-routing.constant"; +import { SEARCH } from "../../../../app-routing.constant"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputGroupComponent, NzInputDirective } from "ng-zorro-antd/input"; @@ -137,7 +137,7 @@ export class SearchBarComponent { } performSearch(keyword: string) { - this.router.navigate([DASHBOARD_SEARCH], { queryParams: { q: keyword } }); + this.router.navigate([SEARCH], { queryParams: { q: keyword } }); } convertToName(resultItem: SearchResultItem): string { diff --git a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts index 2a40a06e20..d25ed19622 100644 --- a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts @@ -243,22 +243,22 @@ describe("ShareAccessComponent", () => { it("uses the workflow dashboard path when sharing a workflow", () => { const message = grantAndCaptureMessage(setupComponent({ type: "workflow", id: 11 })); - expect(message).toContain("/dashboard/user/workflow/11"); + expect(message).toContain("/user/workflow/11"); }); it("uses the dataset dashboard path when sharing a dataset", () => { const message = grantAndCaptureMessage(setupComponent({ type: "dataset", id: 22 })); - expect(message).toContain("/dashboard/user/dataset/22"); + expect(message).toContain("/user/dataset/22"); }); it("uses the project dashboard path when sharing a project", () => { const message = grantAndCaptureMessage(setupComponent({ type: "project", id: 33 })); - expect(message).toContain("/dashboard/user/project/33"); + expect(message).toContain("/user/project/33"); }); it("omits the access URL when sharing a computing-unit", () => { const message = grantAndCaptureMessage(setupComponent({ type: "computing-unit", id: 44 })); - expect(message).not.toContain("/dashboard/user/"); + expect(message).not.toContain("/user/"); }); it("calls ShareAccessService.grantAccess with the selected access level for each tag", () => { diff --git a/frontend/src/app/dashboard/component/user/share-access/share-access.component.ts b/frontend/src/app/dashboard/component/user/share-access/share-access.component.ts index b6b51b9709..81e3739d88 100644 --- a/frontend/src/app/dashboard/component/user/share-access/share-access.component.ts +++ b/frontend/src/app/dashboard/component/user/share-access/share-access.component.ts @@ -27,11 +27,7 @@ import { GmailService } from "../../../../common/service/gmail/gmail.service"; import { NZ_MODAL_DATA, NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { HttpErrorResponse } from "@angular/common/http"; -import { - DASHBOARD_USER_DATASET, - DASHBOARD_USER_PROJECT, - DASHBOARD_USER_WORKFLOW, -} from "../../../../app-routing.constant"; +import { USER_DATASET, USER_PROJECT, USER_WORKFLOW } from "../../../../app-routing.constant"; import { NzMessageService } from "ng-zorro-antd/message"; import { DatasetService } from "../../../service/user/dataset/dataset.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; @@ -196,9 +192,9 @@ export class ShareAccessComponent implements OnInit, OnDestroy { let message = `${this.userService.getCurrentUser()?.email} shared a ${this.type} with you`; if (this.type !== "computing-unit") { let routePath = ""; - if (this.type === "workflow") routePath = DASHBOARD_USER_WORKFLOW; - if (this.type === "dataset") routePath = DASHBOARD_USER_DATASET; - if (this.type === "project") routePath = DASHBOARD_USER_PROJECT; + if (this.type === "workflow") routePath = USER_WORKFLOW; + if (this.type === "dataset") routePath = USER_DATASET; + if (this.type === "project") routePath = USER_PROJECT; message += `, access the ${this.type} at ${location.origin}${routePath}/${this.id}`; } this.accessService diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html index 6fa0d48a78..1383680f8f 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html @@ -33,7 +33,7 @@ <div class="dataset-item-meta-title"> <a *ngIf="!editingName; else customDatasetTitle " - [routerLink]="DASHBOARD_USER_DATASET + '/' + dataset.did" + [routerLink]="USER_DATASET + '/' + dataset.did" class="dataset-name" >{{ dataset.name }}</a > diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts index 3e51374c9b..6846911823 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts @@ -25,7 +25,7 @@ import { ShareAccessComponent } from "../../share-access/share-access.component" import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { NzModalService } from "ng-zorro-antd/modal"; import { DashboardDataset } from "../../../../type/dashboard-dataset.interface"; -import { DASHBOARD_USER_DATASET } from "../../../../../app-routing.constant"; +import { USER_DATASET } from "../../../../../app-routing.constant"; import { NzListItemComponent, NzListItemMetaComponent, @@ -73,7 +73,7 @@ import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; ], }) export class UserDatasetListItemComponent { - protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET; + protected readonly USER_DATASET = USER_DATASET; private _entry?: DashboardDataset; diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts index 1556c2db63..f665b3ea81 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts @@ -19,7 +19,7 @@ import { of, Subject } from "rxjs"; import { UserDatasetComponent } from "./user-dataset.component"; -import { DASHBOARD_USER_DATASET } from "../../../../app-routing.constant"; +import { USER_DATASET } from "../../../../app-routing.constant"; import { UserDatasetVersionCreatorComponent } from "./user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component"; import { SortMethod } from "../../../type/sort-method"; import { User } from "../../../../common/type/user"; @@ -278,7 +278,7 @@ describe("UserDatasetComponent", () => { component.onClickOpenDatasetAddComponent(); - expect(routerMock.navigate).toHaveBeenCalledWith([`${DASHBOARD_USER_DATASET}/123`]); + expect(routerMock.navigate).toHaveBeenCalledWith([`${USER_DATASET}/123`]); }); it("on close with null result: does not navigate", () => { diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts index 165270c053..2deedba104 100644 --- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts @@ -28,7 +28,7 @@ import { DashboardEntry } from "../../../type/dashboard-entry"; import { SearchResultsComponent } from "../search-results/search-results.component"; import { FiltersComponent } from "../filters/filters.component"; import { firstValueFrom } from "rxjs"; -import { DASHBOARD_USER_DATASET } from "../../../../app-routing.constant"; +import { USER_DATASET } from "../../../../app-routing.constant"; import { NzModalService } from "ng-zorro-antd/modal"; import { UserDatasetVersionCreatorComponent } from "./user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component"; import { DashboardDataset } from "../../../type/dashboard-dataset.interface"; @@ -202,7 +202,7 @@ export class UserDatasetComponent implements AfterViewInit { modal.afterClose.pipe(untilDestroyed(this)).subscribe(result => { if (result != null) { const dashboardDataset: DashboardDataset = result as DashboardDataset; - this.router.navigate([`${DASHBOARD_USER_DATASET}/${dashboardDataset.dataset.did}`]); + this.router.navigate([`${USER_DATASET}/${dashboardDataset.dataset.did}`]); } }); } diff --git a/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.spec.ts b/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.spec.ts index 1681e2d1c0..2addd12014 100644 --- a/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.spec.ts @@ -18,6 +18,7 @@ */ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; import { UserIconComponent } from "./user-icon.component"; import { UserService } from "../../../../common/service/user/user.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; @@ -26,6 +27,7 @@ import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { RouterTestingModule } from "@angular/router/testing"; import { AboutComponent } from "../../../../hub/component/about/about.component"; import { commonTestProviders } from "../../../../common/testing/test-utils"; +import { ABOUT } from "../../../../app-routing.constant"; describe("UserIconComponent", () => { let component: UserIconComponent; @@ -52,4 +54,34 @@ describe("UserIconComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + describe("onClickLogout", () => { + it("navigates to /about (no /dashboard prefix) after logout", () => { + const router = TestBed.inject(Router); + const navigateSpy = vi.spyOn(router, "navigate").mockResolvedValue(true); + const userService = TestBed.inject(UserService); + const logoutSpy = vi.spyOn(userService, "logout").mockImplementation(() => {}); + + component.onClickLogout(); + + expect(logoutSpy).toHaveBeenCalledTimes(1); + expect(navigateSpy).toHaveBeenCalledWith([ABOUT]); + expect(ABOUT).toBe("/about"); + }); + + it("clears the flarum_remember cookie on logout", () => { + const router = TestBed.inject(Router); + vi.spyOn(router, "navigate").mockResolvedValue(true); + const userService = TestBed.inject(UserService); + vi.spyOn(userService, "logout").mockImplementation(() => {}); + // Seed the cookie so we can observe it being cleared. jsdom's + // document.cookie is the test surface here; assigning a value with a + // past expiry should expire the cookie immediately. + document.cookie = "flarum_remember=token; path=/;"; + + component.onClickLogout(); + + expect(document.cookie).not.toContain("flarum_remember=token"); + }); + }); }); diff --git a/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.ts b/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.ts index 4b1fd6d4de..94bb7d9d4e 100644 --- a/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.ts +++ b/frontend/src/app/dashboard/component/user/user-icon/user-icon.component.ts @@ -22,7 +22,7 @@ import { UserService } from "../../../../common/service/user/user.service"; import { User } from "../../../../common/type/user"; import { UntilDestroy } from "@ngneat/until-destroy"; import { Router } from "@angular/router"; -import { DASHBOARD_ABOUT } from "../../../../app-routing.constant"; +import { ABOUT } from "../../../../app-routing.constant"; import { UserAvatarComponent } from "../user-avatar/user-avatar.component"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; @@ -63,6 +63,6 @@ export class UserIconComponent { public onClickLogout(): void { this.userService.logout(); document.cookie = "flarum_remember=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; - this.router.navigate([DASHBOARD_ABOUT]); + this.router.navigate([ABOUT]); } } diff --git a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.ts b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.ts index 9bb6750630..562b465e8f 100644 --- a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.ts +++ b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.ts @@ -25,7 +25,7 @@ import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ShareAccessComponent } from "../../share-access/share-access.component"; import { NzModalService } from "ng-zorro-antd/modal"; import { UserService } from "../../../../../common/service/user/user.service"; -import { DASHBOARD_USER_PROJECT } from "../../../../../app-routing.constant"; +import { USER_PROJECT } from "../../../../../app-routing.constant"; import { NzListItemComponent, NzListItemMetaComponent, @@ -90,7 +90,7 @@ import { HighlightSearchTermsPipe } from "../../user-workflow/user-workflow-list ], }) export class UserProjectListItemComponent implements OnInit { - public readonly ROUTER_USER_PROJECT_BASE_URL = DASHBOARD_USER_PROJECT; + public readonly ROUTER_USER_PROJECT_BASE_URL = USER_PROJECT; public readonly MAX_PROJECT_DESCRIPTION_CHAR_COUNT = 10000; private _entry?: DashboardProject; @Input() public keywords: string[] = []; diff --git a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.html b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.html index 4affee3b44..6524c4dbc6 100644 --- a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.html +++ b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.html @@ -39,7 +39,7 @@ <div class="workflow-item-meta-title"> <div *ngIf="!editingName; else customWorkflowTitle " - [routerLink]="DASHBOARD_USER_WORKSPACE + '/' + workflow.wid" + [routerLink]="USER_WORKSPACE + '/' + workflow.wid" [innerHTML]="workflow.name | highlightSearchTerms : keywords" class="workflow-name"></div> <ng-template #customWorkflowTitle> @@ -139,7 +139,7 @@ class="project-label-name" [ngClass]="{'color-tag' : true, 'light-color' : isLightColor(userProjectsMap.get(projectID)!.color!), 'dark-color' : !isLightColor(userProjectsMap.get(projectID)!.color!)}" [ngStyle]="{'color' : isLightColor(userProjectsMap.get(projectID)!.color!) ? 'black' : 'white', 'background-color' : '#' + userProjectsMap.get(projectID)!.color}" - [routerLink]="DASHBOARD_USER_PROJECT + '/' + userProjectsMap.get(projectID)!.pid"> + [routerLink]="USER_PROJECT + '/' + userProjectsMap.get(projectID)!.pid"> {{userProjectsMap.get(projectID)!.name}} </a> <div diff --git a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.ts b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.ts index 4e849e6448..e26a4a7e54 100644 --- a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.ts +++ b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.ts @@ -32,7 +32,7 @@ import { UserProjectService } from "../../../../service/user/project/user-projec import { DashboardEntry } from "../../../../type/dashboard-entry"; import { firstValueFrom } from "rxjs"; import { DownloadService } from "src/app/dashboard/service/user/download/download.service"; -import { DASHBOARD_USER_PROJECT, DASHBOARD_USER_WORKSPACE } from "../../../../../app-routing.constant"; +import { USER_PROJECT, USER_WORKSPACE } from "../../../../../app-routing.constant"; import { GuiConfigService } from "../../../../../common/service/gui-config.service"; import { NzListItemComponent, @@ -88,8 +88,8 @@ import { HighlightSearchTermsPipe } from "./highlight-search-terms.pipe"; ], }) export class UserWorkflowListItemComponent { - protected readonly DASHBOARD_USER_WORKSPACE = DASHBOARD_USER_WORKSPACE; - protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT; + protected readonly USER_WORKSPACE = USER_WORKSPACE; + protected readonly USER_PROJECT = USER_PROJECT; private _entry?: DashboardEntry; @Input() public keywords: string[] = []; diff --git a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.spec.ts b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.spec.ts index 959977cd15..f082ed9dc2 100644 --- a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.spec.ts @@ -60,6 +60,8 @@ import { NzModalService } from "ng-zorro-antd/modal"; import { NzButtonModule } from "ng-zorro-antd/button"; import { DownloadService } from "../../../service/user/download/download.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; +import { Router } from "@angular/router"; +import { USER_WORKSPACE } from "../../../../app-routing.constant"; import type { Mocked } from "vitest"; describe("SavedWorkflowSectionComponent", () => { let component: UserWorkflowComponent; @@ -310,6 +312,23 @@ describe("SavedWorkflowSectionComponent", () => { ); }); + describe("onClickCreateNewWorkflowFromDashboard", () => { + it("navigates to /user/workflow/<wid> (no /dashboard prefix) on successful creation", () => { + const router = TestBed.inject(Router); + const navigateSpy = vi.spyOn(router, "navigate").mockResolvedValue(true); + const persist = TestBed.inject(WorkflowPersistService) as any; + // StubWorkflowPersistService doesn't define createWorkflow — assign the + // method here so the component's call resolves to a controlled observable. + persist.createWorkflow = vi.fn().mockReturnValue(of({ workflow: { wid: 99 } })); + component.pid = undefined; + + component.onClickCreateNewWorkflowFromDashboard(); + + expect(navigateSpy).toHaveBeenCalledWith([USER_WORKSPACE, 99]); + expect(USER_WORKSPACE).toBe("/user/workflow"); + }); + }); + it("downloads checked files", async () => { // If multiple workflows in a single batch download have name conflicts, rename them as workflow-1, workflow-2, etc. component.searchResultsComponent.entries = component.searchResultsComponent.entries.concat( diff --git a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts index f3129f1a9c..584191f8bf 100644 --- a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts +++ b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts @@ -43,7 +43,7 @@ import { UserProjectService } from "../../../service/user/project/user-project.s import { map, mergeMap, switchMap, tap } from "rxjs/operators"; import { DashboardWorkflow } from "../../../type/dashboard-workflow.interface"; import { DownloadService } from "../../../service/user/download/download.service"; -import { DASHBOARD_USER_WORKSPACE } from "../../../../app-routing.constant"; +import { USER_WORKSPACE } from "../../../../app-routing.constant"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzSpaceCompactItemDirective, NzSpaceCompactComponent } from "ng-zorro-antd/space"; @@ -293,7 +293,7 @@ export class UserWorkflowComponent implements AfterViewInit { .subscribe({ next: (wid: number | undefined) => { // Use the wid here for navigation - this.router.navigate([DASHBOARD_USER_WORKSPACE, wid]).then(null); + this.router.navigate([USER_WORKSPACE, wid]).then(null); }, error: (err: unknown) => this.notificationService.error("Workflow creation failed"), }); diff --git a/frontend/src/app/dashboard/service/admin/guard/admin-guard.service.ts b/frontend/src/app/dashboard/service/admin/guard/admin-guard.service.ts index f95d701194..d1e1318cc5 100644 --- a/frontend/src/app/dashboard/service/admin/guard/admin-guard.service.ts +++ b/frontend/src/app/dashboard/service/admin/guard/admin-guard.service.ts @@ -20,7 +20,7 @@ import { Injectable } from "@angular/core"; import { CanActivate, Router } from "@angular/router"; import { UserService } from "../../../../common/service/user/user.service"; -import { DASHBOARD_USER_WORKFLOW } from "../../../../app-routing.constant"; +import { USER_WORKFLOW } from "../../../../app-routing.constant"; /** * AuthGuardService is a service can tell the router whether @@ -37,7 +37,7 @@ export class AdminGuardService implements CanActivate { if (this.userService.isAdmin()) { return true; } else { - this.router.navigate([DASHBOARD_USER_WORKFLOW]); + this.router.navigate([USER_WORKFLOW]); return false; } } diff --git a/frontend/src/app/hub/component/about/local-login/local-login.component.ts b/frontend/src/app/hub/component/about/local-login/local-login.component.ts index 215b17e342..0096807439 100644 --- a/frontend/src/app/hub/component/about/local-login/local-login.component.ts +++ b/frontend/src/app/hub/component/about/local-login/local-login.component.ts @@ -25,7 +25,7 @@ import { UserService } from "../../../../common/service/user/user.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { catchError } from "rxjs/operators"; import { throwError } from "rxjs"; -import { DASHBOARD_USER_WORKFLOW } from "../../../../app-routing.constant"; +import { USER_WORKFLOW } from "../../../../app-routing.constant"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs"; import { NgIf } from "@angular/common"; @@ -131,9 +131,7 @@ export class LocalLoginComponent implements OnInit { }), untilDestroyed(this) ) - .subscribe(() => - this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || DASHBOARD_USER_WORKFLOW) - ); + .subscribe(() => this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || USER_WORKFLOW)); } /** diff --git a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts index a4c91b4abe..504101e32e 100644 --- a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts +++ b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts @@ -23,6 +23,13 @@ import { WorkflowPersistService } from "../../../common/service/workflow-persist import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; import { ChangeDetectorRef } from "@angular/core"; import { commonTestProviders } from "../../../common/testing/test-utils"; +import { DashboardEntry } from "../../../dashboard/type/dashboard-entry"; +import { + HUB_DATASET_RESULT_DETAIL, + HUB_WORKFLOW_RESULT_DETAIL, + USER_DATASET, + USER_WORKSPACE, +} from "../../../app-routing.constant"; describe("BrowseSectionComponent", () => { let component: BrowseSectionComponent; @@ -46,4 +53,34 @@ describe("BrowseSectionComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + describe("entityRoutes initialization", () => { + it("routes owned workflows to the user workspace", () => { + component.currentUid = 1; + component.entities = [{ id: 100, type: "workflow", accessibleUserIds: [1] } as unknown as DashboardEntry]; + component.ngOnInit(); + expect(component.entityRoutes[100]).toEqual([USER_WORKSPACE, "100"]); + }); + + it("routes non-owned workflows to the hub workflow detail page", () => { + component.currentUid = 1; + component.entities = [{ id: 101, type: "workflow", accessibleUserIds: [2] } as unknown as DashboardEntry]; + component.ngOnInit(); + expect(component.entityRoutes[101]).toEqual([HUB_WORKFLOW_RESULT_DETAIL, "101"]); + }); + + it("routes owned datasets to the user dataset page", () => { + component.currentUid = 1; + component.entities = [{ id: 200, type: "dataset", accessibleUserIds: [1] } as unknown as DashboardEntry]; + component.ngOnInit(); + expect(component.entityRoutes[200]).toEqual([USER_DATASET, "200"]); + }); + + it("routes non-owned datasets to the hub dataset detail page", () => { + component.currentUid = 1; + component.entities = [{ id: 201, type: "dataset", accessibleUserIds: [2] } as unknown as DashboardEntry]; + component.ngOnInit(); + expect(component.entityRoutes[201]).toEqual([HUB_DATASET_RESULT_DETAIL, "201"]); + }); + }); }); diff --git a/frontend/src/app/hub/component/browse-section/browse-section.component.ts b/frontend/src/app/hub/component/browse-section/browse-section.component.ts index 7629a8906d..659e018e1e 100644 --- a/frontend/src/app/hub/component/browse-section/browse-section.component.ts +++ b/frontend/src/app/hub/component/browse-section/browse-section.component.ts @@ -23,10 +23,10 @@ import { WorkflowPersistService } from "../../../common/service/workflow-persist import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; import { UntilDestroy } from "@ngneat/until-destroy"; import { - DASHBOARD_HUB_DATASET_RESULT_DETAIL, - DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, - DASHBOARD_USER_DATASET, - DASHBOARD_USER_WORKSPACE, + HUB_DATASET_RESULT_DETAIL, + HUB_WORKFLOW_RESULT_DETAIL, + USER_DATASET, + USER_WORKSPACE, } from "../../../app-routing.constant"; import { AppSettings } from "../../../common/app-setting"; import { NgIf, NgFor, NgStyle, DatePipe } from "@angular/common"; @@ -59,10 +59,10 @@ export class BrowseSectionComponent implements OnInit, OnChanges { @Input() currentUid: number | undefined; defaultBackground: string = "../../../../../assets/card_background.jpg"; - protected readonly DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL; - protected readonly DASHBOARD_USER_WORKSPACE = DASHBOARD_USER_WORKSPACE; - protected readonly DASHBOARD_HUB_DATASET_RESULT_DETAIL = DASHBOARD_HUB_DATASET_RESULT_DETAIL; - protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET; + protected readonly HUB_WORKFLOW_RESULT_DETAIL = HUB_WORKFLOW_RESULT_DETAIL; + protected readonly USER_WORKSPACE = USER_WORKSPACE; + protected readonly HUB_DATASET_RESULT_DETAIL = HUB_DATASET_RESULT_DETAIL; + protected readonly USER_DATASET = USER_DATASET; entityRoutes: { [key: number]: string[] } = {}; private coverImageUrls = new Map<number, string>(); @@ -97,15 +97,15 @@ export class BrowseSectionComponent implements OnInit, OnChanges { if (entity.type === "workflow") { if (this.currentUid !== undefined && owners.includes(this.currentUid)) { - this.entityRoutes[entityId] = [this.DASHBOARD_USER_WORKSPACE, String(entityId)]; + this.entityRoutes[entityId] = [this.USER_WORKSPACE, String(entityId)]; } else { - this.entityRoutes[entityId] = [this.DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, String(entityId)]; + this.entityRoutes[entityId] = [this.HUB_WORKFLOW_RESULT_DETAIL, String(entityId)]; } } else if (entity.type === "dataset") { if (this.currentUid !== undefined && owners.includes(this.currentUid)) { - this.entityRoutes[entityId] = [this.DASHBOARD_USER_DATASET, String(entityId)]; + this.entityRoutes[entityId] = [this.USER_DATASET, String(entityId)]; } else { - this.entityRoutes[entityId] = [this.DASHBOARD_HUB_DATASET_RESULT_DETAIL, String(entityId)]; + this.entityRoutes[entityId] = [this.HUB_DATASET_RESULT_DETAIL, String(entityId)]; } } else { throw new Error("Unexpected type in DashboardEntry."); diff --git a/frontend/src/app/hub/component/hub.component.html b/frontend/src/app/hub/component/hub.component.html index 5ba23062a3..7343cd7d71 100644 --- a/frontend/src/app/hub/component/hub.component.html +++ b/frontend/src/app/hub/component/hub.component.html @@ -24,7 +24,7 @@ nz-tooltip="Home Page" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_HOME"> + [routerLink]="HOME"> <span nz-icon nzType="home"></span> @@ -36,7 +36,7 @@ nz-tooltip="Search public workflows" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_HUB_WORKFLOW_RESULT"> + [routerLink]="HUB_WORKFLOW_RESULT"> <span nz-icon nzType="project"></span> @@ -48,7 +48,7 @@ nz-tooltip="Search public dataset" nzMatchRouter="true" nzTooltipPlacement="right" - [routerLink]="DASHBOARD_HUB_DATASET_RESULT"> + [routerLink]="HUB_DATASET_RESULT"> <span nz-icon nzType="database"></span> diff --git a/frontend/src/app/hub/component/hub.component.ts b/frontend/src/app/hub/component/hub.component.ts index ad9bc079e0..ee006fa255 100644 --- a/frontend/src/app/hub/component/hub.component.ts +++ b/frontend/src/app/hub/component/hub.component.ts @@ -18,11 +18,7 @@ */ import { Component, Input } from "@angular/core"; -import { - DASHBOARD_HOME, - DASHBOARD_HUB_DATASET_RESULT, - DASHBOARD_HUB_WORKFLOW_RESULT, -} from "../../app-routing.constant"; +import { HOME, HUB_DATASET_RESULT, HUB_WORKFLOW_RESULT } from "../../app-routing.constant"; import { GuiConfigService } from "../../common/service/gui-config.service"; import { SidebarTabs } from "../../common/type/gui-config"; import { NgIf } from "@angular/common"; @@ -41,9 +37,9 @@ import { NzIconDirective } from "ng-zorro-antd/icon"; export class HubComponent { @Input() isLogin: boolean = false; @Input() sidebarTabs: SidebarTabs = {} as SidebarTabs; - protected readonly DASHBOARD_HOME = DASHBOARD_HOME; - protected readonly DASHBOARD_HUB_WORKFLOW_RESULT = DASHBOARD_HUB_WORKFLOW_RESULT; - protected readonly DASHBOARD_HUB_DATASET_RESULT = DASHBOARD_HUB_DATASET_RESULT; + protected readonly HOME = HOME; + protected readonly HUB_WORKFLOW_RESULT = HUB_WORKFLOW_RESULT; + protected readonly HUB_DATASET_RESULT = HUB_DATASET_RESULT; constructor(protected config: GuiConfigService) {} } diff --git a/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts b/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts index 8585ee1c54..d81b085037 100644 --- a/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts +++ b/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts @@ -30,11 +30,7 @@ import { UserService } from "../../../common/service/user/user.service"; import { StubUserService } from "../../../common/service/user/stub-user.service"; import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; -import { - DASHBOARD_HOME, - DASHBOARD_HUB_DATASET_RESULT, - DASHBOARD_HUB_WORKFLOW_RESULT, -} from "../../../app-routing.constant"; +import { HOME, HUB_DATASET_RESULT, HUB_WORKFLOW_RESULT } from "../../../app-routing.constant"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("LandingPageComponent", () => { @@ -196,18 +192,18 @@ describe("LandingPageComponent", () => { it("navigateToSearch routes to the workflow hub result for 'workflow'", () => { build(); component.navigateToSearch("workflow"); - expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HUB_WORKFLOW_RESULT]); + expect(routerNavigateSpy).toHaveBeenCalledWith([HUB_WORKFLOW_RESULT]); }); it("navigateToSearch routes to the dataset hub result for 'dataset'", () => { build(); component.navigateToSearch("dataset"); - expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HUB_DATASET_RESULT]); + expect(routerNavigateSpy).toHaveBeenCalledWith([HUB_DATASET_RESULT]); }); it("navigateToSearch routes to the dashboard home for an unknown type", () => { build(); component.navigateToSearch("something-else"); - expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HOME]); + expect(routerNavigateSpy).toHaveBeenCalledWith([HOME]); }); }); diff --git a/frontend/src/app/hub/component/landing-page/landing-page.component.ts b/frontend/src/app/hub/component/landing-page/landing-page.component.ts index b8a7e02de3..2f8a2fff7d 100644 --- a/frontend/src/app/hub/component/landing-page/landing-page.component.ts +++ b/frontend/src/app/hub/component/landing-page/landing-page.component.ts @@ -24,11 +24,7 @@ import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Router } from "@angular/router"; import { SearchService } from "../../../dashboard/service/user/search.service"; import { DashboardEntry } from "../../../dashboard/type/dashboard-entry"; -import { - DASHBOARD_HOME, - DASHBOARD_HUB_DATASET_RESULT, - DASHBOARD_HUB_WORKFLOW_RESULT, -} from "../../../app-routing.constant"; +import { HOME, HUB_DATASET_RESULT, HUB_WORKFLOW_RESULT } from "../../../app-routing.constant"; import { UserService } from "../../../common/service/user/user.service"; import { BrowseSectionComponent } from "../browse-section/browse-section.component"; @@ -119,13 +115,13 @@ export class LandingPageComponent implements OnInit { switch (type) { case "workflow": - path = DASHBOARD_HUB_WORKFLOW_RESULT; + path = HUB_WORKFLOW_RESULT; break; case "dataset": - path = DASHBOARD_HUB_DATASET_RESULT; + path = HUB_DATASET_RESULT; break; default: - path = DASHBOARD_HOME; + path = HOME; } this.router.navigate([path]); 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 5d76382f76..810672c230 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 @@ -30,7 +30,7 @@ import { Role, User } from "src/app/common/type/user"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; -import { DASHBOARD_HUB_WORKFLOW_RESULT, DASHBOARD_USER_WORKSPACE } from "../../../../app-routing.constant"; +import { HUB_WORKFLOW_RESULT, USER_WORKSPACE } from "../../../../app-routing.constant"; import { NgIf, NgClass } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; @@ -203,7 +203,7 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI } goBack(): void { - this.router.navigateByUrl(DASHBOARD_HUB_WORKFLOW_RESULT).catch(() => { + this.router.navigateByUrl(HUB_WORKFLOW_RESULT).catch(() => { this.notificationService.error("Go back failed. Please try again."); }); } @@ -216,7 +216,7 @@ export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnI .cloneWorkflow(this.wid) .pipe(untilDestroyed(this)) .subscribe(newWid => { - this.router.navigate([`${DASHBOARD_USER_WORKSPACE}/${newWid}`]).then(() => { + this.router.navigate([`${USER_WORKSPACE}/${newWid}`]).then(() => { this.notificationService.success("Clone Successful"); }); }); diff --git a/frontend/src/app/workspace/component/menu/menu.component.html b/frontend/src/app/workspace/component/menu/menu.component.html index a21e4d5642..85a856eaba 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.html +++ b/frontend/src/app/workspace/component/menu/menu.component.html @@ -85,7 +85,7 @@ <nz-space-compact id="user-buttons" *ngIf="!displayParticularWorkflowVersion"> - <a [routerLink]="DASHBOARD_USER_WORKFLOW"> + <a [routerLink]="USER_WORKFLOW"> <button nz-button title="dashboard"> diff --git a/frontend/src/app/workspace/component/menu/menu.component.spec.ts b/frontend/src/app/workspace/component/menu/menu.component.spec.ts index 1449174215..a1865a2628 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.spec.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.spec.ts @@ -45,6 +45,8 @@ import { saveAs } from "file-saver"; import type { ModalOptions } from "ng-zorro-antd/modal"; import type { ComputingUnitSelectionComponent } from "../power-button/computing-unit-selection.component"; import { WorkflowContent } from "../../../common/type/workflow"; +import { Router } from "@angular/router"; +import { USER_WORKFLOW } from "../../../app-routing.constant"; import type { Mocked } from "vitest"; vi.mock("file-saver", () => ({ saveAs: vi.fn() })); @@ -466,6 +468,31 @@ describe("MenuComponent", () => { }) ); }); + + it("navigates to /user/workflow (no /dashboard prefix) when the modal reports the owner revoked their own access", async () => { + vi.spyOn(workflowPersistService, "retrieveOwners").mockReturnValue(of([])); + const fakeModalRef = { afterClose: of({ userRevokedOwnAccess: true }) } as unknown as NzModalRef; + vi.spyOn(modalService, "create").mockReturnValue(fakeModalRef); + const router = TestBed.inject(Router); + const navigateSpy = vi.spyOn(router, "navigate").mockResolvedValue(true); + + await component.onClickOpenShareAccess(); + + expect(navigateSpy).toHaveBeenCalledWith([USER_WORKFLOW]); + expect(USER_WORKFLOW).toBe("/user/workflow"); + }); + + it("does not navigate when the share-access modal closes without revoking own access", async () => { + vi.spyOn(workflowPersistService, "retrieveOwners").mockReturnValue(of([])); + const fakeModalRef = { afterClose: of(undefined) } as unknown as NzModalRef; + vi.spyOn(modalService, "create").mockReturnValue(fakeModalRef); + const router = TestBed.inject(Router); + const navigateSpy = vi.spyOn(router, "navigate").mockResolvedValue(true); + + await component.onClickOpenShareAccess(); + + expect(navigateSpy).not.toHaveBeenCalled(); + }); }); it("onClickCreateNewWorkflow resets the graph and navigates back to root", () => { diff --git a/frontend/src/app/workspace/component/menu/menu.component.ts b/frontend/src/app/workspace/component/menu/menu.component.ts index b5621d06d3..69f08e4fef 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.ts +++ b/frontend/src/app/workspace/component/menu/menu.component.ts @@ -50,7 +50,7 @@ import { ResultExportationComponent } from "../result-exportation/result-exporta import { ReportGenerationService } from "../../service/report-generation/report-generation.service"; import { ShareAccessComponent } from "src/app/dashboard/component/user/share-access/share-access.component"; import { PanelService } from "../../service/panel/panel.service"; -import { DASHBOARD_USER_WORKFLOW } from "../../../app-routing.constant"; +import { USER_WORKFLOW } from "../../../app-routing.constant"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { ComputingUnitState } from "../../../common/type/computing-unit-connection.interface"; import { ComputingUnitSelectionComponent } from "../power-button/computing-unit-selection.component"; @@ -138,7 +138,7 @@ export class MenuComponent implements OnInit, OnDestroy { public showGrid: boolean = false; public showNumWorkers: boolean = false; public showStatus: boolean = false; - protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW; + protected readonly USER_WORKFLOW = USER_WORKFLOW; @Input() public writeAccess: boolean = false; @Input() public pid?: number = undefined; @@ -344,7 +344,7 @@ export class MenuComponent implements OnInit, OnDestroy { modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(result => { if (result?.userRevokedOwnAccess) { - this.router.navigate([DASHBOARD_USER_WORKFLOW]); + this.router.navigate([USER_WORKFLOW]); } }); } diff --git a/frontend/src/app/workspace/component/workspace.component.spec.ts b/frontend/src/app/workspace/component/workspace.component.spec.ts index f151ec3e4f..7659f346f3 100644 --- a/frontend/src/app/workspace/component/workspace.component.spec.ts +++ b/frontend/src/app/workspace/component/workspace.component.spec.ts @@ -45,6 +45,7 @@ import { OperatorReuseCacheStatusService } from "../service/workflow-status/oper import { EntityType, HubService } from "../../hub/service/hub.service"; import { commonTestProviders } from "../../common/testing/test-utils"; import { WorkspaceComponent } from "./workspace.component"; +import { USER_WORKSPACE } from "../../app-routing.constant"; describe("WorkspaceComponent", () => { let component: WorkspaceComponent; @@ -305,6 +306,51 @@ describe("WorkspaceComponent", () => { component.registerAutoPersistWorkflow(); expect(workflowActionService.workflowChanged).toHaveBeenCalledTimes(1); }); + + it("updates the URL via location.go to /user/workflow/<wid> (no /dashboard prefix) when the persisted wid differs", async () => { + vi.useFakeTimers(); + try { + const workflowChanged$ = new Subject<void>(); + await createFixture(); + workflowActionService.workflowChanged.mockReturnValue(workflowChanged$.asObservable()); + // Persist returns a workflow with a different wid than what's currently + // on the metadata (wid: 42 in the stub). That mismatch is the trigger + // for the URL update. + const persistedWorkflow = { ...stubWorkflow, wid: 99 } as Workflow; + workflowPersistService.persistWorkflow.mockReturnValue(of(persistedWorkflow)); + + component.registerAutoPersistWorkflow(); + workflowChanged$.next(); + // Flush the debounceTime(SAVE_DEBOUNCE_TIME_IN_MS). + vi.advanceTimersByTime(5000); + + expect(locationMock.go).toHaveBeenCalledWith(`${USER_WORKSPACE}/99`); + expect(USER_WORKSPACE).toBe("/user/workflow"); + } finally { + vi.useRealTimers(); + } + }); + + it("skips the URL update when the persisted wid matches the current metadata", async () => { + vi.useFakeTimers(); + try { + const workflowChanged$ = new Subject<void>(); + await createFixture(); + workflowActionService.workflowChanged.mockReturnValue(workflowChanged$.asObservable()); + // Metadata wid is 42, persisted wid is also 42 → no URL update. + workflowPersistService.persistWorkflow.mockReturnValue(of(stubWorkflow)); + + component.registerAutoPersistWorkflow(); + workflowChanged$.next(); + vi.advanceTimersByTime(5000); + + expect(locationMock.go).not.toHaveBeenCalled(); + // Metadata is still synced even when the URL doesn't change. + expect(workflowActionService.setWorkflowMetadata).toHaveBeenCalledWith(stubWorkflow); + } finally { + vi.useRealTimers(); + } + }); }); describe("updateViewCount", () => { diff --git a/frontend/src/app/workspace/component/workspace.component.ts b/frontend/src/app/workspace/component/workspace.component.ts index 9968c26f64..da220feaab 100644 --- a/frontend/src/app/workspace/component/workspace.component.ts +++ b/frontend/src/app/workspace/component/workspace.component.ts @@ -49,7 +49,7 @@ import { WorkflowMetadata } from "src/app/dashboard/type/workflow-metadata.inter import { EntityType, HubService } from "../../hub/service/hub.service"; import { THROTTLE_TIME_MS } from "../../hub/component/workflow/detail/hub-workflow-detail.component"; import { WorkflowCompilingService } from "../service/compile-workflow/workflow-compiling.service"; -import { DASHBOARD_USER_WORKSPACE } from "../../app-routing.constant"; +import { USER_WORKSPACE } from "../../app-routing.constant"; import { GuiConfigService } from "../../common/service/gui-config.service"; import { checkIfWorkflowBroken } from "../../common/util/workflow-check"; import { NzSpinComponent } from "ng-zorro-antd/spin"; @@ -204,7 +204,7 @@ export class WorkspaceComponent implements AfterViewInit, OnInit, OnDestroy { .pipe(untilDestroyed(this)) .subscribe((updatedWorkflow: Workflow) => { if (this.workflowActionService.getWorkflowMetadata().wid !== updatedWorkflow.wid) { - this.location.go(`${DASHBOARD_USER_WORKSPACE}/${updatedWorkflow.wid}`); + this.location.go(`${USER_WORKSPACE}/${updatedWorkflow.wid}`); } this.workflowActionService.setWorkflowMetadata(updatedWorkflow); });
