mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix: align Inbox notification ages and controls (#156560)
This commit is contained in:
parent
3b77f9653c
commit
318fc3c41f
9 changed files with 343 additions and 115 deletions
39
ui/src/components/relative-time.ts
Normal file
39
ui/src/components/relative-time.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion";
|
||||
import { html, nothing } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { formatDateTimeMs, formatRelativeTimestamp } from "../lib/format.ts";
|
||||
import { OpenClawLightDomElement } from "../lit/openclaw-element.ts";
|
||||
import { PollController } from "../lit/poll-controller.ts";
|
||||
|
||||
class RelativeTime extends OpenClawLightDomElement {
|
||||
@property({ attribute: false }) timestampMs: number | null = null;
|
||||
|
||||
private readonly polling = new PollController(
|
||||
this,
|
||||
60_000,
|
||||
() => this.requestUpdate(),
|
||||
false,
|
||||
"visible",
|
||||
);
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.polling.start();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const timestamp = asDateTimestampMs(this.timestampMs);
|
||||
return timestamp === undefined
|
||||
? nothing
|
||||
: html`<time
|
||||
datetime=${new Date(timestamp).toISOString()}
|
||||
title=${formatDateTimeMs(timestamp)}
|
||||
>${formatRelativeTimestamp(timestamp)}</time
|
||||
>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get("openclaw-relative-time")) {
|
||||
customElements.define("openclaw-relative-time", RelativeTime);
|
||||
}
|
||||
|
|
@ -54,6 +54,14 @@ describe("renderSidebarIssueItem", () => {
|
|||
});
|
||||
|
||||
describe("renderSidebarMentionItem", () => {
|
||||
beforeEach(() => {
|
||||
document.body.append(container);
|
||||
});
|
||||
afterEach(() => {
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const mention: MentionInboxItem = {
|
||||
id: "mention-riley",
|
||||
senderProfileId: "profile-riley",
|
||||
|
|
@ -85,10 +93,28 @@ describe("renderSidebarMentionItem", () => {
|
|||
return params;
|
||||
}
|
||||
|
||||
it("opens the linked session without dismissing the mention", () => {
|
||||
it("expands the thread-titled mention and opens its session without dismissing it", async () => {
|
||||
vi.useFakeTimers({ now: mention.createdAt + 5 * 60_000 });
|
||||
const { onNavigate, onDismiss } = renderMention();
|
||||
const open = container.querySelector<HTMLAnchorElement>("a[data-issue-row-focus]")!;
|
||||
await container.querySelector<HTMLElement & { updateComplete: Promise<boolean> }>(
|
||||
"openclaw-relative-time",
|
||||
)?.updateComplete;
|
||||
const details = container.querySelector("details")!;
|
||||
const summary = details.querySelector("summary")!;
|
||||
const title = summary.querySelector(".sidebar-issues-panel__entity")!;
|
||||
expect(title.textContent).toBe("Release notes");
|
||||
expect(title.nextElementSibling?.textContent?.replace(/\s+/g, " ").trim()).toBe(
|
||||
"Riley mentioned you · 5m ago",
|
||||
);
|
||||
expect(summary.querySelector(".sidebar-issues-panel__chevron svg")).not.toBeNull();
|
||||
expect(container.querySelectorAll("[data-issue-row-focus]")).toHaveLength(1);
|
||||
expect(summary.hasAttribute("data-issue-row-focus")).toBe(true);
|
||||
expect(details.open).toBe(false);
|
||||
summary.click();
|
||||
expect(details.open).toBe(true);
|
||||
const open = container.querySelector<HTMLAnchorElement>("a[href]")!;
|
||||
expect(open.getAttribute("href")).toBe(pathname);
|
||||
expect(open.closest(".sidebar-issues-panel__body")).not.toBeNull();
|
||||
|
||||
let nativeNavigationPreserved = false;
|
||||
open.addEventListener(
|
||||
|
|
@ -109,11 +135,29 @@ describe("renderSidebarMentionItem", () => {
|
|||
expect(onNavigate).toHaveBeenCalledExactlyOnceWith("chat", navigation);
|
||||
expect(onDismiss).not.toHaveBeenCalled();
|
||||
|
||||
container.querySelector<HTMLButtonElement>("[data-mention-id] button")!.click();
|
||||
summary.click();
|
||||
const dismiss = summary.querySelector<HTMLButtonElement>("button")!;
|
||||
expect(dismiss.getAttribute("aria-label")).toBe("Dismiss Release notes");
|
||||
expect(dismiss.textContent?.trim()).toBe("");
|
||||
dismiss.click();
|
||||
expect(details.open).toBe(false);
|
||||
expect(onDismiss).toHaveBeenCalledOnce();
|
||||
expect(onNavigate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves expansion for the same mention but collapses a replacement mention", () => {
|
||||
renderMention();
|
||||
container.querySelector("summary")!.click();
|
||||
expect(container.querySelector("details")?.open).toBe(true);
|
||||
|
||||
renderMention({ mention: { ...mention, excerpt: "Updated release notes" } });
|
||||
expect(container.querySelector("details")?.open).toBe(true);
|
||||
|
||||
renderMention({ mention: { ...mention, id: "mention-next", sessionTitle: "Next release" } });
|
||||
expect(container.querySelector("details")?.open).toBe(false);
|
||||
expect(container.querySelector("summary")?.textContent).toContain("Next release");
|
||||
});
|
||||
|
||||
it("renders the message excerpt as text rather than HTML or Markdown", () => {
|
||||
const excerpt = '<img src="about:blank" onerror="alert(1)"> & **release notes**';
|
||||
renderMention({ mention: { ...mention, excerpt } });
|
||||
|
|
@ -121,6 +165,7 @@ describe("renderSidebarMentionItem", () => {
|
|||
const renderedExcerpt = container.querySelector(".sidebar-mention-row__excerpt")!;
|
||||
expect(renderedExcerpt.textContent).toBe(excerpt);
|
||||
expect(renderedExcerpt.children).toHaveLength(0);
|
||||
expect(renderedExcerpt.closest(".sidebar-issues-panel__body")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("disables repeated dismissal while leaving the session link usable", () => {
|
||||
|
|
@ -130,7 +175,8 @@ describe("renderSidebarMentionItem", () => {
|
|||
dismiss.click();
|
||||
expect(onDismiss).not.toHaveBeenCalled();
|
||||
|
||||
container.querySelector<HTMLAnchorElement>("a[data-issue-row-focus]")!.click();
|
||||
container.querySelector("summary")!.click();
|
||||
container.querySelector<HTMLAnchorElement>("a[href]")!.click();
|
||||
expect(onNavigate).toHaveBeenCalledExactlyOnceWith("chat", navigation);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { html, nothing } from "lit";
|
||||
import { keyed } from "lit/directives/keyed.js";
|
||||
import type { MentionInboxItem } from "../../../packages/gateway-protocol/src/index.js";
|
||||
import type { NavigationRouteId } from "../app-navigation.ts";
|
||||
import { pathForRoute } from "../app-route-paths.ts";
|
||||
|
|
@ -8,7 +9,6 @@ import type { ExecApprovalDecision, ExecApprovalRequest } from "../app/exec-appr
|
|||
import type { UpdateProgress } from "../app/update-confirmation.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { registerSidebarAttentionEnglish } from "../i18n/locales/en-sidebar-attention.ts";
|
||||
import { formatDateTimeMs, formatRelativeTimestamp } from "../lib/format.ts";
|
||||
import { canCallGatewayMethod } from "../lib/gateway-methods.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import type { PresenceViewer } from "../lib/presence-users.ts";
|
||||
|
|
@ -17,6 +17,7 @@ import { areUiSessionKeysEquivalent } from "../lib/sessions/session-key.ts";
|
|||
import { renderSidebarApprovalRow } from "./exec-approval-card.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
import type { SidebarAttentionItem } from "./sidebar-attention-entries.ts";
|
||||
import { renderSidebarNotificationCard } from "./sidebar-notification-card.ts";
|
||||
import "./sidebar-update-card.ts";
|
||||
import "./viewer-facepile.ts";
|
||||
|
||||
|
|
@ -79,57 +80,42 @@ export function renderSidebarMentionItem(params: {
|
|||
data-mention-id=${mention.id}
|
||||
aria-label=${label}
|
||||
>
|
||||
<div class="sidebar-issues-panel__summary sidebar-mention-row__summary">
|
||||
<span class="sidebar-mention-row__avatar" aria-hidden="true">
|
||||
<openclaw-viewer-avatar
|
||||
${keyed(
|
||||
mention.id,
|
||||
renderSidebarNotificationCard({
|
||||
title: mention.sessionTitle,
|
||||
detail: label,
|
||||
timestampMs: mention.createdAt,
|
||||
icon: html`<openclaw-viewer-avatar
|
||||
.user=${sender}
|
||||
.markAsViewer=${false}
|
||||
variant="footer"
|
||||
></openclaw-viewer-avatar>
|
||||
</span>
|
||||
<div class="sidebar-issues-panel__content">
|
||||
<div class="sidebar-mention-row__header">
|
||||
<span class="sidebar-issues-panel__entity" title=${label}>${label}</span>
|
||||
<time
|
||||
class="sidebar-mention-row__age"
|
||||
datetime=${new Date(mention.createdAt).toISOString()}
|
||||
title=${formatDateTimeMs(mention.createdAt)}
|
||||
>${formatRelativeTimestamp(mention.createdAt)}</time
|
||||
>
|
||||
</div>
|
||||
<span class="sidebar-issues-panel__state" title=${mention.sessionTitle}
|
||||
>${mention.sessionTitle}</span
|
||||
>
|
||||
${
|
||||
mention.excerpt
|
||||
? html`<p class="sidebar-mention-row__excerpt">${mention.excerpt}</p>`
|
||||
: nothing
|
||||
}
|
||||
<div class="sidebar-issues-panel__actions sidebar-mention-row__actions">
|
||||
<a
|
||||
class="sidebar-issues-panel__action sidebar-issues-panel__action--primary"
|
||||
href=${target.href}
|
||||
data-issue-row-focus
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
params.onNavigate("chat", target.options);
|
||||
}}
|
||||
>${t("attention.mentions.open")}</a
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-issues-panel__action"
|
||||
?disabled=${params.dismissing}
|
||||
@click=${params.onDismiss}
|
||||
>
|
||||
${t(params.dismissing ? "attention.mentions.dismissing" : "attention.mentions.dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
></openclaw-viewer-avatar>`,
|
||||
onDismiss: params.onDismiss,
|
||||
dismissing: params.dismissing,
|
||||
body: html`
|
||||
${
|
||||
mention.excerpt
|
||||
? html`<p class="sidebar-mention-row__excerpt">${mention.excerpt}</p>`
|
||||
: nothing
|
||||
}
|
||||
<div class="sidebar-issues-panel__actions sidebar-mention-row__actions">
|
||||
<a
|
||||
class="sidebar-issues-panel__action sidebar-issues-panel__action--primary"
|
||||
href=${target.href}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
params.onNavigate("chat", target.options);
|
||||
}}
|
||||
>${t("attention.mentions.open")}</a
|
||||
>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
)}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
|
|
|
|||
68
ui/src/components/sidebar-notification-card.ts
Normal file
68
ui/src/components/sidebar-notification-card.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
import "./relative-time.ts";
|
||||
|
||||
export function renderSidebarNotificationCard(props: {
|
||||
title: string;
|
||||
detail: string;
|
||||
timestampMs?: number | null;
|
||||
icon: TemplateResult;
|
||||
severity?: "error" | "warning";
|
||||
critical?: boolean;
|
||||
dismissing?: boolean;
|
||||
onDismiss?: () => void;
|
||||
body: TemplateResult | typeof nothing;
|
||||
bodyClass?: string;
|
||||
}) {
|
||||
const dismissLabel = t("attention.dismissItem", { item: props.title });
|
||||
return html`<details
|
||||
class="sidebar-issues-panel__details ${props.severity ? `sidebar-issues-panel__details--${props.severity}` : ""}"
|
||||
>
|
||||
<summary class="sidebar-issues-panel__summary" data-issue-row-focus>
|
||||
<span
|
||||
class="sidebar-issues-panel__icon ${props.critical ? "sidebar-issues-panel__icon--critical" : ""}"
|
||||
aria-hidden="true"
|
||||
>${props.icon}</span
|
||||
>
|
||||
<span class="sidebar-issues-panel__content">
|
||||
<span class="sidebar-issues-panel__entity" title=${props.title}>${props.title}</span>
|
||||
<span class="sidebar-issues-panel__state-row sidebar-issues-panel__notification-meta">
|
||||
<span class="sidebar-issues-panel__state" title=${props.detail}>${props.detail}</span>
|
||||
${
|
||||
props.timestampMs == null
|
||||
? nothing
|
||||
: html`
|
||||
<span aria-hidden="true">·</span>
|
||||
<openclaw-relative-time
|
||||
class="sidebar-issues-panel__age"
|
||||
.timestampMs=${props.timestampMs}
|
||||
></openclaw-relative-time>
|
||||
`
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
${
|
||||
props.onDismiss
|
||||
? html`<button
|
||||
type="button"
|
||||
class="sidebar-issues-panel__dismiss"
|
||||
aria-label=${dismissLabel}
|
||||
aria-busy=${props.dismissing ? "true" : nothing}
|
||||
title=${props.dismissing ? t("attention.mentions.dismissing") : dismissLabel}
|
||||
?disabled=${props.dismissing}
|
||||
@click=${(event: Event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
props.onDismiss?.();
|
||||
}}
|
||||
>
|
||||
${icons.x}
|
||||
</button>`
|
||||
: nothing
|
||||
}
|
||||
<span class="sidebar-issues-panel__chevron" aria-hidden="true">${icons.chevronRight}</span>
|
||||
</summary>
|
||||
<div class="sidebar-issues-panel__body ${props.bodyClass ?? ""}">${props.body}</div>
|
||||
</details>`;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import type { UpdateRunRecord } from "../../../src/infra/update-run-record.ts";
|
||||
import type { UpdateAvailable, UpdateScheduleState } from "../api/types.ts";
|
||||
import type { ApplicationStatusBanner } from "../app/update-overlay-helpers.ts";
|
||||
import { formatDateTimeMs } from "../lib/format.ts";
|
||||
import { createUpdateRunFixture } from "../test-helpers/update-run.ts";
|
||||
import "./sidebar-update-card.ts";
|
||||
|
||||
|
|
@ -52,8 +53,9 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
document.body.replaceChildren();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
if (originalWebkit) {
|
||||
Object.defineProperty(window, "webkit", originalWebkit);
|
||||
} else {
|
||||
|
|
@ -331,6 +333,7 @@ describe("SidebarUpdateCard", () => {
|
|||
expect(element.querySelector(".sidebar-issues-panel__entity")?.textContent).toBe(
|
||||
"Update available",
|
||||
);
|
||||
expect(element.querySelector("summary time")).toBeNull();
|
||||
expect(element.querySelector(".sidebar-update-card__action")?.textContent).toContain(
|
||||
"Update Gateway",
|
||||
);
|
||||
|
|
@ -343,6 +346,87 @@ describe("SidebarUpdateCard", () => {
|
|||
expect(element.querySelector("details")?.open).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ state: "running", ageMinutes: 30 },
|
||||
{ state: "succeeded", ageMinutes: 5 },
|
||||
{ state: "failed", ageMinutes: 5 },
|
||||
{ state: "campaign", ageMinutes: 20 },
|
||||
] as const)(
|
||||
"shows the recorded $state age beside the compact row's detail",
|
||||
async ({ state, ageMinutes }) => {
|
||||
const now = Date.parse("2026-09-23T12:00:00Z");
|
||||
vi.useFakeTimers({ now });
|
||||
const element = await mount(null);
|
||||
element.compact = true;
|
||||
if (state === "campaign") {
|
||||
element.updateSchedule = {
|
||||
channel: "stable",
|
||||
autoEnabled: true,
|
||||
target: { kind: "package", version: "2026.9.2" },
|
||||
campaign: {
|
||||
id: "campaign-age",
|
||||
state: "countdown",
|
||||
announcedAtMs: now - 20 * 60_000,
|
||||
updatedAtMs: now - 60_000,
|
||||
applyAtMs: now + 10 * 60_000,
|
||||
forceAtMs: now + 60 * 60_000,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
element.updateRun = createUpdateRunFixture({
|
||||
status: state,
|
||||
phase: state === "running" ? "staging" : "finished",
|
||||
createdAtMs: now - 30 * 60_000,
|
||||
updatedAtMs: now - 60_000,
|
||||
finishedAtMs: state === "running" ? null : now - 5 * 60_000,
|
||||
});
|
||||
}
|
||||
await element.updateComplete;
|
||||
await element.querySelector<HTMLElement & { updateComplete: Promise<boolean> }>(
|
||||
"openclaw-relative-time",
|
||||
)?.updateComplete;
|
||||
|
||||
const title = element.querySelector("summary .sidebar-issues-panel__entity");
|
||||
const meta = title?.nextElementSibling;
|
||||
const time = meta?.querySelector("time");
|
||||
const timestamp = now - ageMinutes * 60_000;
|
||||
expect(meta?.querySelector(".sidebar-issues-panel__state")).not.toBeNull();
|
||||
expect(time?.getAttribute("datetime")).toBe(new Date(timestamp).toISOString());
|
||||
expect(time?.getAttribute("title")).toBe(formatDateTimeMs(timestamp));
|
||||
expect(time?.textContent?.trim()).toBe(`${ageMinutes}m ago`);
|
||||
expect(element.querySelector("details")?.open).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("refreshes a completed update's age without new state and stops ticking on disconnect", async () => {
|
||||
const now = Date.parse("2026-09-23T12:00:00Z");
|
||||
vi.useFakeTimers({ now });
|
||||
vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible");
|
||||
const timersBefore = vi.getTimerCount();
|
||||
const element = await mount(null);
|
||||
element.compact = true;
|
||||
element.updateRun = createUpdateRunFixture({
|
||||
status: "succeeded",
|
||||
phase: "finished",
|
||||
createdAtMs: now - 30 * 60_000,
|
||||
updatedAtMs: now - 5 * 60_000,
|
||||
finishedAtMs: now - 5 * 60_000,
|
||||
});
|
||||
await element.updateComplete;
|
||||
await element.querySelector<HTMLElement & { updateComplete: Promise<boolean> }>(
|
||||
"openclaw-relative-time",
|
||||
)?.updateComplete;
|
||||
expect(element.querySelector("summary time")?.textContent?.trim()).toBe("5m ago");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(element.querySelector("summary time")?.textContent?.trim()).toBe("6m ago");
|
||||
|
||||
element.remove();
|
||||
expect(vi.getTimerCount()).toBe(timersBefore);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(element.querySelector("summary time")?.textContent?.trim()).toBe("6m ago");
|
||||
});
|
||||
|
||||
it("keeps an unauthorized update discoverable without allowing activation", async () => {
|
||||
const element = await mount(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { PollController } from "../lit/poll-controller.ts";
|
|||
import "../styles/sidebar-update-card.css";
|
||||
import { icons } from "./icons.ts";
|
||||
import { isUpdateRunAttentionVisible } from "./sidebar-attention-update.ts";
|
||||
import { renderSidebarNotificationCard } from "./sidebar-notification-card.ts";
|
||||
import "./tooltip.ts";
|
||||
import { renderUpdateGitRevisions } from "./update-git-revisions.ts";
|
||||
|
||||
|
|
@ -196,6 +197,10 @@ class SidebarUpdateCard extends OpenClawLightDomContentsElement {
|
|||
return {
|
||||
title: view.headline,
|
||||
detail: view.compactLabel,
|
||||
timestampMs:
|
||||
this.updateRun.status === "running"
|
||||
? this.updateRun.createdAtMs
|
||||
: this.updateRun.finishedAtMs,
|
||||
icon:
|
||||
this.updateRun.status === "running"
|
||||
? icons.refresh
|
||||
|
|
@ -217,6 +222,7 @@ class SidebarUpdateCard extends OpenClawLightDomContentsElement {
|
|||
const blocked = statusBanner && statusBanner.tone !== "info";
|
||||
const blockedReason = statusBanner?.text.trim() || t("updates.sidebar.blockedSummary");
|
||||
return {
|
||||
timestampMs: campaign?.announcedAtMs,
|
||||
detail: blocked
|
||||
? campaign?.state === "waiting-for-idle" && targetLabel
|
||||
? t("updates.sidebar.blockedWaiting", { target: targetLabel })
|
||||
|
|
@ -245,44 +251,12 @@ class SidebarUpdateCard extends OpenClawLightDomContentsElement {
|
|||
if (!summary) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<details
|
||||
class="sidebar-issues-panel__details sidebar-issues-panel__details--${summary.severity}"
|
||||
>
|
||||
<summary class="sidebar-issues-panel__summary" data-issue-row-focus>
|
||||
<span
|
||||
class="sidebar-issues-panel__icon ${
|
||||
summary.critical ? "sidebar-issues-panel__icon--critical" : ""
|
||||
}"
|
||||
aria-hidden="true"
|
||||
>${summary.icon}</span
|
||||
>
|
||||
<span class="sidebar-issues-panel__content">
|
||||
<span class="sidebar-issues-panel__entity" title=${summary.title}>${summary.title}</span>
|
||||
<span class="sidebar-issues-panel__state" title=${summary.detail}>${summary.detail}</span>
|
||||
</span>
|
||||
${
|
||||
this.onDismiss
|
||||
? html`<button
|
||||
type="button"
|
||||
class="sidebar-issues-panel__dismiss"
|
||||
aria-label=${t("attention.dismissItem", { item: summary.title })}
|
||||
title=${t("attention.dismissItem", { item: summary.title })}
|
||||
@click=${(event: Event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.onDismiss?.();
|
||||
}}
|
||||
>
|
||||
${icons.x}
|
||||
</button>`
|
||||
: nothing
|
||||
}
|
||||
<span class="sidebar-issues-panel__chevron" aria-hidden="true">${icons.chevronRight}</span>
|
||||
</summary>
|
||||
<div class="sidebar-issues-panel__body sidebar-update-issue__body">
|
||||
${this.renderCompactDetails()}
|
||||
</div>
|
||||
</details>`;
|
||||
return renderSidebarNotificationCard({
|
||||
...summary,
|
||||
onDismiss: this.onDismiss,
|
||||
body: this.renderCompactDetails(),
|
||||
bodyClass: "sidebar-update-issue__body",
|
||||
});
|
||||
}
|
||||
|
||||
private renderCompactDetails() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
installMockGateway,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
import { compactCronJobFixture } from "../test-helpers/cron.ts";
|
||||
import { createUpdateRunFixture } from "../test-helpers/update-run.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({ name: "Inbox dismissal clarity" });
|
||||
|
|
@ -29,12 +30,24 @@ suite.define(() => {
|
|||
await suite.withPage(
|
||||
{ viewport: { width, height: 900 }, colorScheme: "dark", locale: "en-US" },
|
||||
async ({ page }) => {
|
||||
const notificationAt = Date.now() - 5 * 60_000;
|
||||
const gateway = await installMockGateway(page, {
|
||||
presenceUsers: [
|
||||
{ self: true, id: "alex", name: "Alex", identity: { type: "profile", id: "alex" } },
|
||||
],
|
||||
featureMethods: [...defaultControlUiFeatureMethods, "mentions.list", "mentions.dismiss"],
|
||||
methodResponses: {
|
||||
"update.status": {
|
||||
activeRun: null,
|
||||
lastRun: createUpdateRunFixture({
|
||||
status: "succeeded",
|
||||
phase: "finished",
|
||||
createdAtMs: notificationAt - 60_000,
|
||||
updatedAtMs: notificationAt,
|
||||
finishedAtMs: notificationAt,
|
||||
after: { version: "2026.9.2" },
|
||||
}),
|
||||
},
|
||||
"models.authStatus": { providers: [], ts: 1 },
|
||||
"cron.list": {
|
||||
jobs: [compactCronJobFixture(failedJob)],
|
||||
|
|
@ -57,7 +70,7 @@ suite.define(() => {
|
|||
agentId: "main",
|
||||
sessionTitle: "Release review",
|
||||
messageId: "message-review",
|
||||
createdAt: Date.now(),
|
||||
createdAt: notificationAt,
|
||||
expiresAt: Date.now() + 60_000,
|
||||
excerpt: "Can you review the release notes?",
|
||||
},
|
||||
|
|
@ -81,10 +94,38 @@ suite.define(() => {
|
|||
const panel = page.locator("#sidebar-issues-panel");
|
||||
const automation = panel.locator('[data-attention-kind="cronFailed"]');
|
||||
const mention = panel.locator('[data-mention-id="mention-review"]');
|
||||
const update = panel.locator('[data-attention-kind="updateAvailable"]');
|
||||
const approval = panel.locator('[data-approval-id="approval-pending"]');
|
||||
await automation.waitFor();
|
||||
await mention.waitFor();
|
||||
await update.waitFor();
|
||||
await approval.waitFor();
|
||||
expect(await mention.locator("summary .sidebar-issues-panel__entity").textContent()).toBe(
|
||||
"Release review",
|
||||
);
|
||||
expect(await mention.locator("summary .sidebar-issues-panel__state").textContent()).toBe(
|
||||
"Riley mentioned you",
|
||||
);
|
||||
for (const notification of [mention, update]) {
|
||||
const summary = notification.locator("summary");
|
||||
const body = notification.locator(".sidebar-issues-panel__body");
|
||||
expect(await notification.locator("details").getAttribute("open")).toBeNull();
|
||||
expect(await summary.locator(".sidebar-issues-panel__state-row time").isVisible()).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await summary.locator(".sidebar-issues-panel__chevron").isVisible()).toBe(true);
|
||||
expect(await body.isVisible()).toBe(false);
|
||||
await summary.click();
|
||||
await body.waitFor({ state: "visible" });
|
||||
if (notification === mention) {
|
||||
expect(await body.getByRole("link", { name: "Open", exact: true }).isVisible()).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await body.textContent()).toContain("Can you review the release notes?");
|
||||
}
|
||||
await summary.click();
|
||||
await body.waitFor({ state: "hidden" });
|
||||
}
|
||||
if (process.env.OPENCLAW_CAPTURE_UI_PROOF === "1") {
|
||||
await page.screenshot({
|
||||
path: path.join(suite.artifactDir, `inbox-${width}.png`),
|
||||
|
|
@ -98,10 +139,15 @@ suite.define(() => {
|
|||
).toBe("");
|
||||
expect(
|
||||
(
|
||||
await mention.getByRole("button", { name: "Dismiss", exact: true }).textContent()
|
||||
await mention
|
||||
.getByRole("button", { name: "Dismiss Release review", exact: true })
|
||||
.textContent()
|
||||
)?.trim(),
|
||||
).toBe("Dismiss");
|
||||
).toBe("");
|
||||
expect(await panel.locator(".sidebar-issues-panel__dismiss-help").count()).toBe(0);
|
||||
await update.getByRole("button", { name: /^Dismiss .*OpenClaw updated/ }).click();
|
||||
await update.waitFor({ state: "hidden" });
|
||||
expect(await gateway.getRequests("update.run")).toHaveLength(0);
|
||||
|
||||
// A scoped bulk action must not acknowledge another tab's mention.
|
||||
await panel.getByRole("tab", { name: /Automations/ }).click();
|
||||
|
|
|
|||
|
|
@ -91,11 +91,13 @@ suite.define(() => {
|
|||
await panel.waitFor({ state: "hidden" });
|
||||
expect(await drawer.getAttribute("aria-hidden")).toBeNull();
|
||||
await page.locator(".sidebar-issues-button:visible").click();
|
||||
await panel.locator('[data-mention-id="mobile-mention"] summary').click();
|
||||
await panel.locator('[data-mention-id="mobile-mention"] a').click();
|
||||
await waitForControlUiRoute(page, { pathname: "/chat/main", routeId: "chat" });
|
||||
await expectClosed();
|
||||
// An Inbox link to the already-open session must close the drawer too.
|
||||
await openInbox();
|
||||
await panel.locator('[data-mention-id="mobile-mention"] summary').click();
|
||||
await panel.locator('[data-mention-id="mobile-mention"] a').click();
|
||||
await expectClosed();
|
||||
expect(await gateway.getRequests("mentions.dismiss")).toHaveLength(0);
|
||||
|
|
|
|||
|
|
@ -606,28 +606,11 @@ footer.sidebar-issues-panel__mentions-note {
|
|||
color: var(--accent-foreground);
|
||||
}
|
||||
|
||||
.sidebar-mention-row__summary {
|
||||
align-items: flex-start;
|
||||
.sidebar-issues-panel__notification-meta .sidebar-issues-panel__state {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.sidebar-mention-row__avatar {
|
||||
flex: none;
|
||||
margin-block-start: 2px;
|
||||
}
|
||||
|
||||
.sidebar-mention-row__header {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-mention-row__header .sidebar-issues-panel__entity {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.sidebar-mention-row__age {
|
||||
.sidebar-issues-panel__age {
|
||||
flex: none;
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue