Scope blocked-by summaries to sub-issue lists

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-25 22:01:16 -05:00
1 parent 6b4cacc484
commit 570fdce388
7 files changed
+22 -12

No files matched your search

+4 -1
View File
@@ -939,7 +939,10 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
type: "blocks",
});
const result = await svc.list(companyId);
const defaultResult = await svc.list(companyId);
expect(defaultResult.find((issue) => issue.id === blockedId)?.blockedBy).toBeUndefined();
const result = await svc.list(companyId, { includeBlockedBy: true });
const byId = new Map(result.map((issue) => [issue.id, issue]));
expect(byId.get(blockedId)?.blockedBy).toEqual([
+1
View File
@@ -949,6 +949,7 @@ export function issueRoutes(
req.query.includeRoutineExecutions === "true" || req.query.includeRoutineExecutions === "1",
excludeRoutineExecutions:
req.query.excludeRoutineExecutions === "true" || req.query.excludeRoutineExecutions === "1",
includeBlockedBy: req.query.includeBlockedBy === "true" || req.query.includeBlockedBy === "1",
q: req.query.q as string | undefined,
limit,
});
+9 -4
View File
@@ -101,6 +101,7 @@ export interface IssueFilters {
originId?: string;
includeRoutineExecutions?: boolean;
excludeRoutineExecutions?: boolean;
includeBlockedBy?: boolean;
q?: string;
limit?: number;
}
@@ -1303,10 +1304,11 @@ async function blockedByMapForIssues(
): Promise<Map<string, IssueRelationIssueSummary[]>> {
const map = new Map<string, IssueRelationIssueSummary[]>();
const uniqueIssueIds = [...new Set(issueIds)];
if (uniqueIssueIds.length === 0) return map;
for (const issueId of uniqueIssueIds) {
map.set(issueId, []);
}
if (uniqueIssueIds.length === 0) return map;
for (const issueIdChunk of chunkList(uniqueIssueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
const rows = await dbOrTx
@@ -1840,6 +1842,7 @@ export function issueService(db: Db) {
const inboxArchivedByUserId = filters?.inboxArchivedByUserId?.trim() || undefined;
const unreadForUserId = filters?.unreadForUserId?.trim() || undefined;
const contextUserId = unreadForUserId ?? touchedByUserId ?? inboxArchivedByUserId;
const includeBlockedBy = filters?.includeBlockedBy === true;
const rawSearch = filters?.q?.trim() ?? "";
const hasSearch = rawSearch.length > 0;
const escapedSearch = hasSearch ? escapeLikePattern(rawSearch) : "";
@@ -1978,7 +1981,9 @@ export function issueService(db: Db) {
? userReadStatsForIssues(db, companyId, contextUserId, issueIds)
: Promise.resolve([]),
lastActivityStatsForIssues(db, companyId, issueIds),
blockedByMapForIssues(db, companyId, issueIds),
includeBlockedBy
? blockedByMapForIssues(db, companyId, issueIds)
: Promise.resolve(new Map<string, IssueRelationIssueSummary[]>()),
]);
const statsByIssueId = new Map(statsRows.map((row) => [row.issueId, row]));
const lastActivityByIssueId = new Map(lastActivityRows.map((row) => [row.issueId, row]));
@@ -1994,7 +1999,7 @@ export function issueService(db: Db) {
) ?? row.updatedAt;
return {
...row,
blockedBy: blockedByMap.get(row.id) ?? [],
...(includeBlockedBy ? { blockedBy: blockedByMap.get(row.id) ?? [] } : {}),
lastActivityAt,
...(blockerAttentionByIssueId.has(row.id) ? { blockerAttention: blockerAttentionByIssueId.get(row.id) } : {}),
};
@@ -2012,7 +2017,7 @@ export function issueService(db: Db) {
) ?? row.updatedAt;
return {
...row,
blockedBy: blockedByMap.get(row.id) ?? [],
...(includeBlockedBy ? { blockedBy: blockedByMap.get(row.id) ?? [] } : {}),
lastActivityAt,
...(blockerAttentionByIssueId.has(row.id) ? { blockerAttention: blockerAttentionByIssueId.get(row.id) } : {}),
...deriveIssueUserContext(row, contextUserId, {
+2 -2
View File
@@ -25,10 +25,10 @@ describe("issuesApi.list", () => {
});
it("passes descendantOf through to the company issues endpoint", async () => {
await issuesApi.list("company-1", { descendantOf: "issue-root-1", limit: 25 });
await issuesApi.list("company-1", { descendantOf: "issue-root-1", includeBlockedBy: true, limit: 25 });
expect(mockApi.get).toHaveBeenCalledWith(
"/companies/company-1/issues?descendantOf=issue-root-1&limit=25",
"/companies/company-1/issues?descendantOf=issue-root-1&includeBlockedBy=true&limit=25",
);
});
+2
View File
@@ -45,6 +45,7 @@ export const issuesApi = {
originId?: string;
descendantOf?: string;
includeRoutineExecutions?: boolean;
includeBlockedBy?: boolean;
q?: string;
limit?: number;
},
@@ -66,6 +67,7 @@ export const issuesApi = {
if (filters?.originId) params.set("originId", filters.originId);
if (filters?.descendantOf) params.set("descendantOf", filters.descendantOf);
if (filters?.includeRoutineExecutions) params.set("includeRoutineExecutions", "true");
if (filters?.includeBlockedBy) params.set("includeBlockedBy", "true");
if (filters?.q) params.set("q", filters.q);
if (filters?.limit) params.set("limit", String(filters.limit));
const qs = params.toString();
+2 -3
View File
@@ -204,7 +204,7 @@ function saveIssueColumns(key: string, columns: InboxIssueColumn[]) {
function sortIssues(issues: Issue[], state: IssueViewState): Issue[] {
if (state.sortField === "workflow") {
const ordered = workflowSort(issues);
return state.sortDir === "desc" ? ordered.reverse() : ordered;
return state.sortDir === "desc" ? [...ordered].reverse() : ordered;
}
const sorted = [...issues];
const dir = state.sortDir === "asc" ? 1 : -1;
@@ -848,9 +848,8 @@ export function IssuesList({
const checklistAffordanceEnabled = useMemo(
() =>
defaultSortField === "workflow"
&& viewState.sortField === "workflow"
&& viewState.groupBy === "none",
[defaultSortField, viewState.groupBy, viewState.sortField],
[defaultSortField, viewState.groupBy],
);
const workflowChecklistMeta = useMemo(() => {
if (!checklistAffordanceEnabled) return null;
+2 -2
View File
@@ -1151,7 +1151,7 @@ export function IssueDetail() {
issue?.id && resolvedCompanyId
? queryKeys.issues.listByDescendantRoot(resolvedCompanyId, issue.id)
: ["issues", "parent", "pending"],
queryFn: () => issuesApi.list(resolvedCompanyId!, { descendantOf: issue!.id }),
queryFn: () => issuesApi.list(resolvedCompanyId!, { descendantOf: issue!.id, includeBlockedBy: true }),
enabled: !!resolvedCompanyId && !!issue?.id,
placeholderData: keepPreviousDataForSameQueryTail<Issue[]>(issue?.id ?? "pending"),
});
@@ -3155,7 +3155,7 @@ export function IssueDetail() {
projectId={issue.projectId ?? undefined}
viewStateKey={`paperclip:issue-detail:${issue.id}:subissues-view`}
issueLinkState={resolvedIssueDetailState ?? location.state}
searchFilters={{ descendantOf: issue.id }}
searchFilters={{ descendantOf: issue.id, includeBlockedBy: true }}
searchWithinLoadedIssues
baseCreateIssueDefaults={buildSubIssueDefaultsForViewer(issue, currentUserId)}
createIssueLabel="Sub-issue"