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-5168-d1cf4ebde7f49346533e3c9506013f97c7fef88f in repository https://gitbox.apache.org/repos/asf/texera.git
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))
