fix(inbox): address Greptile review findings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
50e9f69010
commit
833842b391
8 changed files with 49 additions and 29 deletions
|
|
@ -143,7 +143,6 @@ export interface Issue {
|
|||
mentionedProjects?: Project[];
|
||||
myLastTouchAt?: Date | null;
|
||||
lastExternalCommentAt?: Date | null;
|
||||
lastActivityAt?: Date | null;
|
||||
isUnreadForMe?: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
|
|
|||
|
|
@ -1075,7 +1075,6 @@ export function agentRoutes(db: Db) {
|
|||
projectId: issue.projectId,
|
||||
goalId: issue.goalId,
|
||||
parentId: issue.parentId,
|
||||
lastActivityAt: (issue as typeof issue & { lastActivityAt?: Date | null }).lastActivityAt ?? issue.updatedAt,
|
||||
updatedAt: issue.updatedAt,
|
||||
activeRun: issue.activeRun,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { MobileBottomNav } from "./MobileBottomNav";
|
|||
import { WorktreeBanner } from "./WorktreeBanner";
|
||||
import { DevRestartBanner } from "./DevRestartBanner";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { GeneralSettingsProvider } from "../context/GeneralSettingsContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
|
|
@ -265,12 +266,13 @@ export function Layout() {
|
|||
}, [location.hash, location.pathname, location.search]);
|
||||
|
||||
return (
|
||||
<div
|
||||
<GeneralSettingsProvider value={{ keyboardShortcutsEnabled }}>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background text-foreground pt-[env(safe-area-inset-top)]",
|
||||
isMobile ? "min-h-dvh" : "flex h-dvh flex-col overflow-hidden",
|
||||
)}
|
||||
>
|
||||
>
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
|
|
@ -442,6 +444,7 @@ export function Layout() {
|
|||
<NewGoalDialog />
|
||||
<NewAgentDialog />
|
||||
<ToastViewport />
|
||||
</div>
|
||||
</div>
|
||||
</GeneralSettingsProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
28
ui/src/context/GeneralSettingsContext.tsx
Normal file
28
ui/src/context/GeneralSettingsContext.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export interface GeneralSettingsContextValue {
|
||||
keyboardShortcutsEnabled: boolean;
|
||||
}
|
||||
|
||||
const GeneralSettingsContext = createContext<GeneralSettingsContextValue>({
|
||||
keyboardShortcutsEnabled: false,
|
||||
});
|
||||
|
||||
export function GeneralSettingsProvider({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: GeneralSettingsContextValue;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<GeneralSettingsContext.Provider value={value}>
|
||||
{children}
|
||||
</GeneralSettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useGeneralSettings() {
|
||||
return useContext(GeneralSettingsContext);
|
||||
}
|
||||
|
|
@ -180,7 +180,6 @@ function makeIssue(id: string, isUnreadForMe: boolean): Issue {
|
|||
labelIds: [],
|
||||
myLastTouchAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
lastExternalCommentAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
lastActivityAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
isUnreadForMe,
|
||||
};
|
||||
}
|
||||
|
|
@ -358,10 +357,10 @@ describe("inbox helpers", () => {
|
|||
|
||||
it("mixes approvals into the inbox feed by most recent activity", () => {
|
||||
const newerIssue = makeIssue("1", true);
|
||||
newerIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
newerIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
|
||||
const olderIssue = makeIssue("2", false);
|
||||
olderIssue.lastActivityAt = new Date("2026-03-11T02:00:00.000Z");
|
||||
olderIssue.lastExternalCommentAt = new Date("2026-03-11T02:00:00.000Z");
|
||||
|
||||
const approval = makeApprovalWithTimestamps(
|
||||
"approval-between",
|
||||
|
|
@ -386,21 +385,19 @@ describe("inbox helpers", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("prefers canonical lastActivityAt over comment-only timestamps", () => {
|
||||
const activityIssue = makeIssue("1", true);
|
||||
activityIssue.lastExternalCommentAt = new Date("2026-03-11T01:00:00.000Z");
|
||||
activityIssue.lastActivityAt = new Date("2026-03-11T05:00:00.000Z");
|
||||
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 commentIssue = makeIssue("2", true);
|
||||
commentIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
commentIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
const olderIssue = makeIssue("2", true);
|
||||
olderIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
|
||||
expect(getRecentTouchedIssues([commentIssue, activityIssue]).map((issue) => issue.id)).toEqual(["1", "2"]);
|
||||
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.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
issue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
|
||||
const joinRequest = makeJoinRequest("join-1");
|
||||
joinRequest.createdAt = new Date("2026-03-11T03:00:00.000Z");
|
||||
|
|
@ -485,7 +482,7 @@ describe("inbox helpers", () => {
|
|||
it("limits recent touched issues before unread badge counting", () => {
|
||||
const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => {
|
||||
const issue = makeIssue(String(index + 1), index < 3);
|
||||
issue.lastActivityAt = new Date(Date.UTC(2026, 2, 31, 0, 0, 0, 0) - index * 60_000);
|
||||
issue.lastExternalCommentAt = new Date(Date.UTC(2026, 2, 31, 0, 0, 0, 0) - index * 60_000);
|
||||
return issue;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -217,9 +217,6 @@ export function normalizeTimestamp(value: string | Date | null | undefined): num
|
|||
}
|
||||
|
||||
export function issueLastActivityTimestamp(issue: Issue): number {
|
||||
const lastActivityAt = normalizeTimestamp(issue.lastActivityAt);
|
||||
if (lastActivityAt > 0) return lastActivityAt;
|
||||
|
||||
const lastExternalCommentAt = normalizeTimestamp(issue.lastExternalCommentAt);
|
||||
if (lastExternalCommentAt > 0) return lastExternalCommentAt;
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ function createIssue(overrides: Partial<Issue> = {}): Issue {
|
|||
labelIds: [],
|
||||
myLastTouchAt: null,
|
||||
lastExternalCommentAt: null,
|
||||
lastActivityAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||
isUnreadForMe: false,
|
||||
...overrides,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { instanceSettingsApi } from "../api/instanceSettings";
|
|||
import { projectsApi } from "../api/projects";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useGeneralSettings } from "../context/GeneralSettingsContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import {
|
||||
armIssueDetailInboxQuickArchive,
|
||||
|
|
@ -213,7 +214,7 @@ export function InboxIssueMetaLeading({
|
|||
}
|
||||
|
||||
function issueActivityText(issue: Issue): string {
|
||||
return `Updated ${timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt)}`;
|
||||
return `Updated ${timeAgo(issue.lastExternalCommentAt ?? issue.updatedAt)}`;
|
||||
}
|
||||
|
||||
function issueTrailingGridTemplate(columns: InboxIssueColumn[]): string {
|
||||
|
|
@ -245,7 +246,7 @@ export function InboxIssueTrailingColumns({
|
|||
assigneeName: string | null;
|
||||
currentUserId: string | null;
|
||||
}) {
|
||||
const activityText = timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt);
|
||||
const activityText = timeAgo(issue.lastExternalCommentAt ?? issue.updatedAt);
|
||||
const userLabel = formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User";
|
||||
|
||||
return (
|
||||
|
|
@ -791,10 +792,7 @@ export function Inbox() {
|
|||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const keyboardShortcutsEnabled = useQuery({
|
||||
queryKey: queryKeys.instance.generalSettings,
|
||||
queryFn: () => instanceSettingsApi.getGeneral(),
|
||||
}).data?.keyboardShortcuts === true;
|
||||
const { keyboardShortcutsEnabled } = useGeneralSettings();
|
||||
const { data: experimentalSettings } = useQuery({
|
||||
queryKey: queryKeys.instance.experimentalSettings,
|
||||
queryFn: () => instanceSettingsApi.getExperimental(),
|
||||
|
|
@ -1778,7 +1776,7 @@ export function Inbox() {
|
|||
<div key="today-divider" className="flex items-center gap-3 px-4 my-2">
|
||||
<div className="flex-1 border-t border-zinc-600" />
|
||||
<span className="shrink-0 text-[11px] font-medium uppercase tracking-wider text-zinc-500">
|
||||
Today
|
||||
Earlier
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue