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
The following commit(s) were added to refs/heads/main by this push:
new 0f5f791e78 fix: route share-by-email link to correct dashboard path by
type (#5168)
0f5f791e78 is described below
commit 0f5f791e78d144af4b0cc1b29f5d5bb3400f5169
Author: Meng Wang <[email protected]>
AuthorDate: Sat May 23 16:37:53 2026 -0700
fix: route share-by-email link to correct dashboard path by type (#5168)
### What changes were proposed in this PR?
`ShareAccessComponent.grantAccess` hard-coded the
`dashboard/user/workflow/` path segment when constructing the
share-notification email's "access the ... at ..." URL, regardless of
which resource type the dialog was opened for. Because the dialog is
opened with `type: "dataset"` / `type: "project"` / `type: "workflow"`
from the respective list-item components, the link in the email for
shared **datasets** and shared **projects** pointed at the workflow
route and 404'd on click.
Branch on `this.type` to pick the correct route from the existing
`app-routing.constant.ts` constants:
- `workflow` → `DASHBOARD_USER_WORKFLOW` (`/dashboard/user/workflow`)
- `dataset` → `DASHBOARD_USER_DATASET` (`/dashboard/user/dataset`)
- `project` → `DASHBOARD_USER_PROJECT` (`/dashboard/user/project`)
`computing-unit` is unaffected — the existing `if (this.type !==
"computing-unit")` guard already skips the URL line for that type.
Note: the issue mentioned the dataset case only; the project case has
the same defect and is fixed by the same change.
### Any related issues, documentation, discussions?
Closes #5163.
Follow-up to #4200 (which fixed the missing `/dashboard/user/` prefix on
the same line per #3583); this fixes the remaining hard-coded `workflow`
segment.
### How was this PR tested?
Added `share-access.component.spec.ts` (Vitest + TestBed +
`HttpClientTestingModule`) with four cases that exercise `grantAccess`:
- `type=workflow`, id=11 → email message contains
`/dashboard/user/workflow/11`
- `type=dataset`, id=22 → email message contains
`/dashboard/user/dataset/22`
- `type=project`, id=33 → email message contains
`/dashboard/user/project/33`
- `type=computing-unit`, id=44 → email message does NOT contain
`/dashboard/user/`
Each case mocks the component's injected dependencies
(`ShareAccessService`, `UserService`, `GmailService`,
`NotificationService`, `NzMessageService`, `NzModalService`,
`NzModalRef`, `WorkflowPersistService`, `DatasetService`,
`WorkflowActionService`, `NZ_MODAL_DATA`), runs `grantAccess`, and
inspects the message passed to `gmailService.sendEmail` to confirm the
URL formatting.
Verified locally:
- `yarn test --include='**/share-access.component.spec.ts'` — 4 passed.
- `yarn prettier --check
src/app/dashboard/component/user/share-access/share-access.component.{ts,spec.ts}`
— clean.
Manual verification path:
1. Share a dataset via the dataset list dashboard, entering a real email
address.
2. Check the recipient's inbox: the link should now read
`https://<host>/dashboard/user/dataset/<did>` and resolve to the dataset
detail page on click.
3. Repeat for a project: link is `/dashboard/user/project/<pid>`.
4. Repeat for a workflow: link is `/dashboard/user/workflow/<wid>`
(unchanged behavior).
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (claude-opus-4-7)
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.../share-access/share-access.component.spec.ts | 110 +++++++++++++++++++++
.../user/share-access/share-access.component.ts | 14 ++-
2 files changed, 122 insertions(+), 2 deletions(-)
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
new file mode 100644
index 0000000000..b5c525956e
--- /dev/null
+++
b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { of } from "rxjs";
+
+import { NZ_MODAL_DATA, NzModalRef, NzModalService } from
"ng-zorro-antd/modal";
+import { NzMessageService } from "ng-zorro-antd/message";
+
+import { ShareAccessComponent } from "./share-access.component";
+import { ShareAccessService } from
"../../../service/user/share-access/share-access.service";
+import { UserService } from "../../../../common/service/user/user.service";
+import { GmailService } from "../../../../common/service/gmail/gmail.service";
+import { NotificationService } from
"../../../../common/service/notification/notification.service";
+import { DatasetService } from "../../../service/user/dataset/dataset.service";
+import { WorkflowPersistService } from
"src/app/common/service/workflow-persist/workflow-persist.service";
+import { WorkflowActionService } from
"src/app/workspace/service/workflow-graph/model/workflow-action.service";
+
+describe("ShareAccessComponent.grantAccess", () => {
+ let gmailSpy: { sendEmail: ReturnType<typeof vi.fn> };
+ let accessServiceSpy: {
+ grantAccess: ReturnType<typeof vi.fn>;
+ getAccessList: ReturnType<typeof vi.fn>;
+ getOwner: ReturnType<typeof vi.fn>;
+ };
+
+ function setupComponent(type: string, id: number): ShareAccessComponent {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule, NoopAnimationsModule,
ShareAccessComponent],
+ providers: [
+ { provide: NZ_MODAL_DATA, useValue: { type, id, allOwners: [],
inWorkspace: false } },
+ { provide: ShareAccessService, useValue: accessServiceSpy },
+ {
+ provide: UserService,
+ useValue: { getCurrentUser: () => ({ email: "[email protected]" }) },
+ },
+ { provide: GmailService, useValue: gmailSpy },
+ { provide: NotificationService, useValue: { success: vi.fn(), error:
vi.fn() } },
+ { provide: NzMessageService, useValue: { error: vi.fn() } },
+ { provide: NzModalService, useValue: {} },
+ { provide: NzModalRef, useValue: { close: vi.fn() } },
+ {
+ provide: WorkflowPersistService,
+ useValue: { getWorkflowIsPublished:
vi.fn().mockReturnValue(of("Private")) },
+ },
+ {
+ provide: DatasetService,
+ useValue: {
+ getDataset: vi.fn().mockReturnValue(of({ dataset: { isPublic:
false } })),
+ },
+ },
+ { provide: WorkflowActionService, useValue: {} },
+ ],
+ });
+ return TestBed.createComponent(ShareAccessComponent).componentInstance;
+ }
+
+ beforeEach(() => {
+ gmailSpy = { sendEmail: vi.fn() };
+ accessServiceSpy = {
+ grantAccess: vi.fn().mockReturnValue(of(null)),
+ getAccessList: vi.fn().mockReturnValue(of([])),
+ getOwner: vi.fn().mockReturnValue(of("[email protected]")),
+ };
+ });
+
+ function grantAndCaptureMessage(c: ShareAccessComponent): string {
+ c.emailTags = ["[email protected]"];
+ c.grantAccess();
+ return gmailSpy.sendEmail.mock.calls[0][1] as string;
+ }
+
+ it("uses the workflow dashboard path when sharing a workflow", () => {
+ const message = grantAndCaptureMessage(setupComponent("workflow", 11));
+ expect(message).toContain("/dashboard/user/workflow/11");
+ });
+
+ it("uses the dataset dashboard path when sharing a dataset", () => {
+ const message = grantAndCaptureMessage(setupComponent("dataset", 22));
+ expect(message).toContain("/dashboard/user/dataset/22");
+ });
+
+ it("uses the project dashboard path when sharing a project", () => {
+ const message = grantAndCaptureMessage(setupComponent("project", 33));
+ expect(message).toContain("/dashboard/user/project/33");
+ });
+
+ it("omits the access URL when sharing a computing-unit", () => {
+ const message = grantAndCaptureMessage(setupComponent("computing-unit",
44));
+ expect(message).not.toContain("/dashboard/user/");
+ });
+});
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 576e104aed..b6b51b9709 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,6 +27,11 @@ 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 { 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";
@@ -189,8 +194,13 @@ export class ShareAccessComponent implements OnInit,
OnDestroy {
if (this.emailTags.length > 0) {
this.emailTags.forEach(email => {
let message = `${this.userService.getCurrentUser()?.email} shared a
${this.type} with you`;
- if (this.type !== "computing-unit")
- message += `, access the ${this.type} at
${location.origin}/dashboard/user/workflow/${this.id}`;
+ 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;
+ message += `, access the ${this.type} at
${location.origin}${routePath}/${this.id}`;
+ }
this.accessService
.grantAccess(this.type, this.id, email,
this.validateForm.value.accessLevel)
.pipe(untilDestroyed(this))