fix(ui): remove workspace from task and inbox filters (#15788)

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 <noreply@paperclip.ing>
This commit is contained in:
authored and GitHub committed 2026-10-10 01:13:35 -07:00
1 parent de042e79ef
commit 30c8aa1cc1
11 files changed
+74 -70

No files matched your search

@@ -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
/>,
);
+1 -34
View File
@@ -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({
</div>
) : null}
{workspaces && workspaces.length > 0 ? (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Workspace</span>
{streamlined && workspaces.length > SEARCHABLE_FILTER_THRESHOLD ? (
<FilterOptionSearch value={workspaceSearch} onChange={setWorkspaceSearch} label="Workspaces" />
) : null}
<div data-filter-options="workspaces" className={streamlined ? "space-y-0.5" : "max-h-32 space-y-0.5 overflow-y-auto"}>
{(streamlined ? visibleWorkspaces : workspaces).map((workspace) => (
<label key={workspace.id} className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent/50">
<Checkbox
checked={state.workspaces.includes(workspace.id)}
onCheckedChange={() => onChange({ workspaces: toggleIssueFilterValue(state.workspaces, workspace.id) })}
/>
<HardDrive className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-sm">{workspace.name}</span>
</label>
))}
</div>
</div>
) : null}
{enableExternalObjectFilters ? (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">External object status</span>
+50
View File
@@ -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(
<IssuesList
issues={[alphaIssue, betaIssue]}
agents={[]}
projects={[]}
viewStateKey="paperclip:test-issues"
onUpdateIssue={() => 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",
+3 -11
View File
@@ -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<string, string>();
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<CreatorOption[]>(() => {
const options = new Map<string, CreatorOption>();
const knownAgentIds = new Set<string>();
@@ -1920,7 +1913,6 @@ function StreamlinedIssuesList({
enableExternalObjectFilters={externalObjectsEnabled}
enableRoutineVisibilityFilter={enableRoutineVisibilityFilter}
iconOnly
workspaces={isolatedWorkspacesEnabled ? workspaceOptions : undefined}
presentation={rowPresentation === "task" ? "streamlined" : "legacy"}
/>
+1 -11
View File
@@ -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<string, string>();
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<CreatorOption[]>(() => {
const options = new Map<string, CreatorOption>();
const knownAgentIds = new Set<string>();
@@ -1814,7 +1805,6 @@ export function IssuesList({
enableExternalObjectFilters={externalObjectsEnabled}
enableRoutineVisibilityFilter={enableRoutineVisibilityFilter}
iconOnly
workspaces={isolatedWorkspacesEnabled ? workspaceOptions : undefined}
/>
{/* Sort (list view only) */}
+2 -2
View File
@@ -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,
+8 -2
View File
@@ -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 {
+7 -3
View File
@@ -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"}
/>
<Popover>
@@ -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,
-2
View File
@@ -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}
/>
<Popover>
<PopoverTrigger asChild>
@@ -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}
/>
<Popover>
<PopoverTrigger asChild>
@@ -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" },
]}
@@ -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) => ({