diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.html b/frontend/src/app/dashboard/component/user/list-item/list-item.component.html index 20b98b71ac4..88a0c138519 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.html +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.html @@ -74,7 +74,9 @@ -
+
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 eca823a4ee7..44b3c4aa112 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 @@ -25,6 +25,7 @@ import { NzModalService } from "ng-zorro-antd/modal"; import { of, Subject, throwError } from "rxjs"; import { ActionType, HubService } from "../../../../hub/service/hub.service"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { By } from "@angular/platform-browser"; import { RouterTestingModule } from "@angular/router/testing"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { UserService } from "../../../../common/service/user/user.service"; @@ -265,7 +266,12 @@ describe("ListItemComponent", () => { it("onEditDescription opens the edit modal and applies the change, then closes it", () => { component.editable = true; - component.entry = { id: 1, description: "old", type: "workflow" } as unknown as DashboardEntry; + component.entry = { + id: 1, + description: "old", + type: "workflow", + accessLevel: "WRITE", + } as unknown as DashboardEntry; const descriptionChange = new Subject(); const modalRef = { componentInstance: { descriptionChange }, destroy: vi.fn() }; const createSpy = vi.spyOn(modalService, "create").mockReturnValue(modalRef as any); @@ -288,6 +294,72 @@ describe("ListItemComponent", () => { expect(createSpy).not.toHaveBeenCalled(); }); + describe("read-only entries cannot edit the description (#3497)", () => { + // `editable` only means "private dashboard view" and is hardcoded true by the + // container, so the row itself must consult the entry's access level. + const makeEntry = (accessLevel: string) => + ({ + id: 1, + name: "wf", + description: "old", + type: "workflow", + accessLevel, + workflow: { isOwner: accessLevel === "WRITE", accessLevel }, + accessibleUserIds: [], + likeCount: 0, + viewCount: 0, + isLiked: false, + size: 0, + }) as unknown as DashboardEntry; + + const stubModal = () => + vi + .spyOn(modalService, "create") + .mockReturnValue({ + componentInstance: { descriptionChange: new Subject() }, + destroy: vi.fn(), + } as any); + + it("does not open the description editor without WRITE access", () => { + component.editable = true; + component.entry = makeEntry("READ"); + const createSpy = stubModal(); + + component.onEditDescription(); + + expect(createSpy).not.toHaveBeenCalled(); + expect(workflowPersistService.updateWorkflowDescription).not.toHaveBeenCalled(); + }); + + it("hides the Edit Description control for read-only entries", () => { + component.editable = true; + component.isPrivateSearch = true; + component.entry = makeEntry("READ"); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css('button[title="Edit Description"]'))).toBeNull(); + }); + + it("still opens the description editor with WRITE access", () => { + component.editable = true; + component.entry = makeEntry("WRITE"); + const createSpy = stubModal(); + + component.onEditDescription(); + + expect(createSpy).toHaveBeenCalled(); + }); + + it("still renders the Edit Description control with WRITE access", () => { + component.editable = true; + component.isPrivateSearch = true; + component.entry = makeEntry("WRITE"); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css('button[title="Edit Description"]'))).not.toBeNull(); + }); + }); + it("onCheckboxChange toggles the entry's checked flag and emits the change", () => { const entry = { checked: false } as unknown as DashboardEntry; const emitSpy = vi.fn(); 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 5291b79b1d0..a6eb482779c 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 @@ -275,8 +275,17 @@ export class ListItemComponent implements OnChanges { }, 0); } + /** + * `editable` only means "this is the private dashboard view" and is hardcoded to true + * by the container, so it is not an access check. Editing the description additionally + * requires write access to this specific entry (#3497). + */ + get canEditDescription(): boolean { + return this.editable && this.entry.accessLevel === "WRITE"; + } + onEditDescription(): void { - if (!this.editable) return; + if (!this.canEditDescription) return; this.originalDescription = this.entry.description;