mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 10:30:37 +09:00
merge master into pap-1078-qol-fixes
Resolve the keyboard shortcut conflicts after [#2539](https://github.com/paperclipai/paperclip/pull/2539) and [#2540](https://github.com/paperclipai/paperclip/pull/2540), keep the release package rewrite working with cliVersion, and stabilize the provisioning timeout in the full suite. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
fb3b57ab1f
59 changed files with 16794 additions and 375 deletions
|
|
@ -1,18 +1,32 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
|
||||
import type {
|
||||
Approval,
|
||||
DashboardSummary,
|
||||
ExecutionWorkspace,
|
||||
HeartbeatRun,
|
||||
Issue,
|
||||
JoinRequest,
|
||||
ProjectWorkspace,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
DEFAULT_INBOX_ISSUE_COLUMNS,
|
||||
computeInboxBadgeData,
|
||||
getAvailableInboxIssueColumns,
|
||||
getApprovalsForTab,
|
||||
getInboxWorkItems,
|
||||
getInboxKeyboardSelectionIndex,
|
||||
getRecentTouchedIssues,
|
||||
getUnreadTouchedIssues,
|
||||
isMineInboxTab,
|
||||
loadInboxIssueColumns,
|
||||
loadLastInboxTab,
|
||||
normalizeInboxIssueColumns,
|
||||
RECENT_ISSUES_LIMIT,
|
||||
resolveIssueWorkspaceName,
|
||||
resolveInboxSelectionIndex,
|
||||
saveInboxIssueColumns,
|
||||
saveLastInboxTab,
|
||||
shouldShowInboxSection,
|
||||
} from "./inbox";
|
||||
|
|
@ -170,6 +184,63 @@ function makeIssue(id: string, isUnreadForMe: boolean): Issue {
|
|||
};
|
||||
}
|
||||
|
||||
function makeProjectWorkspace(overrides: Partial<ProjectWorkspace> = {}): ProjectWorkspace {
|
||||
return {
|
||||
id: "project-workspace-1",
|
||||
companyId: "company-1",
|
||||
projectId: "project-1",
|
||||
name: "Primary workspace",
|
||||
sourceType: "local_path",
|
||||
cwd: "/tmp/project",
|
||||
repoUrl: null,
|
||||
repoRef: null,
|
||||
defaultRef: null,
|
||||
visibility: "default",
|
||||
setupCommand: null,
|
||||
cleanupCommand: null,
|
||||
remoteProvider: null,
|
||||
remoteWorkspaceRef: null,
|
||||
sharedWorkspaceKey: null,
|
||||
metadata: null,
|
||||
runtimeConfig: null,
|
||||
isPrimary: true,
|
||||
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeExecutionWorkspace(overrides: Partial<ExecutionWorkspace> = {}): ExecutionWorkspace {
|
||||
return {
|
||||
id: "execution-workspace-1",
|
||||
companyId: "company-1",
|
||||
projectId: "project-1",
|
||||
projectWorkspaceId: "project-workspace-1",
|
||||
sourceIssueId: "issue-1",
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "PAP-1 branch",
|
||||
status: "active",
|
||||
cwd: "/tmp/project/worktree",
|
||||
repoUrl: null,
|
||||
baseRef: null,
|
||||
branchName: "pap-1",
|
||||
providerType: "git_worktree",
|
||||
providerRef: null,
|
||||
derivedFromExecutionWorkspaceId: null,
|
||||
lastUsedAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
openedAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
closedAt: null,
|
||||
cleanupEligibleAt: null,
|
||||
cleanupReason: null,
|
||||
config: null,
|
||||
metadata: null,
|
||||
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const dashboard: DashboardSummary = {
|
||||
companyId: "company-1",
|
||||
agents: {
|
||||
|
|
@ -314,6 +385,16 @@ describe("inbox helpers", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("sorts touched issues by latest external comment timestamp", () => {
|
||||
const newerIssue = makeIssue("1", true);
|
||||
newerIssue.lastExternalCommentAt = new Date("2026-03-11T05:00:00.000Z");
|
||||
|
||||
const olderIssue = makeIssue("2", true);
|
||||
olderIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
|
||||
expect(getRecentTouchedIssues([olderIssue, newerIssue]).map((issue) => issue.id)).toEqual(["1", "2"]);
|
||||
});
|
||||
|
||||
it("mixes join requests into the inbox feed by most recent activity", () => {
|
||||
const issue = makeIssue("1", true);
|
||||
issue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
|
|
@ -419,6 +500,116 @@ describe("inbox helpers", () => {
|
|||
expect(loadLastInboxTab()).toBe("all");
|
||||
});
|
||||
|
||||
it("defaults issue columns to the current inbox layout", () => {
|
||||
expect(loadInboxIssueColumns()).toEqual(DEFAULT_INBOX_ISSUE_COLUMNS);
|
||||
});
|
||||
|
||||
it("normalizes saved issue columns to valid values in canonical order", () => {
|
||||
saveInboxIssueColumns(["labels", "updated", "status", "workspace", "labels", "assignee"]);
|
||||
|
||||
expect(loadInboxIssueColumns()).toEqual(["status", "assignee", "workspace", "labels", "updated"]);
|
||||
expect(normalizeInboxIssueColumns(["project", "workspace", "wat", "id"])).toEqual(["id", "project", "workspace"]);
|
||||
});
|
||||
|
||||
it("hides the workspace column option unless isolated workspaces are enabled", () => {
|
||||
expect(getAvailableInboxIssueColumns(false)).toEqual(["status", "id", "assignee", "project", "labels", "updated"]);
|
||||
expect(getAvailableInboxIssueColumns(true)).toEqual([
|
||||
"status",
|
||||
"id",
|
||||
"assignee",
|
||||
"project",
|
||||
"workspace",
|
||||
"labels",
|
||||
"updated",
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows hiding every optional issue column down to the title-only view", () => {
|
||||
saveInboxIssueColumns([]);
|
||||
expect(loadInboxIssueColumns()).toEqual([]);
|
||||
});
|
||||
|
||||
it("shows explicit workspace names but leaves the default workspace blank", () => {
|
||||
const issue = makeIssue("1", true);
|
||||
issue.projectId = "project-1";
|
||||
issue.projectWorkspaceId = "project-workspace-1";
|
||||
issue.executionWorkspaceId = "execution-workspace-1";
|
||||
|
||||
const executionWorkspace = makeExecutionWorkspace();
|
||||
const defaultWorkspace = makeProjectWorkspace();
|
||||
const secondaryWorkspace = makeProjectWorkspace({
|
||||
id: "project-workspace-2",
|
||||
name: "Secondary workspace",
|
||||
isPrimary: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveIssueWorkspaceName(issue, {
|
||||
executionWorkspaceById: new Map([[executionWorkspace.id, executionWorkspace]]),
|
||||
projectWorkspaceById: new Map([
|
||||
[defaultWorkspace.id, defaultWorkspace],
|
||||
[secondaryWorkspace.id, secondaryWorkspace],
|
||||
]),
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
||||
}),
|
||||
).toBe("PAP-1 branch");
|
||||
|
||||
issue.executionWorkspaceId = null;
|
||||
expect(
|
||||
resolveIssueWorkspaceName(issue, {
|
||||
projectWorkspaceById: new Map([
|
||||
[defaultWorkspace.id, defaultWorkspace],
|
||||
[secondaryWorkspace.id, secondaryWorkspace],
|
||||
]),
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
issue.projectWorkspaceId = secondaryWorkspace.id;
|
||||
expect(
|
||||
resolveIssueWorkspaceName(issue, {
|
||||
projectWorkspaceById: new Map([
|
||||
[defaultWorkspace.id, defaultWorkspace],
|
||||
[secondaryWorkspace.id, secondaryWorkspace],
|
||||
]),
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
||||
}),
|
||||
).toBe("Secondary workspace");
|
||||
|
||||
issue.projectWorkspaceId = null;
|
||||
expect(
|
||||
resolveIssueWorkspaceName(issue, {
|
||||
projectWorkspaceById: new Map([
|
||||
[defaultWorkspace.id, defaultWorkspace],
|
||||
[secondaryWorkspace.id, secondaryWorkspace],
|
||||
]),
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
issue.executionWorkspaceId = "execution-workspace-shared-default";
|
||||
issue.projectWorkspaceId = defaultWorkspace.id;
|
||||
expect(
|
||||
resolveIssueWorkspaceName(issue, {
|
||||
executionWorkspaceById: new Map([[
|
||||
issue.executionWorkspaceId,
|
||||
makeExecutionWorkspace({
|
||||
id: issue.executionWorkspaceId,
|
||||
mode: "shared_workspace",
|
||||
strategyType: "project_primary",
|
||||
projectWorkspaceId: defaultWorkspace.id,
|
||||
name: "PAP-1067",
|
||||
}),
|
||||
]]),
|
||||
projectWorkspaceById: new Map([
|
||||
[defaultWorkspace.id, defaultWorkspace],
|
||||
[secondaryWorkspace.id, secondaryWorkspace],
|
||||
]),
|
||||
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("maps legacy new-tab storage to mine", () => {
|
||||
localStorage.setItem("paperclip:inbox:last-tab", "new");
|
||||
expect(loadLastInboxTab()).toBe("mine");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import type {
|
||||
Approval,
|
||||
DashboardSummary,
|
||||
HeartbeatRun,
|
||||
Issue,
|
||||
JoinRequest,
|
||||
} from "@paperclipai/shared";
|
||||
import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
|
||||
|
||||
export const RECENT_ISSUES_LIMIT = 100;
|
||||
export const FAILED_RUN_STATUSES = new Set(["failed", "timed_out"]);
|
||||
|
|
@ -12,8 +6,12 @@ export const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending", "revision_reques
|
|||
export const DISMISSED_KEY = "paperclip:inbox:dismissed";
|
||||
export const READ_ITEMS_KEY = "paperclip:inbox:read-items";
|
||||
export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab";
|
||||
export const INBOX_ISSUE_COLUMNS_KEY = "paperclip:inbox:issue-columns";
|
||||
export type InboxTab = "mine" | "recent" | "unread" | "all";
|
||||
export type InboxApprovalFilter = "all" | "actionable" | "resolved";
|
||||
export const inboxIssueColumns = ["status", "id", "assignee", "project", "workspace", "labels", "updated"] as const;
|
||||
export type InboxIssueColumn = (typeof inboxIssueColumns)[number];
|
||||
export const DEFAULT_INBOX_ISSUE_COLUMNS: InboxIssueColumn[] = ["status", "id", "updated"];
|
||||
export type InboxWorkItem =
|
||||
| {
|
||||
kind: "issue";
|
||||
|
|
@ -79,6 +77,80 @@ export function saveReadInboxItems(ids: Set<string>) {
|
|||
}
|
||||
}
|
||||
|
||||
export function normalizeInboxIssueColumns(columns: Iterable<string | InboxIssueColumn>): InboxIssueColumn[] {
|
||||
const selected = new Set(columns);
|
||||
return inboxIssueColumns.filter((column) => selected.has(column));
|
||||
}
|
||||
|
||||
export function getAvailableInboxIssueColumns(enableWorkspaceColumn: boolean): InboxIssueColumn[] {
|
||||
if (enableWorkspaceColumn) return [...inboxIssueColumns];
|
||||
return inboxIssueColumns.filter((column) => column !== "workspace");
|
||||
}
|
||||
|
||||
export function loadInboxIssueColumns(): InboxIssueColumn[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(INBOX_ISSUE_COLUMNS_KEY);
|
||||
if (raw === null) return DEFAULT_INBOX_ISSUE_COLUMNS;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return DEFAULT_INBOX_ISSUE_COLUMNS;
|
||||
return normalizeInboxIssueColumns(parsed);
|
||||
} catch {
|
||||
return DEFAULT_INBOX_ISSUE_COLUMNS;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveInboxIssueColumns(columns: InboxIssueColumn[]) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
INBOX_ISSUE_COLUMNS_KEY,
|
||||
JSON.stringify(normalizeInboxIssueColumns(columns)),
|
||||
);
|
||||
} catch {
|
||||
// Ignore localStorage failures.
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveIssueWorkspaceName(
|
||||
issue: Pick<Issue, "executionWorkspaceId" | "projectId" | "projectWorkspaceId">,
|
||||
{
|
||||
executionWorkspaceById,
|
||||
projectWorkspaceById,
|
||||
defaultProjectWorkspaceIdByProjectId,
|
||||
}: {
|
||||
executionWorkspaceById?: ReadonlyMap<string, {
|
||||
name: string;
|
||||
mode: "shared_workspace" | "isolated_workspace" | "operator_branch" | "adapter_managed" | "cloud_sandbox";
|
||||
projectWorkspaceId: string | null;
|
||||
}>;
|
||||
projectWorkspaceById?: ReadonlyMap<string, { name: string }>;
|
||||
defaultProjectWorkspaceIdByProjectId?: ReadonlyMap<string, string>;
|
||||
},
|
||||
): string | null {
|
||||
const defaultProjectWorkspaceId = issue.projectId
|
||||
? defaultProjectWorkspaceIdByProjectId?.get(issue.projectId) ?? null
|
||||
: null;
|
||||
|
||||
if (issue.executionWorkspaceId) {
|
||||
const executionWorkspace = executionWorkspaceById?.get(issue.executionWorkspaceId) ?? null;
|
||||
const linkedProjectWorkspaceId =
|
||||
executionWorkspace?.projectWorkspaceId ?? issue.projectWorkspaceId ?? null;
|
||||
const isDefaultSharedExecutionWorkspace =
|
||||
executionWorkspace?.mode === "shared_workspace" && linkedProjectWorkspaceId === defaultProjectWorkspaceId;
|
||||
if (isDefaultSharedExecutionWorkspace) return null;
|
||||
|
||||
const workspaceName = executionWorkspace?.name;
|
||||
if (workspaceName) return workspaceName;
|
||||
}
|
||||
|
||||
if (issue.projectWorkspaceId) {
|
||||
if (issue.projectWorkspaceId === defaultProjectWorkspaceId) return null;
|
||||
const workspaceName = projectWorkspaceById?.get(issue.projectWorkspaceId)?.name;
|
||||
if (workspaceName) return workspaceName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadLastInboxTab(): InboxTab {
|
||||
try {
|
||||
const raw = localStorage.getItem(INBOX_LAST_TAB_KEY);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ describe("keyboardShortcuts helpers", () => {
|
|||
|
||||
expect(hasBlockingShortcutDialog(root)).toBe(false);
|
||||
});
|
||||
|
||||
it("archives only the first clean y press", () => {
|
||||
const button = document.createElement("button");
|
||||
|
||||
|
|
|
|||
|
|
@ -78,35 +78,37 @@ export function buildProjectWorkspaceSummaries(input: {
|
|||
})) continue;
|
||||
|
||||
const existing = summaries.get(`execution:${executionWorkspace.id}`);
|
||||
const nextIssues = [...(existing?.issues ?? []), issue].sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
);
|
||||
const nextIssues = existing?.issues ?? [];
|
||||
nextIssues.push(issue);
|
||||
|
||||
summaries.set(`execution:${executionWorkspace.id}`, {
|
||||
key: `execution:${executionWorkspace.id}`,
|
||||
kind: "execution_workspace",
|
||||
workspaceId: executionWorkspace.id,
|
||||
workspaceName: executionWorkspace.name,
|
||||
cwd: executionWorkspace.cwd ?? null,
|
||||
branchName: executionWorkspace.branchName ?? executionWorkspace.baseRef ?? null,
|
||||
lastUpdatedAt: maxDate(
|
||||
existing?.lastUpdatedAt,
|
||||
executionWorkspace.lastUsedAt,
|
||||
executionWorkspace.updatedAt,
|
||||
issue.updatedAt,
|
||||
),
|
||||
projectWorkspaceId: executionWorkspace.projectWorkspaceId ?? issue.projectWorkspaceId ?? null,
|
||||
executionWorkspaceId: executionWorkspace.id,
|
||||
executionWorkspaceStatus: executionWorkspace.status,
|
||||
serviceCount: executionWorkspace.runtimeServices?.length ?? 0,
|
||||
runningServiceCount: executionWorkspace.runtimeServices?.filter((service) => service.status === "running").length ?? 0,
|
||||
primaryServiceUrl: executionWorkspace.runtimeServices?.find((service) => service.url)?.url ?? null,
|
||||
hasRuntimeConfig: Boolean(
|
||||
executionWorkspace.config?.workspaceRuntime
|
||||
?? projectWorkspacesById.get(executionWorkspace.projectWorkspaceId ?? issue.projectWorkspaceId ?? "")?.runtimeConfig?.workspaceRuntime,
|
||||
),
|
||||
issues: nextIssues,
|
||||
});
|
||||
if (!existing) {
|
||||
summaries.set(`execution:${executionWorkspace.id}`, {
|
||||
key: `execution:${executionWorkspace.id}`,
|
||||
kind: "execution_workspace",
|
||||
workspaceId: executionWorkspace.id,
|
||||
workspaceName: executionWorkspace.name,
|
||||
cwd: executionWorkspace.cwd ?? null,
|
||||
branchName: executionWorkspace.branchName ?? executionWorkspace.baseRef ?? null,
|
||||
lastUpdatedAt: maxDate(
|
||||
executionWorkspace.lastUsedAt,
|
||||
executionWorkspace.updatedAt,
|
||||
issue.updatedAt,
|
||||
),
|
||||
projectWorkspaceId: executionWorkspace.projectWorkspaceId ?? issue.projectWorkspaceId ?? null,
|
||||
executionWorkspaceId: executionWorkspace.id,
|
||||
executionWorkspaceStatus: executionWorkspace.status,
|
||||
serviceCount: executionWorkspace.runtimeServices?.length ?? 0,
|
||||
runningServiceCount: executionWorkspace.runtimeServices?.filter((service) => service.status === "running").length ?? 0,
|
||||
primaryServiceUrl: executionWorkspace.runtimeServices?.find((service) => service.url)?.url ?? null,
|
||||
hasRuntimeConfig: Boolean(
|
||||
executionWorkspace.config?.workspaceRuntime
|
||||
?? projectWorkspacesById.get(executionWorkspace.projectWorkspaceId ?? issue.projectWorkspaceId ?? "")?.runtimeConfig?.workspaceRuntime,
|
||||
),
|
||||
issues: nextIssues,
|
||||
});
|
||||
} else {
|
||||
existing.lastUpdatedAt = maxDate(existing.lastUpdatedAt, issue.updatedAt);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -115,27 +117,30 @@ export function buildProjectWorkspaceSummaries(input: {
|
|||
if (!projectWorkspace) continue;
|
||||
|
||||
const existing = summaries.get(`project:${projectWorkspace.id}`);
|
||||
const nextIssues = [...(existing?.issues ?? []), issue].sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
);
|
||||
const nextIssues = existing?.issues ?? [];
|
||||
nextIssues.push(issue);
|
||||
|
||||
summaries.set(`project:${projectWorkspace.id}`, {
|
||||
key: `project:${projectWorkspace.id}`,
|
||||
kind: "project_workspace",
|
||||
workspaceId: projectWorkspace.id,
|
||||
workspaceName: projectWorkspace.name,
|
||||
cwd: projectWorkspace.cwd ?? null,
|
||||
branchName: projectWorkspace.repoRef ?? projectWorkspace.defaultRef ?? null,
|
||||
lastUpdatedAt: maxDate(existing?.lastUpdatedAt, projectWorkspace.updatedAt, issue.updatedAt),
|
||||
projectWorkspaceId: projectWorkspace.id,
|
||||
executionWorkspaceId: null,
|
||||
executionWorkspaceStatus: null,
|
||||
serviceCount: projectWorkspace.runtimeServices?.length ?? 0,
|
||||
runningServiceCount: projectWorkspace.runtimeServices?.filter((service) => service.status === "running").length ?? 0,
|
||||
primaryServiceUrl: projectWorkspace.runtimeServices?.find((service) => service.url)?.url ?? null,
|
||||
hasRuntimeConfig: Boolean(projectWorkspace.runtimeConfig?.workspaceRuntime),
|
||||
issues: nextIssues,
|
||||
});
|
||||
if (!existing) {
|
||||
summaries.set(`project:${projectWorkspace.id}`, {
|
||||
key: `project:${projectWorkspace.id}`,
|
||||
kind: "project_workspace",
|
||||
workspaceId: projectWorkspace.id,
|
||||
workspaceName: projectWorkspace.name,
|
||||
cwd: projectWorkspace.cwd ?? null,
|
||||
branchName: projectWorkspace.repoRef ?? projectWorkspace.defaultRef ?? null,
|
||||
lastUpdatedAt: maxDate(projectWorkspace.updatedAt, issue.updatedAt),
|
||||
projectWorkspaceId: projectWorkspace.id,
|
||||
executionWorkspaceId: null,
|
||||
executionWorkspaceStatus: null,
|
||||
serviceCount: projectWorkspace.runtimeServices?.length ?? 0,
|
||||
runningServiceCount: projectWorkspace.runtimeServices?.filter((service) => service.status === "running").length ?? 0,
|
||||
primaryServiceUrl: projectWorkspace.runtimeServices?.find((service) => service.url)?.url ?? null,
|
||||
hasRuntimeConfig: Boolean(projectWorkspace.runtimeConfig?.workspaceRuntime),
|
||||
issues: nextIssues,
|
||||
});
|
||||
} else {
|
||||
existing.lastUpdatedAt = maxDate(existing.lastUpdatedAt, issue.updatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
for (const projectWorkspace of input.project.workspaces) {
|
||||
|
|
@ -165,8 +170,17 @@ export function buildProjectWorkspaceSummaries(input: {
|
|||
});
|
||||
}
|
||||
|
||||
return [...summaries.values()].sort((a, b) => {
|
||||
const result = [...summaries.values()];
|
||||
// Sort issues within each summary once (instead of on every insertion)
|
||||
const issueTime = (issue: Issue) => new Date(issue.updatedAt).getTime();
|
||||
for (const summary of result) {
|
||||
if (summary.issues.length > 1) {
|
||||
summary.issues.sort((a, b) => issueTime(b) - issueTime(a));
|
||||
}
|
||||
}
|
||||
result.sort((a, b) => {
|
||||
const diff = b.lastUpdatedAt.getTime() - a.lastUpdatedAt.getTime();
|
||||
return diff !== 0 ? diff : a.workspaceName.localeCompare(b.workspaceName);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue