mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
Add live run issue filter
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2d57b8dafa
commit
fbbf2e9c41
13 files changed
+266
-50
No files matched your search
@@ -79,5 +79,6 @@ describe("IssueFiltersPopover", () => {
|
||||
element.className.includes("md:grid-cols-3"),
|
||||
);
|
||||
expect(layoutGrid?.className).toContain("grid-cols-1");
|
||||
expect(popoverContent?.textContent).toContain("Live runs only");
|
||||
});
|
||||
});
|
||||
@@ -344,9 +344,16 @@ export function IssueFiltersPopover({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{enableRoutineVisibilityFilter ? (
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">Visibility</span>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground">Visibility</span>
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent/50">
|
||||
<Checkbox
|
||||
checked={state.liveOnly}
|
||||
onCheckedChange={(checked) => onChange({ liveOnly: checked === true })}
|
||||
/>
|
||||
<span className="text-sm">Live runs only</span>
|
||||
</label>
|
||||
{enableRoutineVisibilityFilter ? (
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent/50">
|
||||
<Checkbox
|
||||
checked={state.hideRoutineExecutions}
|
||||
@@ -354,8 +361,8 @@ export function IssueFiltersPopover({
|
||||
/>
|
||||
<span className="text-sm">Hide routine runs</span>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
issuePriorityOrder,
|
||||
normalizeIssueFilterState,
|
||||
resolveIssueFilterWorkspaceId,
|
||||
shouldIncludeIssueFilterWorkspaceOption,
|
||||
issueStatusOrder,
|
||||
type IssueFilterState,
|
||||
} from "../lib/issue-filters";
|
||||
@@ -483,6 +484,10 @@ export function IssuesList({
|
||||
}
|
||||
return map;
|
||||
}, [projects]);
|
||||
const defaultProjectWorkspaceIds = useMemo(
|
||||
() => new Set(defaultProjectWorkspaceIdByProjectId.values()),
|
||||
[defaultProjectWorkspaceIdByProjectId],
|
||||
);
|
||||
|
||||
const executionWorkspaceById = useMemo(() => {
|
||||
const map = new Map<string, {
|
||||
@@ -499,17 +504,27 @@ export function IssuesList({
|
||||
}
|
||||
return map;
|
||||
}, [executionWorkspaces]);
|
||||
const issueFilterWorkspaceContext = useMemo(() => ({
|
||||
executionWorkspaceById,
|
||||
defaultProjectWorkspaceIdByProjectId,
|
||||
}), [defaultProjectWorkspaceIdByProjectId, executionWorkspaceById]);
|
||||
|
||||
const workspaceNameMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const [workspaceId, workspace] of projectWorkspaceById) {
|
||||
if (!shouldIncludeIssueFilterWorkspaceOption({ id: workspaceId }, defaultProjectWorkspaceIds)) continue;
|
||||
map.set(workspaceId, workspace.name);
|
||||
}
|
||||
for (const [workspaceId, workspace] of executionWorkspaceById) {
|
||||
if (!shouldIncludeIssueFilterWorkspaceOption({
|
||||
id: workspaceId,
|
||||
mode: workspace.mode,
|
||||
projectWorkspaceId: workspace.projectWorkspaceId,
|
||||
}, defaultProjectWorkspaceIds)) continue;
|
||||
map.set(workspaceId, workspace.name);
|
||||
}
|
||||
return map;
|
||||
}, [executionWorkspaceById, projectWorkspaceById]);
|
||||
}, [defaultProjectWorkspaceIds, executionWorkspaceById, projectWorkspaceById]);
|
||||
|
||||
const workspaceOptions = useMemo(() => {
|
||||
const options = new Map<string, string>();
|
||||
@@ -635,9 +650,27 @@ export function IssuesList({
|
||||
const searchScopedIssues = normalizedIssueSearch.length > 0 && searchWithinLoadedIssues
|
||||
? sourceIssues.filter((issue) => issueMatchesLocalSearch(issue, normalizedIssueSearch))
|
||||
: sourceIssues;
|
||||
const filteredByControls = applyIssueFilters(searchScopedIssues, viewState, currentUserId, enableRoutineVisibilityFilter);
|
||||
const filteredByControls = applyIssueFilters(
|
||||
searchScopedIssues,
|
||||
viewState,
|
||||
currentUserId,
|
||||
enableRoutineVisibilityFilter,
|
||||
liveIssueIds,
|
||||
issueFilterWorkspaceContext,
|
||||
);
|
||||
return sortIssues(filteredByControls, viewState);
|
||||
}, [boardIssues, issues, searchedIssues, searchWithinLoadedIssues, viewState, normalizedIssueSearch, currentUserId, enableRoutineVisibilityFilter]);
|
||||
}, [
|
||||
boardIssues,
|
||||
issues,
|
||||
searchedIssues,
|
||||
searchWithinLoadedIssues,
|
||||
viewState,
|
||||
normalizedIssueSearch,
|
||||
currentUserId,
|
||||
enableRoutineVisibilityFilter,
|
||||
liveIssueIds,
|
||||
issueFilterWorkspaceContext,
|
||||
]);
|
||||
|
||||
const { data: labels } = useQuery({
|
||||
queryKey: queryKeys.issues.labels(selectedCompanyId!),
|
||||
@@ -664,7 +697,10 @@ export function IssuesList({
|
||||
.map((p) => ({ key: p, label: issueFilterLabel(p), items: groups[p]! }));
|
||||
}
|
||||
if (viewState.groupBy === "workspace") {
|
||||
const groups = groupBy(filtered, (issue) => resolveIssueFilterWorkspaceId(issue) ?? "__no_workspace");
|
||||
const groups = groupBy(
|
||||
filtered,
|
||||
(issue) => resolveIssueFilterWorkspaceId(issue, issueFilterWorkspaceContext) ?? "__no_workspace",
|
||||
);
|
||||
return Object.keys(groups)
|
||||
.sort((a, b) => {
|
||||
// Groups with items first, "no workspace" last
|
||||
@@ -708,7 +744,17 @@ export function IssuesList({
|
||||
: (agentName(key) ?? key.slice(0, 8)),
|
||||
items: groups[key]!,
|
||||
}));
|
||||
}, [filtered, viewState.groupBy, agents, agentName, currentUserId, workspaceNameMap, issueTitleMap, companyUserLabelMap]);
|
||||
}, [
|
||||
filtered,
|
||||
issueFilterWorkspaceContext,
|
||||
viewState.groupBy,
|
||||
agents,
|
||||
agentName,
|
||||
currentUserId,
|
||||
workspaceNameMap,
|
||||
issueTitleMap,
|
||||
companyUserLabelMap,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewState.viewMode !== "list") return;
|
||||
@@ -1125,7 +1171,7 @@ export function IssuesList({
|
||||
columns={visibleTrailingIssueColumns}
|
||||
projectName={issueProject?.name ?? null}
|
||||
projectColor={issueProject?.color ?? null}
|
||||
workspaceId={resolveIssueFilterWorkspaceId(issue)}
|
||||
workspaceId={resolveIssueFilterWorkspaceId(issue, issueFilterWorkspaceContext)}
|
||||
workspaceName={resolveIssueWorkspaceName(issue, {
|
||||
executionWorkspaceById,
|
||||
projectWorkspaceById,
|
||||
|
||||
@@ -841,6 +841,7 @@ describe("inbox helpers", () => {
|
||||
labels: [],
|
||||
projects: [],
|
||||
workspaces: [],
|
||||
liveOnly: false,
|
||||
hideRoutineExecutions: true,
|
||||
},
|
||||
}).map((issue) => issue.id),
|
||||
@@ -860,6 +861,7 @@ describe("inbox helpers", () => {
|
||||
labels: [],
|
||||
projects: [],
|
||||
workspaces: [],
|
||||
liveOnly: false,
|
||||
hideRoutineExecutions: true,
|
||||
},
|
||||
}),
|
||||
@@ -879,6 +881,7 @@ describe("inbox helpers", () => {
|
||||
labels: [],
|
||||
projects: [],
|
||||
workspaces: [],
|
||||
liveOnly: false,
|
||||
hideRoutineExecutions: true,
|
||||
},
|
||||
}),
|
||||
@@ -944,6 +947,7 @@ describe("inbox helpers", () => {
|
||||
labels: ["label-1"],
|
||||
projects: ["project-1"],
|
||||
workspaces: ["workspace-1"],
|
||||
liveOnly: true,
|
||||
hideRoutineExecutions: false,
|
||||
},
|
||||
});
|
||||
@@ -958,6 +962,7 @@ describe("inbox helpers", () => {
|
||||
labels: [],
|
||||
projects: [],
|
||||
workspaces: [],
|
||||
liveOnly: false,
|
||||
hideRoutineExecutions: true,
|
||||
},
|
||||
});
|
||||
@@ -973,6 +978,7 @@ describe("inbox helpers", () => {
|
||||
labels: ["label-1"],
|
||||
projects: ["project-1"],
|
||||
workspaces: ["workspace-1"],
|
||||
liveOnly: true,
|
||||
hideRoutineExecutions: false,
|
||||
},
|
||||
});
|
||||
@@ -987,6 +993,7 @@ describe("inbox helpers", () => {
|
||||
labels: [],
|
||||
projects: [],
|
||||
workspaces: [],
|
||||
liveOnly: false,
|
||||
hideRoutineExecutions: true,
|
||||
},
|
||||
});
|
||||
@@ -1004,6 +1011,7 @@ describe("inbox helpers", () => {
|
||||
labels: null,
|
||||
projects: ["project-1"],
|
||||
workspaces: ["workspace-1", false],
|
||||
liveOnly: "yes",
|
||||
hideRoutineExecutions: "yes",
|
||||
},
|
||||
}));
|
||||
@@ -1019,6 +1027,7 @@ describe("inbox helpers", () => {
|
||||
labels: [],
|
||||
projects: ["project-1"],
|
||||
workspaces: ["workspace-1"],
|
||||
liveOnly: false,
|
||||
hideRoutineExecutions: false,
|
||||
},
|
||||
});
|
||||
|
||||
+3
-1
@@ -445,6 +445,7 @@ export function getInboxSearchSupplementIssues({
|
||||
issueFilters,
|
||||
currentUserId,
|
||||
enableRoutineVisibilityFilter = false,
|
||||
liveIssueIds,
|
||||
}: {
|
||||
query: string;
|
||||
filteredWorkItems: InboxWorkItem[];
|
||||
@@ -453,6 +454,7 @@ export function getInboxSearchSupplementIssues({
|
||||
issueFilters: IssueFilterState;
|
||||
currentUserId?: string | null;
|
||||
enableRoutineVisibilityFilter?: boolean;
|
||||
liveIssueIds?: ReadonlySet<string>;
|
||||
}): Issue[] {
|
||||
const normalizedQuery = query.trim();
|
||||
if (!normalizedQuery) return [];
|
||||
@@ -462,7 +464,7 @@ export function getInboxSearchSupplementIssues({
|
||||
.map((item) => item.issue.id),
|
||||
...archivedSearchIssues.map((issue) => issue.id),
|
||||
]);
|
||||
return applyIssueFilters(remoteIssues, issueFilters, currentUserId, enableRoutineVisibilityFilter)
|
||||
return applyIssueFilters(remoteIssues, issueFilters, currentUserId, enableRoutineVisibilityFilter, liveIssueIds)
|
||||
.filter((issue) => !visibleIssueIds.has(issue.id));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
import { applyIssueFilters, countActiveIssueFilters, defaultIssueFilterState } from "./issue-filters";
|
||||
import {
|
||||
applyIssueFilters,
|
||||
countActiveIssueFilters,
|
||||
defaultIssueFilterState,
|
||||
resolveIssueFilterWorkspaceId,
|
||||
shouldIncludeIssueFilterWorkspaceOption,
|
||||
} from "./issue-filters";
|
||||
|
||||
function makeIssue(overrides: Partial<Issue> = {}): Issue {
|
||||
return {
|
||||
@@ -66,4 +72,100 @@ describe("issue filters", () => {
|
||||
creators: ["user:user-1"],
|
||||
})).toBe(1);
|
||||
});
|
||||
|
||||
it("filters issues to live issue ids when live-only is enabled", () => {
|
||||
const issues = [
|
||||
makeIssue({ id: "live-issue" }),
|
||||
makeIssue({ id: "idle-issue" }),
|
||||
];
|
||||
|
||||
const filtered = applyIssueFilters(
|
||||
issues,
|
||||
{ ...defaultIssueFilterState, liveOnly: true },
|
||||
null,
|
||||
false,
|
||||
new Set(["live-issue"]),
|
||||
);
|
||||
|
||||
expect(filtered.map((issue) => issue.id)).toEqual(["live-issue"]);
|
||||
});
|
||||
|
||||
it("counts the live-only filter as an active filter group", () => {
|
||||
expect(countActiveIssueFilters({
|
||||
...defaultIssueFilterState,
|
||||
liveOnly: true,
|
||||
})).toBe(1);
|
||||
});
|
||||
|
||||
it("does not treat default project workspaces as workspace filter matches", () => {
|
||||
const issue = makeIssue({
|
||||
id: "default-workspace-issue",
|
||||
projectId: "project-1",
|
||||
projectWorkspaceId: "workspace-default",
|
||||
});
|
||||
const workspaceContext = {
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([["project-1", "workspace-default"]]),
|
||||
};
|
||||
|
||||
expect(resolveIssueFilterWorkspaceId(issue, workspaceContext)).toBeNull();
|
||||
expect(applyIssueFilters(
|
||||
[issue],
|
||||
{ ...defaultIssueFilterState, workspaces: ["workspace-default"] },
|
||||
null,
|
||||
false,
|
||||
undefined,
|
||||
workspaceContext,
|
||||
)).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not treat shared default execution workspaces as workspace filter matches", () => {
|
||||
const issue = makeIssue({
|
||||
id: "shared-default-issue",
|
||||
projectId: "project-1",
|
||||
projectWorkspaceId: "workspace-default",
|
||||
executionWorkspaceId: "execution-shared-default",
|
||||
});
|
||||
const workspaceContext = {
|
||||
executionWorkspaceById: new Map([[
|
||||
"execution-shared-default",
|
||||
{ mode: "shared_workspace", projectWorkspaceId: "workspace-default" },
|
||||
]]),
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([["project-1", "workspace-default"]]),
|
||||
};
|
||||
|
||||
expect(resolveIssueFilterWorkspaceId(issue, workspaceContext)).toBeNull();
|
||||
expect(shouldIncludeIssueFilterWorkspaceOption(
|
||||
{ id: "execution-shared-default", mode: "shared_workspace", projectWorkspaceId: "workspace-default" },
|
||||
new Set(["workspace-default"]),
|
||||
)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps non-default project and isolated execution workspaces filterable", () => {
|
||||
const featureIssue = makeIssue({
|
||||
id: "feature-issue",
|
||||
projectId: "project-1",
|
||||
projectWorkspaceId: "workspace-feature",
|
||||
});
|
||||
const executionIssue = makeIssue({
|
||||
id: "execution-issue",
|
||||
projectId: "project-1",
|
||||
projectWorkspaceId: "workspace-default",
|
||||
executionWorkspaceId: "execution-isolated",
|
||||
});
|
||||
const workspaceContext = {
|
||||
executionWorkspaceById: new Map([[
|
||||
"execution-isolated",
|
||||
{ mode: "isolated_workspace", projectWorkspaceId: "workspace-default" },
|
||||
]]),
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([["project-1", "workspace-default"]]),
|
||||
};
|
||||
|
||||
expect(resolveIssueFilterWorkspaceId(featureIssue, workspaceContext)).toBe("workspace-feature");
|
||||
expect(resolveIssueFilterWorkspaceId(executionIssue, workspaceContext)).toBe("execution-isolated");
|
||||
expect(shouldIncludeIssueFilterWorkspaceOption({ id: "workspace-feature" }, new Set(["workspace-default"]))).toBe(true);
|
||||
expect(shouldIncludeIssueFilterWorkspaceOption(
|
||||
{ id: "execution-isolated", mode: "isolated_workspace", projectWorkspaceId: "workspace-default" },
|
||||
new Set(["workspace-default"]),
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,15 @@
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
|
||||
export type IssueFilterWorkspaceLookup = {
|
||||
mode?: string | null;
|
||||
projectWorkspaceId?: string | null;
|
||||
};
|
||||
|
||||
export type IssueFilterWorkspaceContext = {
|
||||
executionWorkspaceById?: ReadonlyMap<string, IssueFilterWorkspaceLookup>;
|
||||
defaultProjectWorkspaceIdByProjectId?: ReadonlyMap<string, string>;
|
||||
};
|
||||
|
||||
export type IssueFilterState = {
|
||||
statuses: string[];
|
||||
priorities: string[];
|
||||
@@ -8,6 +18,7 @@ export type IssueFilterState = {
|
||||
labels: string[];
|
||||
projects: string[];
|
||||
workspaces: string[];
|
||||
liveOnly?: boolean;
|
||||
hideRoutineExecutions: boolean;
|
||||
};
|
||||
|
||||
@@ -19,6 +30,7 @@ export const defaultIssueFilterState: IssueFilterState = {
|
||||
labels: [],
|
||||
projects: [],
|
||||
workspaces: [],
|
||||
liveOnly: false,
|
||||
hideRoutineExecutions: false,
|
||||
};
|
||||
|
||||
@@ -59,6 +71,7 @@ export function normalizeIssueFilterState(value: unknown): IssueFilterState {
|
||||
labels: normalizeIssueFilterValueArray(candidate.labels),
|
||||
projects: normalizeIssueFilterValueArray(candidate.projects),
|
||||
workspaces: normalizeIssueFilterValueArray(candidate.workspaces),
|
||||
liveOnly: candidate.liveOnly === true,
|
||||
hideRoutineExecutions: candidate.hideRoutineExecutions === true,
|
||||
};
|
||||
}
|
||||
@@ -68,9 +81,41 @@ export function toggleIssueFilterValue(values: string[], value: string): string[
|
||||
}
|
||||
|
||||
export function resolveIssueFilterWorkspaceId(
|
||||
issue: Pick<Issue, "executionWorkspaceId" | "projectWorkspaceId">,
|
||||
issue: Pick<Issue, "executionWorkspaceId" | "projectId" | "projectWorkspaceId">,
|
||||
context: IssueFilterWorkspaceContext = {},
|
||||
): string | null {
|
||||
return issue.executionWorkspaceId ?? issue.projectWorkspaceId ?? null;
|
||||
const defaultProjectWorkspaceId = issue.projectId
|
||||
? context.defaultProjectWorkspaceIdByProjectId?.get(issue.projectId) ?? null
|
||||
: null;
|
||||
|
||||
if (issue.executionWorkspaceId) {
|
||||
const executionWorkspace = context.executionWorkspaceById?.get(issue.executionWorkspaceId) ?? null;
|
||||
const linkedProjectWorkspaceId =
|
||||
executionWorkspace?.projectWorkspaceId ?? issue.projectWorkspaceId ?? null;
|
||||
const isDefaultSharedExecutionWorkspace =
|
||||
executionWorkspace?.mode === "shared_workspace"
|
||||
&& linkedProjectWorkspaceId != null
|
||||
&& linkedProjectWorkspaceId === defaultProjectWorkspaceId;
|
||||
if (isDefaultSharedExecutionWorkspace) return null;
|
||||
return issue.executionWorkspaceId;
|
||||
}
|
||||
|
||||
if (issue.projectWorkspaceId) {
|
||||
if (issue.projectWorkspaceId === defaultProjectWorkspaceId) return null;
|
||||
return issue.projectWorkspaceId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldIncludeIssueFilterWorkspaceOption(
|
||||
workspace: { id: string; mode?: string | null; projectWorkspaceId?: string | null },
|
||||
defaultProjectWorkspaceIds: ReadonlySet<string>,
|
||||
): boolean {
|
||||
if (defaultProjectWorkspaceIds.has(workspace.id)) return false;
|
||||
return !(workspace.mode === "shared_workspace"
|
||||
&& workspace.projectWorkspaceId != null
|
||||
&& defaultProjectWorkspaceIds.has(workspace.projectWorkspaceId));
|
||||
}
|
||||
|
||||
export function applyIssueFilters(
|
||||
@@ -78,8 +123,13 @@ export function applyIssueFilters(
|
||||
state: IssueFilterState,
|
||||
currentUserId?: string | null,
|
||||
enableRoutineVisibilityFilter = false,
|
||||
liveIssueIds?: ReadonlySet<string>,
|
||||
workspaceContext: IssueFilterWorkspaceContext = {},
|
||||
): Issue[] {
|
||||
let result = issues;
|
||||
if (state.liveOnly) {
|
||||
result = result.filter((issue) => liveIssueIds?.has(issue.id) === true);
|
||||
}
|
||||
if (enableRoutineVisibilityFilter && state.hideRoutineExecutions) {
|
||||
result = result.filter((issue) => issue.originKind !== "routine_execution");
|
||||
}
|
||||
@@ -112,7 +162,7 @@ export function applyIssueFilters(
|
||||
}
|
||||
if (state.workspaces.length > 0) {
|
||||
result = result.filter((issue) => {
|
||||
const workspaceId = resolveIssueFilterWorkspaceId(issue);
|
||||
const workspaceId = resolveIssueFilterWorkspaceId(issue, workspaceContext);
|
||||
return workspaceId != null && state.workspaces.includes(workspaceId);
|
||||
});
|
||||
}
|
||||
@@ -131,6 +181,7 @@ export function countActiveIssueFilters(
|
||||
if (state.labels.length > 0) count += 1;
|
||||
if (state.projects.length > 0) count += 1;
|
||||
if (state.workspaces.length > 0) count += 1;
|
||||
if (state.liveOnly) count += 1;
|
||||
if (enableRoutineVisibilityFilter && state.hideRoutineExecutions) count += 1;
|
||||
return count;
|
||||
}
|
||||
@@ -57,6 +57,19 @@ describe("collectLiveIssueIds", () => {
|
||||
adapterType: "codex_local",
|
||||
issueId: "issue-2",
|
||||
},
|
||||
{
|
||||
id: "run-5",
|
||||
status: "succeeded",
|
||||
invocationSource: "scheduler",
|
||||
triggerDetail: null,
|
||||
startedAt: "2026-04-20T10:04:00.000Z",
|
||||
finishedAt: "2026-04-20T10:05:00.000Z",
|
||||
createdAt: "2026-04-20T10:04:00.000Z",
|
||||
agentId: "agent-5",
|
||||
agentName: "Done",
|
||||
adapterType: "codex_local",
|
||||
issueId: "completed-issue",
|
||||
},
|
||||
];
|
||||
|
||||
expect([...collectLiveIssueIds(liveRuns)]).toEqual(["issue-1", "issue-2"]);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { LiveRunForIssue } from "../api/heartbeats";
|
||||
|
||||
function isLiveRunStatus(status: string): boolean {
|
||||
return status === "queued" || status === "running";
|
||||
}
|
||||
|
||||
export function collectLiveIssueIds(liveRuns: readonly LiveRunForIssue[] | null | undefined): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const run of liveRuns ?? []) {
|
||||
if (run.issueId) ids.add(run.issueId);
|
||||
if (run.issueId && isLiveRunStatus(run.status)) ids.add(run.issueId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "../components/WorkspaceRuntimeControls";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { collectLiveIssueIds } from "../lib/liveIssueIds";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { cn, formatDateTime, issueUrl, projectRouteRef, projectWorkspaceUrl } from "../lib/utils";
|
||||
|
||||
@@ -271,13 +272,7 @@ function ExecutionWorkspaceIssuesList({
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const liveIssueIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const run of liveRuns ?? []) {
|
||||
if (run.issueId) ids.add(run.issueId);
|
||||
}
|
||||
return ids;
|
||||
}, [liveRuns]);
|
||||
const liveIssueIds = useMemo(() => collectLiveIssueIds(liveRuns), [liveRuns]);
|
||||
|
||||
const updateIssue = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) => issuesApi.update(id, data),
|
||||
|
||||
+8
-12
@@ -23,6 +23,7 @@ import {
|
||||
countActiveIssueFilters,
|
||||
type IssueFilterState,
|
||||
} from "../lib/issue-filters";
|
||||
import { collectLiveIssueIds } from "../lib/liveIssueIds";
|
||||
import { formatAssigneeUserLabel } from "../lib/assignees";
|
||||
import { buildCompanyUserLabelMap, buildCompanyUserProfileMap } from "../lib/company-members";
|
||||
import {
|
||||
@@ -826,6 +827,7 @@ export function Inbox() {
|
||||
enabled: !!selectedCompanyId,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const liveIssueIds = useMemo(() => collectLiveIssueIds(liveRuns), [liveRuns]);
|
||||
const { data: companyMembers } = useQuery({
|
||||
queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!),
|
||||
queryFn: () => accessApi.listUserDirectory(selectedCompanyId!),
|
||||
@@ -845,12 +847,12 @@ export function Inbox() {
|
||||
const mineIssues = useMemo(() => getRecentTouchedIssues(mineIssuesRaw), [mineIssuesRaw]);
|
||||
const touchedIssues = useMemo(() => getRecentTouchedIssues(touchedIssuesRaw), [touchedIssuesRaw]);
|
||||
const visibleMineIssues = useMemo(
|
||||
() => applyIssueFilters(mineIssues, issueFilters, currentUserId, true),
|
||||
[mineIssues, issueFilters, currentUserId],
|
||||
() => applyIssueFilters(mineIssues, issueFilters, currentUserId, true, liveIssueIds),
|
||||
[mineIssues, issueFilters, currentUserId, liveIssueIds],
|
||||
);
|
||||
const visibleTouchedIssues = useMemo(
|
||||
() => applyIssueFilters(touchedIssues, issueFilters, currentUserId, true),
|
||||
[touchedIssues, issueFilters, currentUserId],
|
||||
() => applyIssueFilters(touchedIssues, issueFilters, currentUserId, true, liveIssueIds),
|
||||
[touchedIssues, issueFilters, currentUserId, liveIssueIds],
|
||||
);
|
||||
const unreadTouchedIssues = useMemo(
|
||||
() => visibleTouchedIssues.filter((issue) => issue.isUnreadForMe),
|
||||
@@ -1004,14 +1006,6 @@ export function Inbox() {
|
||||
),
|
||||
[heartbeatRuns, dismissedAtByKey],
|
||||
);
|
||||
const liveIssueIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const run of liveRuns ?? []) {
|
||||
if (run.issueId) ids.add(run.issueId);
|
||||
}
|
||||
return ids;
|
||||
}, [liveRuns]);
|
||||
|
||||
const approvalsToRender = useMemo(() => {
|
||||
let filtered = getApprovalsForTab(approvals ?? [], tab, allApprovalFilter, currentUserId);
|
||||
if (tab === "mine") {
|
||||
@@ -1159,12 +1153,14 @@ export function Inbox() {
|
||||
issueFilters,
|
||||
currentUserId,
|
||||
enableRoutineVisibilityFilter: true,
|
||||
liveIssueIds,
|
||||
}),
|
||||
[
|
||||
archivedSearchIssues,
|
||||
currentUserId,
|
||||
filteredWorkItems,
|
||||
issueFilters,
|
||||
liveIssueIds,
|
||||
normalizedSearchQuery,
|
||||
remoteIssueSearchResults,
|
||||
],
|
||||
|
||||
@@ -24,6 +24,7 @@ import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { PageTabBar } from "../components/PageTabBar";
|
||||
import { ProjectWorkspacesContent } from "../components/ProjectWorkspacesContent";
|
||||
import { buildProjectWorkspaceSummaries } from "../lib/project-workspaces-tab";
|
||||
import { collectLiveIssueIds } from "../lib/liveIssueIds";
|
||||
import { projectRouteRef } from "../lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
@@ -175,13 +176,7 @@ function ProjectIssuesList({ projectId, companyId }: { projectId: string; compan
|
||||
enabled: !!companyId,
|
||||
});
|
||||
|
||||
const liveIssueIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const run of liveRuns ?? []) {
|
||||
if (run.issueId) ids.add(run.issueId);
|
||||
}
|
||||
return ids;
|
||||
}, [liveRuns]);
|
||||
const liveIssueIds = useMemo(() => collectLiveIssueIds(liveRuns), [liveRuns]);
|
||||
|
||||
const { data: issues, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.issues.listByProject(companyId, projectId),
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { groupBy } from "../lib/groupBy";
|
||||
import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb";
|
||||
import { collectLiveIssueIds } from "../lib/liveIssueIds";
|
||||
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
||||
import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects";
|
||||
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
||||
@@ -492,13 +493,7 @@ export function Routines() {
|
||||
() => new Map((projects ?? []).map((project) => [project.id, project])),
|
||||
[projects],
|
||||
);
|
||||
const liveIssueIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const run of liveRuns ?? []) {
|
||||
if (run.issueId) ids.add(run.issueId);
|
||||
}
|
||||
return ids;
|
||||
}, [liveRuns]);
|
||||
const liveIssueIds = useMemo(() => collectLiveIssueIds(liveRuns), [liveRuns]);
|
||||
const routineGroups = useMemo(
|
||||
() => buildRoutineGroups(routines ?? [], routineViewState.groupBy, projectById, agentById),
|
||||
[agentById, projectById, routineViewState.groupBy, routines],
|
||||
|
||||
Reference in new issue
Block a user