mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 22:24:16 +01:00
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:
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
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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) */}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in new issue
Block a user