diff --git a/doc/PRODUCT.md b/doc/PRODUCT.md
index 6ddeae94ac0..5cf4795e89c 100644
--- a/doc/PRODUCT.md
+++ b/doc/PRODUCT.md
@@ -148,6 +148,7 @@ The task Filters menu also offers **Routine tasks**. It selects tasks with
when other automatic tasks are hidden. The routines list's **View all tasks**
link opens `/issues?view=all&originKind=routine_execution&sortField=created&sortDir=desc` with that checkbox
selected. The URL retains the selection across reloads and task navigation.
+Coordinator task labels use repeated `label` URL parameters so selected labels survive reloads and shared task-list links.
Sort controls save `sortField` and `sortDir` in the URL. URL sorting overrides
saved collection sorting, and the routines link opens newest-created tasks first.
Shared sort links work across Coordinator, Streamlined UI, and Classic UI, including
diff --git a/ui/src/pages/CoordinatorTasks.test.tsx b/ui/src/pages/CoordinatorTasks.test.tsx
index 97a6e2de3dd..3b038bc6692 100644
--- a/ui/src/pages/CoordinatorTasks.test.tsx
+++ b/ui/src/pages/CoordinatorTasks.test.tsx
@@ -6,25 +6,31 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createIssue } from "../../storybook/fixtures/paperclipData";
import { coordinatorAttentionTasksQueryKey } from "@/lib/coordinator-attention";
import { MemoryRouter } from "@/lib/router";
-import { useLocation, useNavigate } from "react-router-dom";
+import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { CoordinatorTasks } from "./CoordinatorTasks";
+import { Issues } from "./Issues";
import { queryKeys } from "@/lib/queryKeys";
+import { IssueFilterPanel } from "@/components/IssueFilterPanel";
+import type { ComponentProps } from "react";
import type { TaskListSort } from "@/lib/task-list";
import { taskListViews, taskListViewStorageKey, type TaskListView } from "@/lib/task-list-views";
-const mocks = vi.hoisted(() => ({ list: vi.fn(), get: vi.fn(), attention: vi.fn(), breadcrumbs: vi.fn(), liveRuns: vi.fn(), localTrusted: false, identity: { userId: "person" as string | null, settled: true } }));
+const mocks = vi.hoisted(() => ({ list: vi.fn(), get: vi.fn(), attention: vi.fn(), breadcrumbs: vi.fn(), liveRuns: vi.fn(), localTrusted: false, realFilters: false, labels: [] as { id: string; name: string; color: string }[], identity: { userId: "person" as string | null, settled: true } }));
vi.mock("@/api/heartbeats", () => ({ heartbeatsApi: { liveRunsForCompany: mocks.liveRuns } }));
-vi.mock("@/api/issues", () => ({ issuesApi: { list: mocks.list, listCompact: mocks.list, get: mocks.get, listLabels: async () => [], update: vi.fn() } }));
+vi.mock("@/api/issues", () => ({ issuesApi: { list: mocks.list, listCompact: mocks.list, get: mocks.get, listLabels: async () => mocks.labels, update: vi.fn() } }));
vi.mock("@/api/attention", () => ({ attentionApi: { taskIndex: mocks.attention } }));
vi.mock("@/api/agents", () => ({ agentsApi: { list: async () => [] } }));
vi.mock("@/api/projects", () => ({ projectsApi: { list: async () => [] } }));
vi.mock("@/api/access", () => ({ accessApi: { listUserDirectory: async () => ({ users: [] }) } }));
vi.mock("@/api/companies-query", () => ({ useAccountIdentity: () => mocks.identity }));
-vi.mock("@/hooks/useCoordinatorWorkspaceEnabled", () => ({ useCoordinatorWorkspaceEnabled: () => ({ userId: mocks.identity.userId ?? (mocks.localTrusted ? "local-board" : null), loaded: mocks.identity.settled }) }));
+vi.mock("@/hooks/useCoordinatorWorkspaceEnabled", () => ({ useCoordinatorWorkspaceEnabled: () => ({ enabled: true, userId: mocks.identity.userId ?? (mocks.localTrusted ? "local-board" : null), loaded: mocks.identity.settled }) }));
+vi.mock("@/hooks/useStreamlinedUiEnabled", () => ({ useStreamlinedUiEnabled: () => ({ enabled: true }) }));
+vi.mock("@/hooks/useCombinedInboxTasksEnabled", () => ({ useCombinedInboxTasksEnabled: () => ({ enabled: false }) }));
vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompanyId: "company" }) }));
vi.mock("@/context/DialogContext", () => ({ useDialogActions: () => ({ openNewIssue: vi.fn() }) }));
vi.mock("@/context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: mocks.breadcrumbs }) }));
-vi.mock("@/components/TaskListToolbar", () => ({ TaskListToolbar: ({ filters, view, onViewChange, onLayoutChange }: { filters: { onChange: (value: { liveOnly?: boolean; priorities?: string[]; assignees?: string[]; statuses?: string[]; routineTasksOnly?: boolean }) => void }; view: TaskListView; onViewChange: (view: TaskListView) => void; onLayoutChange: (layout: "list" | "board") => void }) => <>
+vi.mock("@/components/TaskListToolbar", () => ({ TaskListToolbar: ({ filters, view, onViewChange, onLayoutChange }: { filters: Omit, "onClear">; view: TaskListView; onViewChange: (view: TaskListView) => void; onLayoutChange: (layout: "list" | "board") => void }) => <>
+ {mocks.realFilters ? filters.onChange({ labels: [] })} /> : null}
{view}
{taskListViews.map(option => )}
@@ -48,6 +54,7 @@ describe("Coordinator tasks using the proposed table", () => {
const pending = createIssue({ id: "pending", companyId: "company", status: "todo", parentId: null, title: "Needs a channel answer" });
beforeEach(() => {
vi.clearAllMocks(); localStorage.clear();
+ mocks.realFilters = false; mocks.labels = [];
mocks.identity = { userId: "person", settled: true };
mocks.localTrusted = false;
mocks.liveRuns.mockResolvedValue([]);
@@ -82,6 +89,7 @@ describe("Coordinator tasks using the proposed table", () => {
expect(host.querySelector("[data-selected-view]")?.textContent).toBe(value);
});
it("uses the resolved local-board principal when no account session exists", async () => {
+ mocks.realFilters = false; mocks.labels = [];
mocks.identity = { userId: null, settled: true }; mocks.localTrusted = true;
mocks.list.mockResolvedValue([createIssue({ id: "local", companyId: "company", parentId: null, createdByUserId: "local-board" })]);
await mount("/issues");
@@ -124,9 +132,11 @@ describe("Coordinator tasks using the proposed table", () => {
it("waits for the viewer identity before resolving their saved view", async () => {
const key = taskListViewStorageKey("coordinator", "person");
localStorage.setItem(key, "all");
+ mocks.realFilters = false; mocks.labels = [];
mocks.identity = { userId: null, settled: false };
await mount("/issues");
expect(localStorage.getItem(key)).toBe("all");
+ mocks.realFilters = false; mocks.labels = [];
mocks.identity = { userId: "person", settled: true };
await mount("/issues");
expect(host.querySelector("[data-selected-view]")?.textContent).toBe("all");
@@ -282,6 +292,129 @@ describe("Coordinator tasks using the proposed table", () => {
expect(mocks.list).toHaveBeenCalledWith("company", expect.objectContaining({ q: query }), expect.anything());
expect(host.querySelector('[data-task-id="server-match"]')).not.toBeNull();
});
+ it("keeps a selected label and other URL filters after sorting and reloading", async () => {
+ mocks.realFilters = true;
+ mocks.labels = [{ id: "qa", name: "qa-task", color: "var(--accent)" }];
+ mocks.list.mockResolvedValue([
+ createIssue({ id: "qa-done", companyId: "company", parentId: null, assigneeAgentId: "chosen", status: "done", labelIds: ["qa"] }),
+ createIssue({ id: "other-done", companyId: "company", parentId: null, assigneeAgentId: "chosen", status: "done", labelIds: [] }),
+ createIssue({ id: "qa-todo", companyId: "company", parentId: null, assigneeAgentId: "chosen", status: "todo", labelIds: ["qa"] }),
+ ]);
+ await mount("/issues?view=all&status=done&assignee=chosen&workspace=team&q=release&keep=value");
+ const taskIds = () => [...host.querySelectorAll("[data-task-id]")].map(node => node.getAttribute("data-task-id"));
+ const labelCheckbox = () => host.querySelector('[role="checkbox"][aria-label="qa-task"]')!;
+ flushSync(() => labelCheckbox().click());
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-done"]));
+ expect(labelCheckbox().getAttribute("aria-checked")).toBe("true");
+ await vi.waitFor(() => expect(host.querySelector("output")!.textContent).toContain("label=qa"));
+ flushSync(() => [...host.querySelectorAll("button")].find(button => button.textContent === "Sort by task ID")!.click());
+ await vi.waitFor(() => expect(host.querySelector("output")!.textContent).toContain("sortField=taskId"));
+ const search = host.querySelector("output")!.textContent!;
+ flushSync(() => root.unmount());
+ root = createRoot(host);
+ client.clear();
+ await mount(`/issues${search}`);
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-done"]));
+ expect(labelCheckbox().getAttribute("aria-checked")).toBe("true");
+ const params = new URLSearchParams(host.querySelector("output")!.textContent!);
+ expect(params.getAll("label")).toEqual(["qa"]);
+ expect(Object.fromEntries(params)).toMatchObject({ view: "all", status: "done", assignee: "chosen", workspace: "team", q: "release", keep: "value", sortField: "taskId", sortDir: "desc" });
+ });
+ it("restores repeated labels from label-only links and follows history without clearing other filters", async () => {
+ mocks.realFilters = true;
+ mocks.labels = [{ id: "qa", name: "qa-task", color: "var(--accent)" }, { id: "review", name: "review-task", color: "var(--accent)" }];
+ mocks.list.mockResolvedValue([
+ createIssue({ id: "qa-high", companyId: "company", parentId: null, labelIds: ["qa"], priority: "high" }),
+ createIssue({ id: "review-high", companyId: "company", parentId: null, labelIds: ["review"], priority: "high" }),
+ createIssue({ id: "review-low", companyId: "company", parentId: null, labelIds: ["review"], priority: "low" }),
+ createIssue({ id: "unlabeled", companyId: "company", parentId: null, labelIds: [], priority: "high" }),
+ ]);
+ function Navigation() {
+ const navigate = useNavigate();
+ return <>
+ >;
+ }
+ flushSync(() => root.render());
+ const taskIds = () => [...host.querySelectorAll("[data-task-id]")].map(node => node.getAttribute("data-task-id"));
+ const click = (label: string) => flushSync(() => [...host.querySelectorAll("button")].find(button => button.textContent === label)!.click());
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high", "review-high", "review-low"]));
+ expect(host.querySelector("[data-selected-view]")?.textContent).toBe("all");
+ expect(host.querySelector('[role="checkbox"][aria-label="qa-task"]')?.getAttribute("aria-checked")).toBe("true");
+ expect(host.querySelector('[role="checkbox"][aria-label="review-task"]')?.getAttribute("aria-checked")).toBe("true");
+ click("High priority"); click("Review label");
+ await vi.waitFor(() => expect(taskIds()).toEqual(["review-high"]));
+ click("Back");
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high", "review-high"]));
+ click("Forward");
+ await vi.waitFor(() => expect(taskIds()).toEqual(["review-high"]));
+ flushSync(() => host.querySelector('[role="checkbox"][aria-label="qa-task"]')!.click());
+ await vi.waitFor(() => expect(new URLSearchParams(host.querySelector("output")!.textContent!).getAll("label")).toEqual(["review", "qa"]));
+ expect(taskIds()).toEqual(["qa-high", "review-high"]);
+ flushSync(() => host.querySelector('[role="checkbox"][aria-label="qa-task"]')!.click());
+ await vi.waitFor(() => expect(new URLSearchParams(host.querySelector("output")!.textContent!).getAll("label")).toEqual(["review"]));
+ flushSync(() => host.querySelector('[role="checkbox"][aria-label="review-task"]')!.click());
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high", "review-high", "unlabeled"]));
+ await vi.waitFor(() => expect(new URLSearchParams(host.querySelector("output")!.textContent!).getAll("label")).toEqual([]));
+ const params = new URLSearchParams(host.querySelector("output")!.textContent!);
+ expect(params.getAll("label")).toEqual([]);
+ expect(params.get("keep")).toBe("value");
+ });
+ it("keeps unknown labels restrictive and removes them when clearing an empty collection", async () => {
+ mocks.list.mockResolvedValue([createIssue({ id: "unlabeled", companyId: "company", parentId: null, labelIds: [] })]);
+ await mount("/issues?view=all&label=missing&label=other-missing&keep=value&sortField=taskId&sortDir=asc");
+ expect(host.querySelectorAll("[data-task-id]")).toHaveLength(0);
+ flushSync(() => [...host.querySelectorAll("button")].find(button => button.textContent === "Clear filters")!.click());
+ await vi.waitFor(() => expect(host.querySelector('[data-task-id="unlabeled"]')).not.toBeNull());
+ await vi.waitFor(() => expect(new URLSearchParams(host.querySelector("output")!.textContent!).getAll("label")).toEqual([]));
+ const params = new URLSearchParams(host.querySelector("output")!.textContent!);
+ expect(params.getAll("label")).toEqual([]);
+ expect(Object.fromEntries(params)).toMatchObject({ view: "all", keep: "value", sortField: "taskId", sortDir: "asc" });
+ });
+ it.each(["mine", "blocked", "recent", "everything"])("keeps Coordinator and local filters/layout when the last label is removed from a legacy %s link", async view => {
+ mocks.realFilters = true;
+ mocks.labels = [{ id: "qa", name: "qa-task", color: "var(--accent)" }];
+ mocks.list.mockResolvedValue([
+ createIssue({ id: "qa-high", companyId: "company", parentId: null, labelIds: ["qa"], priority: "high" }),
+ createIssue({ id: "qa-low", companyId: "company", parentId: null, labelIds: ["qa"], priority: "low" }),
+ createIssue({ id: "unlabeled-high", companyId: "company", parentId: null, labelIds: [], priority: "high" }),
+ ]);
+ function CurrentRoute() {
+ const location = useLocation(); const navigate = useNavigate();
+ return <>
+
+ >;
+ }
+ flushSync(() => root.render(
+ } />Inbox owner
} />
+ ));
+ const taskIds = () => [...host.querySelectorAll("[data-task-id]")].map(node => node.getAttribute("data-task-id"));
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high", "qa-low"]));
+ flushSync(() => [...host.querySelectorAll("button")].find(button => button.textContent === "High priority")!.click());
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high"]));
+ flushSync(() => [...host.querySelectorAll("button")].find(button => button.textContent === "Board view")!.click());
+ expect(host.querySelector(".design-task-list-panel")?.hidden).toBe(true);
+ flushSync(() => host.querySelector('[role="checkbox"][aria-label="qa-task"]')!.click());
+ await vi.waitFor(() => expect(host.querySelector("[data-route]")?.textContent).toBe("/issues?view=all&keep=value#saved"));
+ expect(host.querySelector(".design-task-list-panel")?.hidden).toBe(true);
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high", "unlabeled-high"]));
+ expect(host.textContent).not.toContain("Inbox owner");
+ await vi.waitFor(() => expect(new URLSearchParams(host.querySelector(".design-task-list-panel output")!.textContent!).getAll("label")).toEqual([]));
+ expect(Object.fromEntries(new URLSearchParams(host.querySelector(".design-task-list-panel output")!.textContent!))).toMatchObject({ view: "all", keep: "value" });
+ flushSync(() => host.querySelector('[role="checkbox"][aria-label="qa-task"]')!.click());
+ await vi.waitFor(() => expect(host.querySelector("[data-route]")?.textContent).toBe("/issues?view=all&keep=value&label=qa#saved"));
+ expect(taskIds()).toEqual(["qa-high"]);
+ flushSync(() => host.querySelector('[role="checkbox"][aria-label="qa-task"]')!.click());
+ await vi.waitFor(() => expect(host.querySelector("[data-route]")?.textContent).toBe("/issues?view=all&keep=value#saved"));
+ const click = (label: string) => flushSync(() => [...host.querySelectorAll("button")].find(button => button.textContent === label)!.click());
+ click("Restore label link");
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high"]));
+ click("Back");
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high", "unlabeled-high"]));
+ click("Forward");
+ await vi.waitFor(() => expect(taskIds()).toEqual(["qa-high"]));
+ expect(host.querySelector(".design-task-list-panel")?.hidden).toBe(true);
+ expect(host.querySelector("[data-route]")?.textContent).toBe("/issues?view=all&label=qa&keep=value#saved");
+ });
it("still applies assignee filters to server search matches", async () => {
mocks.list.mockResolvedValue([
createIssue({ id: "chosen", companyId: "company", parentId: null, assigneeAgentId: "chosen-agent" }),
diff --git a/ui/src/pages/CoordinatorTasks.tsx b/ui/src/pages/CoordinatorTasks.tsx
index 697efd5e9ab..98c401bea66 100644
--- a/ui/src/pages/CoordinatorTasks.tsx
+++ b/ui/src/pages/CoordinatorTasks.tsx
@@ -29,7 +29,7 @@ import { coordinatorAttentionTasksQueryKey } from "@/lib/coordinator-attention";
import { createIssueDetailLocationState } from "@/lib/issueDetailBreadcrumb";
import { defaultIssueFilterState, type IssueFilterState } from "@/lib/issue-filters";
import { queryKeys } from "@/lib/queryKeys";
-import { useLocation, useSearchParams } from "@/lib/router";
+import { useLocation, useNavigate, useSearchParams } from "@/lib/router";
import { loadTaskListView, saveTaskListView, taskListViews, taskListViewStorageKey, type TaskListView } from "@/lib/task-list-views";
import { normalizeTaskViewKey, ORGANIZATION_SCOPED_PARAMS, taskView } from "@/lib/task-views";
import type { Issue } from "@paperclipai/shared";
@@ -46,16 +46,18 @@ export function CoordinatorTasks() {
const client = useQueryClient();
const listRef = useRef(null);
const location = useLocation();
+ const navigate = useNavigate();
const [params, setParams] = useSearchParams();
const [layout, setLayout] = useState<"list" | "board">("list");
const urlAssignee = JSON.stringify(params.getAll("assignee"));
const urlStatuses = JSON.stringify(params.getAll("status"));
+ const urlLabels = JSON.stringify(params.getAll("label").filter(Boolean));
const urlView = params.get("view");
const viewStorageKey = taskListViewStorageKey("coordinator", userId ?? "local-board");
const [preference, setPreference] = useState(() => ({ key: viewStorageKey, view: loadTaskListView(viewStorageKey, undefined, "inbox") }));
const savedView = preference.key === viewStorageKey ? preference.view : loadTaskListView(viewStorageKey, undefined, "inbox");
const explicitView = urlView === "needs_attention" ? "blocked" : taskListViews.find(option => option.value === urlView)?.value;
- const organizationScoped = [...ORGANIZATION_SCOPED_PARAMS, "workspaceId", "status", "sortField", "sortDir"].some(param => params.getAll(param).some(Boolean));
+ const organizationScoped = [...ORGANIZATION_SCOPED_PARAMS, "workspaceId", "status", "label", "sortField", "sortDir"].some(param => params.getAll(param).some(Boolean));
const view: TaskListView = explicitView ?? (organizationScoped || normalizeTaskViewKey(urlView) ? "all" : savedView);
useEffect(() => {
if (!identitySettled) return;
@@ -89,7 +91,7 @@ export function CoordinatorTasks() {
const normalizedView = normalizeTaskViewKey(urlView);
const presetStatuses = normalizedView && taskView(normalizedView).surface === "issues" ? taskView(normalizedView).statuses ?? [] : [];
const initialStatuses: string[] = params.has("status") ? params.getAll("status") : presetStatuses;
- const previousUrlFilters = useRef({ assignee: urlAssignee, statuses: urlStatuses, view: urlView, routineTasksOnly });
+ const previousUrlFilters = useRef({ assignee: urlAssignee, statuses: urlStatuses, labels: urlLabels, view: urlView, routineTasksOnly });
const needsAttention = view === "blocked";
const query = params.get("q") ?? "";
const deferredQuery = useDeferredValue(query.trim());
@@ -196,48 +198,49 @@ export function CoordinatorTasks() {
searchMode: "server",
liveIssueIds: collectLiveIssueIds(liveRuns.data, records),
viewStorageKey,
- initialFilters: { assignees: params.getAll("assignee"), statuses: initialStatuses, routineTasksOnly },
+ initialFilters: { labels: params.getAll("label").filter(Boolean), assignees: params.getAll("assignee"), statuses: initialStatuses, routineTasksOnly },
});
useEffect(() => {
const previous = previousUrlFilters.current;
- if (previous.assignee === urlAssignee && previous.view === urlView && previous.statuses === urlStatuses && previous.routineTasksOnly === routineTasksOnly) return;
- previousUrlFilters.current = { assignee: urlAssignee, statuses: urlStatuses, view: urlView, routineTasksOnly };
+ if (previous.assignee === urlAssignee && previous.view === urlView && previous.statuses === urlStatuses && previous.labels === urlLabels && previous.routineTasksOnly === routineTasksOnly) return;
+ previousUrlFilters.current = { assignee: urlAssignee, statuses: urlStatuses, labels: urlLabels, view: urlView, routineTasksOnly };
collection.updateFilters({
+ ...(previous.labels !== urlLabels ? { labels: params.getAll("label").filter(Boolean) } : {}),
...(previous.routineTasksOnly !== routineTasksOnly ? { routineTasksOnly } : {}),
...(previous.assignee !== urlAssignee ? { assignees: params.getAll("assignee") } : {}),
...(previous.view !== urlView || previous.statuses !== urlStatuses ? { statuses: initialStatuses } : {}),
});
- }, [urlAssignee, urlView, urlStatuses, initialStatuses, routineTasksOnly, collection.updateFilters]);
+ }, [urlAssignee, urlView, urlStatuses, urlLabels, initialStatuses, routineTasksOnly, collection.updateFilters]);
const updateFilters = (patch: Partial) => {
collection.updateFilters(patch);
- if (patch.assignees === undefined && patch.statuses === undefined && patch.routineTasksOnly === undefined) return;
- setParams(current => {
- const next = new URLSearchParams(current);
- if (patch.routineTasksOnly !== undefined) {
- if (patch.routineTasksOnly) next.set("originKind", "routine_execution");
- else next.delete("originKind");
- }
- if (patch.assignees !== undefined) {
- next.delete("assignee");
- for (const id of patch.assignees) next.append("assignee", id);
- }
- if (patch.statuses !== undefined) {
- next.delete("status");
- for (const status of patch.statuses) next.append("status", status);
- const preset = normalizeTaskViewKey(next.get("view"));
- if (preset && taskView(preset).surface === "issues") next.set("view", "all");
- }
- return next;
- }, { replace: true });
+ if (patch.assignees === undefined && patch.statuses === undefined && patch.labels === undefined && patch.routineTasksOnly === undefined) return;
+ const next = new URLSearchParams(params);
+ if (patch.routineTasksOnly !== undefined) {
+ if (patch.routineTasksOnly) next.set("originKind", "routine_execution");
+ else next.delete("originKind");
+ }
+ if (patch.labels !== undefined) {
+ next.delete("label");
+ for (const id of patch.labels) next.append("label", id);
+ }
+ if (patch.assignees !== undefined) {
+ next.delete("assignee");
+ for (const id of patch.assignees) next.append("assignee", id);
+ }
+ if (patch.statuses !== undefined) {
+ next.delete("status");
+ for (const status of patch.statuses) next.append("status", status);
+ const preset = normalizeTaskViewKey(next.get("view"));
+ if (preset && taskView(preset).surface === "issues") next.set("view", "all");
+ }
+ navigate({ search: next.toString(), hash: location.hash }, { replace: true });
};
const clearFilters = () => {
collection.updateFilters(defaultIssueFilterState);
- setParams(current => {
- const next = new URLSearchParams(current);
- for (const key of ["q", "assignee", "status", "originKind"]) next.delete(key);
- if (next.get("view") !== "needs_attention") next.set("view", "all");
- return next;
- }, { replace: true });
+ const next = new URLSearchParams(params);
+ for (const key of ["q", "assignee", "status", "label", "originKind"]) next.delete(key);
+ if (next.get("view") !== "needs_attention") next.set("view", "all");
+ navigate({ search: next.toString(), hash: location.hash }, { replace: true });
};
const update = useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => issuesApi.update(id, data),
onSuccess: () => { void client.invalidateQueries({ queryKey: queryKeys.issues.list(companyId!) }); void client.invalidateQueries({ queryKey: queryKeys.attention(companyId!) }); },
diff --git a/ui/src/pages/Issues.coordinator-routing.test.tsx b/ui/src/pages/Issues.coordinator-routing.test.tsx
index a8e50b54e4b..db3c661c140 100644
--- a/ui/src/pages/Issues.coordinator-routing.test.tsx
+++ b/ui/src/pages/Issues.coordinator-routing.test.tsx
@@ -40,3 +40,13 @@ it.each(["workspace=workspace", "originKind=routine_execution"])("keeps organiza
try { expect(host.textContent).toContain("Coordinator tasks"); expect(host.textContent).not.toContain("Inbox owner"); }
finally { cleanup(); }
});
+
+it.each(["mine", "blocked", "recent", "everything"])("keeps shared %s label links on the Coordinator collection", async view => {
+ const url = `/ORG/issues?view=${view}&label=qa&label=review&sortField=taskId&sortDir=asc&keep=value#saved`;
+ const { host, cleanup } = render(url);
+ try {
+ await vi.waitFor(() => expect(host.textContent).toContain("Coordinator tasks"));
+ expect(host.textContent).not.toContain("Inbox owner");
+ expect(host.querySelector("output")?.textContent).toBe(url.replace(`view=${view}`, "view=all"));
+ } finally { cleanup(); }
+});
diff --git a/ui/src/pages/Issues.tsx b/ui/src/pages/Issues.tsx
index 4109a1cfb84..f99a97d1c46 100644
--- a/ui/src/pages/Issues.tsx
+++ b/ui/src/pages/Issues.tsx
@@ -105,7 +105,12 @@ export function Issues() {
const [params] = useSearchParams();
const location = useLocation();
const requestedView = normalizeTaskViewKey(params.get(TASK_VIEW_PARAM));
- const organizationScoped = [...ORGANIZATION_SCOPED_PARAMS, "workspaceId"].some(param => params.getAll(param).some(Boolean));
+ const organizationScoped = [...ORGANIZATION_SCOPED_PARAMS, "workspaceId", "label"].some(param => params.getAll(param).some(Boolean));
+ if (!designSandbox && coordinatorEnabled && params.getAll("label").some(Boolean) && requestedView && requestedView !== "unread" && taskView(requestedView).surface === "inbox") {
+ const next = new URLSearchParams(params);
+ next.set(TASK_VIEW_PARAM, "all");
+ return ;
+ }
if (!designSandbox && coordinatorEnabled && !organizationScoped && requestedView && requestedView !== "unread" && taskView(requestedView).surface === "inbox") {
const next = new URLSearchParams(params); next.delete(TASK_VIEW_PARAM);
const search = next.toString();