You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/06/01 23:42:11 UTC
(texera) 01/02: refactor: Drop /dashboard prefix from user-facing URLs (#5151)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
commit d26bff669e0e253da451be3e64567a353036e00d
Author: Matthew B. <mg...@uci.edu>
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. <mg...@uci.edu>
Co-authored-by: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
.../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);
});