From 30c8aa1cc1c67900fc3fde354ed743ea437ba9d4 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sat, 10 Oct 2026 01:13:35 -0700 Subject: [PATCH] fix(ui): remove workspace from task and inbox filters (#15788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updated 14 checked lines erclip is the open source app people use to manage AI agents for work > - The board shows tasks in a list and in the inbox > - Each of those screens has a filter menu > - The filter menu includes a Workspace section > - The list already shows the workspace name, and the user can group by workspace > - This pull request removes Workspace from those filter menus > - The benefit is a shorter filter menu. The workspace label and the group option stay ## Linked Issues or Issue Description No public GitHub issue exists for this change. This description follows the enhancement template. Related pull request: Refs #15403. That pull request also edits the task list. It does not remove the Workspace filter. **What existing behavior does this improve?** The filter menu on the task list and the inbox. **Subsystem affected** ui/ — React + Vite board UI **Current behavior** The filter menu shows a Workspace section when isolated workspaces are on. The user can select one or more workspaces there. The task list also shows the workspace name as a label. The user can group the list by workspace. A workspace value in the page address also limits the list. **Proposed behavior** The filter menu does not show Workspace. The workspace name stays in the list. Group by workspace stays. A workspace value in the page address still limits the task list. A click on a workspace name in the list still limits the list to that workspace. **Reason and benefit** The filter menu repeats a choice that the list and the group control already give. Removal of that section makes the filter menu shorter. Saved links that include a workspace value still open the same limited list. **Breaking changes** The filter menu no longer lets the user pick a workspace. A saved workspace query still works. Decision filters are not part of this change. ## What Changed - Remove the Workspace section from the task list filter menu - Remove the Workspace section from the inbox filter menu - Keep workspace labels, group by workspace, and workspace query scope - Update the filter menu test so it checks that Workspace is absent - Drop a saved workspace selection when preferences load, and keep the other saved filters ## Verification - Run `corepack pnpm exec vitest run --config vitest.config.ts src/components/IssueFiltersPopover.test.tsx src/components/IssuesList.test.tsx src/pages/Inbox.test.tsx src/lib/issue-filters.test.ts src/components/IssueColumns.test.tsx` from `ui/` - Result: 5 files passed, 111 tests passed - Open the task list filter menu. Confirm that Workspace is not a section - Open the inbox filter menu. Confirm that Workspace is not a section - Open a task list with a workspace query. Confirm that the list shows only tasks for that workspace - Group the task list by workspace. Confirm that the groups still appear - A browser check of the live screens was not run in this workspace ## Risks - Low risk. The change removes a menu section. It does not change filter matching - A user who used the menu to pick a workspace must use the workspace label or the page address - Pull request #15403 also edits the task list. Merge order can cause a small conflict in `ui/src/components/IssuesList.tsx` ROADMAP.md does not plan this filter removal. This change does not add a core feature. ## Model Used - Provider: xAI - Model: Grok 4.7 - Capabilities: tool use, code edits, and test execution ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com> Co-authored-by: Paperclip --- .../components/IssueFiltersPopover.test.tsx | 4 +- ui/src/components/IssueFiltersPopover.tsx | 35 +------------ ui/src/components/IssuesList.test.tsx | 50 +++++++++++++++++++ ui/src/components/IssuesList.tsx | 14 ++---- ui/src/components/LegacyIssuesList.tsx | 12 +---- ui/src/lib/inbox.test.ts | 4 +- ui/src/lib/inbox.ts | 10 +++- ui/src/pages/Inbox.tsx | 10 ++-- ui/src/pages/LegacyInbox.tsx | 2 - .../stories/external-objects.stories.tsx | 2 - .../stories/issue-management.stories.tsx | 1 - 11 files changed, 74 insertions(+), 70 deletions(-) diff --git a/ui/src/components/IssueFiltersPopover.test.tsx b/ui/src/components/IssueFiltersPopover.test.tsx index 69eb612fee5..796735a8eb6 100644 --- a/ui/src/components/IssueFiltersPopover.test.tsx +++ b/ui/src/components/IssueFiltersPopover.test.tsx @@ -65,7 +65,6 @@ describe("IssueFiltersPopover", () => { agents={[{ id: "agent-1", name: "Agent One" }]} projects={[{ id: "project-1", name: "Project One" }]} labels={[{ id: "label-1", name: "Bug", color: "#ff0000" }]} - workspaces={[{ id: "workspace-1", name: "Workspace One" }]} enableRoutineVisibilityFilter />, ); @@ -73,6 +72,8 @@ describe("IssueFiltersPopover", () => { const popoverContent = container.querySelector("[data-testid='popover-content']"); expect(popoverContent).not.toBeNull(); + expect(popoverContent?.querySelector('[data-filter-options="workspaces"]')).toBeNull(); + expect(popoverContent?.textContent).not.toContain("Workspace"); expect(popoverContent?.className).toContain("overflow-y-auto"); expect(popoverContent?.className).toContain("max-h-(--sz-calc-9)"); expect(popoverContent?.querySelectorAll(".overflow-y-auto").length).toBe(0); @@ -97,7 +98,6 @@ describe("IssueFiltersPopover", () => { agents={[{ id: "agent-1", name: "Agent One" }]} projects={[{ id: "project-1", name: "Project One" }]} labels={[{ id: "label-1", name: "Bug", color: "#ff0000" }]} - workspaces={[{ id: "workspace-1", name: "Workspace One" }]} enableRoutineVisibilityFilter />, ); diff --git a/ui/src/components/IssueFiltersPopover.tsx b/ui/src/components/IssueFiltersPopover.tsx index 05f717493e2..5546054e1f9 100644 --- a/ui/src/components/IssueFiltersPopover.tsx +++ b/ui/src/components/IssueFiltersPopover.tsx @@ -4,7 +4,7 @@ import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Bot, Check, Filter, HardDrive, Search, User, X } from "lucide-react"; +import { Bot, Check, Filter, Search, User, X } from "lucide-react"; import { PriorityIcon } from "./PriorityIcon"; import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags"; import { StatusIcon } from "./StatusIcon"; @@ -43,11 +43,6 @@ type LabelOption = { color: string; }; -type WorkspaceOption = { - id: string; - name: string; -}; - type CreatorOption = { id: string; label: string; @@ -116,7 +111,6 @@ export function IssueFiltersPopover({ enableRoutineVisibilityFilter = false, buttonVariant = "ghost", iconOnly = false, - workspaces, creators, presentation = "legacy", inboxScopeFilters, @@ -132,7 +126,6 @@ export function IssueFiltersPopover({ enableRoutineVisibilityFilter?: boolean; buttonVariant?: "ghost" | "outline"; iconOnly?: boolean; - workspaces?: WorkspaceOption[]; creators?: CreatorOption[]; presentation?: "legacy" | "streamlined"; inboxScopeFilters?: InboxScopeFilters; @@ -142,7 +135,6 @@ export function IssueFiltersPopover({ const [assigneeSearch, setAssigneeSearch] = useState(""); const [projectSearch, setProjectSearch] = useState(""); const [labelSearch, setLabelSearch] = useState(""); - const [workspaceSearch, setWorkspaceSearch] = useState(""); const visibleAgents = useMemo( () => searchIssueFilterOptions(agents, assigneeSearch, (option) => option.name), [agents, assigneeSearch], @@ -155,10 +147,6 @@ export function IssueFiltersPopover({ () => searchIssueFilterOptions(labels, labelSearch, (option) => option.name), [labels, labelSearch], ); - const visibleWorkspaces = useMemo( - () => searchIssueFilterOptions(workspaces, workspaceSearch, (option) => option.name), - [workspaces, workspaceSearch], - ); const creatorOptions = creators ?? []; const creatorOptionById = useMemo( () => new Map(creatorOptions.map((option) => [option.id, option])), @@ -490,27 +478,6 @@ export function IssueFiltersPopover({ ) : null} - {workspaces && workspaces.length > 0 ? ( -
- Workspace - {streamlined && workspaces.length > SEARCHABLE_FILTER_THRESHOLD ? ( - - ) : null} -
- {(streamlined ? visibleWorkspaces : workspaces).map((workspace) => ( - - ))} -
-
- ) : null} - {enableExternalObjectFilters ? (
External object status diff --git a/ui/src/components/IssuesList.test.tsx b/ui/src/components/IssuesList.test.tsx index 72cdcf23ecd..f50a3332c04 100644 --- a/ui/src/components/IssuesList.test.tsx +++ b/ui/src/components/IssuesList.test.tsx @@ -1928,6 +1928,56 @@ describe("IssuesList", () => { }); }); + it("ignores a saved workspace filter when the page address has no workspace query", async () => { + localStorage.setItem( + taskCollectionPreferencesStorageKey({ + companyId: "company-1", + collectionKey: "paperclip:test-issues", + }), + JSON.stringify({ + version: 1, + companyId: "company-1", + collectionKey: "paperclip:test-issues", + viewState: { + workspaces: ["workspace-alpha"], + }, + columns: ["id"], + }), + ); + const alphaIssue = createIssue({ + id: "issue-alpha", + identifier: "PAP-40", + title: "Alpha issue", + executionWorkspaceId: "workspace-alpha", + }); + const betaIssue = createIssue({ + id: "issue-beta", + identifier: "PAP-41", + title: "Beta issue", + executionWorkspaceId: "workspace-beta", + }); + + const { root } = renderWithQueryClient( + undefined} + />, + container, + ); + + await waitForAssertion(() => { + expect(container.textContent).toContain("Alpha issue"); + expect(container.textContent).toContain("Beta issue"); + }); + + act(() => { + root.unmount(); + }); + }); + it("applies an initial workspace filter from the issues URL state", async () => { const alphaIssue = createIssue({ id: "issue-alpha", diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index da467d40fda..4603c567b9f 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -207,6 +207,9 @@ function normalizeIssueViewState(value: unknown): IssueViewState { ...defaultViewState, ...parsed, ...normalizeIssueFilterState(parsed), + // A saved workspace selection has no filter control. Drop it on load. + // `initialWorkspaces` from the page address is applied after this. + workspaces: [], sortField: ["status", "priority", "title", "created", "updated", "workflow"].includes(parsed.sortField ?? "") ? parsed.sortField as IssueSortField : defaultViewState.sortField, @@ -1043,16 +1046,6 @@ function StreamlinedIssuesList({ return map; }, [defaultProjectWorkspaceIds, executionWorkspaceById, projectWorkspaceById]); - const workspaceOptions = useMemo(() => { - const options = new Map(); - for (const [workspaceId, workspaceName] of workspaceNameMap) { - options.set(workspaceId, workspaceName); - } - return [...options.entries()] - .sort((a, b) => a[1].localeCompare(b[1])) - .map(([id, name]) => ({ id, name })); - }, [workspaceNameMap]); - const creatorOptions = useMemo(() => { const options = new Map(); const knownAgentIds = new Set(); @@ -1920,7 +1913,6 @@ function StreamlinedIssuesList({ enableExternalObjectFilters={externalObjectsEnabled} enableRoutineVisibilityFilter={enableRoutineVisibilityFilter} iconOnly - workspaces={isolatedWorkspacesEnabled ? workspaceOptions : undefined} presentation={rowPresentation === "task" ? "streamlined" : "legacy"} /> diff --git a/ui/src/components/LegacyIssuesList.tsx b/ui/src/components/LegacyIssuesList.tsx index 55cfbb48f6d..5cd5db5358a 100644 --- a/ui/src/components/LegacyIssuesList.tsx +++ b/ui/src/components/LegacyIssuesList.tsx @@ -198,6 +198,7 @@ function getViewState(key: string): IssueViewState { ...defaultViewState, ...parsed, ...normalizeIssueFilterState(parsed), + workspaces: [], boardCardDensity: normalizeBoardCardDensity(parsed.boardCardDensity), boardColdLaneMode: normalizeBoardColdLaneMode(parsed.boardColdLaneMode), boardColumnPageSize: normalizeBoardColumnPageSize(parsed.boardColumnPageSize), @@ -965,16 +966,6 @@ export function IssuesList({ return map; }, [defaultProjectWorkspaceIds, executionWorkspaceById, projectWorkspaceById]); - const workspaceOptions = useMemo(() => { - const options = new Map(); - for (const [workspaceId, workspaceName] of workspaceNameMap) { - options.set(workspaceId, workspaceName); - } - return [...options.entries()] - .sort((a, b) => a[1].localeCompare(b[1])) - .map(([id, name]) => ({ id, name })); - }, [workspaceNameMap]); - const creatorOptions = useMemo(() => { const options = new Map(); const knownAgentIds = new Set(); @@ -1814,7 +1805,6 @@ export function IssuesList({ enableExternalObjectFilters={externalObjectsEnabled} enableRoutineVisibilityFilter={enableRoutineVisibilityFilter} iconOnly - workspaces={isolatedWorkspacesEnabled ? workspaceOptions : undefined} /> {/* Sort (list view only) */} diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 36b3baf8067..79d0afbf397 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -1159,7 +1159,7 @@ describe("inbox helpers", () => { creators: ["user:user-1"], labels: ["label-1"], projects: ["project-1"], - workspaces: ["workspace-1"], + workspaces: [], liveOnly: true, externalObjectStatuses: [], hideRoutineExecutions: false, @@ -1210,7 +1210,7 @@ describe("inbox helpers", () => { creators: ["user:user-1"], labels: [], projects: ["project-1"], - workspaces: ["workspace-1"], + workspaces: [], liveOnly: false, externalObjectStatuses: [], hideRoutineExecutions: false, diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index c95b015352c..368cc213a48 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -217,7 +217,10 @@ export function loadInboxFilterPreferences( return { allCategoryFilter: normalizeInboxCategoryFilter(parsed.allCategoryFilter), allApprovalFilter: normalizeInboxApprovalFilter(parsed.allApprovalFilter), - issueFilters: normalizeIssueFilterState(parsed.issueFilters), + issueFilters: { + ...normalizeIssueFilterState(parsed.issueFilters), + workspaces: [], + }, }; } catch { return { @@ -240,7 +243,10 @@ export function saveInboxFilterPreferences( JSON.stringify({ allCategoryFilter: normalizeInboxCategoryFilter(preferences.allCategoryFilter), allApprovalFilter: normalizeInboxApprovalFilter(preferences.allApprovalFilter), - issueFilters: normalizeIssueFilterState(preferences.issueFilters), + issueFilters: { + ...normalizeIssueFilterState(preferences.issueFilters), + workspaces: [], + }, }), ); } catch { diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index 37944e25d83..88ae042b349 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -221,7 +221,13 @@ function normalizeInboxCollectionViewState(value: unknown): StreamlinedInboxView ? category : "everything", allApprovalFilter: approval === "actionable" || approval === "resolved" ? approval : "all", - issueFilters: normalizeIssueFilterState(candidate.issueFilters), + issueFilters: { + ...normalizeIssueFilterState(candidate.issueFilters), + // The filter menu no longer offers Workspace. Drop a saved selection so + // it cannot keep hiding tasks. A task-link workspace query is applied + // by the task list after this load. + workspaces: [], + }, showDateGroupSeparators: candidate.showDateGroupSeparators !== false, }; } @@ -2482,7 +2488,6 @@ function StreamlinedInbox({ enableRoutineVisibilityFilter buttonVariant="outline" iconOnly - workspaces={isolatedWorkspacesEnabled ? executionWorkspaces.filter((w) => w.mode === "isolated_workspace").map((w) => ({ id: w.id, name: w.name })) : undefined} presentation={streamlinedUiEnabled ? "streamlined" : "legacy"} /> @@ -2588,7 +2593,6 @@ function StreamlinedInbox({ enableRoutineVisibilityFilter buttonVariant="outline" iconOnly - workspaces={isolatedWorkspacesEnabled ? executionWorkspaces.filter((w) => w.mode === "isolated_workspace").map((w) => ({ id: w.id, name: w.name })) : undefined} presentation={streamlinedUiEnabled ? "streamlined" : "legacy"} inboxScopeFilters={tab === "all" ? { category: allCategoryFilter, diff --git a/ui/src/pages/LegacyInbox.tsx b/ui/src/pages/LegacyInbox.tsx index 35196635373..21d1a885e15 100644 --- a/ui/src/pages/LegacyInbox.tsx +++ b/ui/src/pages/LegacyInbox.tsx @@ -2353,7 +2353,6 @@ export function Inbox() { enableRoutineVisibilityFilter buttonVariant="outline" iconOnly - workspaces={isolatedWorkspacesEnabled ? executionWorkspaces.filter((w) => w.mode === "isolated_workspace").map((w) => ({ id: w.id, name: w.name })) : undefined} /> @@ -2452,7 +2451,6 @@ export function Inbox() { enableRoutineVisibilityFilter buttonVariant="outline" iconOnly - workspaces={isolatedWorkspacesEnabled ? executionWorkspaces.filter((w) => w.mode === "isolated_workspace").map((w) => ({ id: w.id, name: w.name })) : undefined} /> diff --git a/ui/storybook/stories/external-objects.stories.tsx b/ui/storybook/stories/external-objects.stories.tsx index 00108d96e13..e0e91816dae 100644 --- a/ui/storybook/stories/external-objects.stories.tsx +++ b/ui/storybook/stories/external-objects.stories.tsx @@ -32,7 +32,6 @@ import { import type { IssueExternalObjectGroup } from "@/hooks/useIssueExternalObjects"; import { storybookAgents, - storybookExecutionWorkspaces, storybookIssueLabels, storybookIssues, storybookProjects, @@ -650,7 +649,6 @@ function FilterPopoverWithExternalChecked() { currentUserId="user-board" enableRoutineVisibilityFilter buttonVariant="outline" - workspaces={storybookExecutionWorkspaces.map((workspace) => ({ id: workspace.id, name: workspace.name }))} creators={[ { id: "user:user-board", label: "Riley Board", kind: "user", searchText: "board user human" }, ]} diff --git a/ui/storybook/stories/issue-management.stories.tsx b/ui/storybook/stories/issue-management.stories.tsx index 1939c5b3bfd..e1774d82d92 100644 --- a/ui/storybook/stories/issue-management.stories.tsx +++ b/ui/storybook/stories/issue-management.stories.tsx @@ -596,7 +596,6 @@ function OpenFiltersPopover() { currentUserId="user-board" enableRoutineVisibilityFilter buttonVariant="outline" - workspaces={storybookExecutionWorkspaces.map((workspace) => ({ id: workspace.id, name: workspace.name }))} creators={[ { id: "user:user-board", label: "Riley Board", kind: "user", searchText: "board user human" }, ...storybookAgents.map((agent) => ({