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;