fix(ui): align run history dropdown text (#127443)

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
ClawSweeper
2026-08-21 13:12:10 -07:00
committed by GitHub
parent 519877a317
commit 229ce32e32
6 changed files with 129 additions and 47 deletions
+1 -1
View File
@@ -4233,7 +4233,7 @@ ui/src/pages/config/view-schema.ts 2
ui/src/pages/config/view.ts 5
ui/src/pages/connection/connection-page.ts 1
ui/src/pages/connection/view.ts 3
ui/src/pages/cron/view-runs.ts 5
ui/src/pages/cron/view-runs.ts 3
ui/src/pages/cron/view.ts 9
ui/src/pages/custodian/custodian-session-store.ts 1
ui/src/pages/custodian/custodian-surface.ts 2
@@ -0,0 +1,77 @@
import { expect, it } from "vitest";
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
const suite = createControlUiE2eSuite({
name: "Control UI cron run filter typography E2E",
startServerBeforeBrowser: true,
unavailableMessage: (executablePath) =>
`Playwright Chromium is not installed or cannot start at ${executablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
});
suite.define(() => {
it("keeps dropdown text aligned at mobile and desktop widths", async () => {
for (const viewport of [
{ height: 844, width: 390 },
{ height: 900, width: 1_280 },
]) {
await suite.withPage(
{
locale: "en-US",
serviceWorkers: "block",
viewport,
},
async ({ page }) => {
const gateway = await installMockGateway(page, {
methodResponses: {
"cron.list": {
jobs: [],
snapshotRevision: "cron-run-filter-typography",
total: 0,
offset: 0,
limit: 50,
hasMore: false,
nextOffset: null,
},
"cron.runs": {
entries: [],
total: 0,
offset: 0,
limit: 50,
hasMore: false,
nextOffset: null,
},
"cron.status": { enabled: true, jobs: 0, nextWakeAtMs: null },
},
});
await page.goto(`${suite.server.baseUrl}cron`);
await page.getByRole("tab", { name: "Run history", exact: true }).click();
const labels = page.locator(".cron-run-filters .cron-filter-dropdown__trigger span");
await expect.poll(() => labels.count()).toBe(3);
const fontSizes = await labels.evaluateAll((elements) =>
elements.map((element) => getComputedStyle(element).fontSize),
);
expect(new Set(fontSizes).size).toBe(1);
await page.getByRole("button", { name: "Sort Newest first", exact: true }).click();
await page.locator('wa-dropdown-item[value="asc"]').click();
await page.getByRole("button", { name: "Sort Oldest first", exact: true }).waitFor();
await expect
.poll(async () =>
(await gateway.getRequests("cron.runs")).some((request) => {
const params = request.params;
return (
typeof params === "object" &&
params !== null &&
"sortDir" in params &&
params.sortDir === "asc"
);
}),
)
.toBe(true);
},
);
}
});
});
+10 -5
View File
@@ -45,14 +45,19 @@ suite.define(() => {
const response = await page.goto(`${suite.server.baseUrl}cron`);
expect(response?.status()).toBe(200);
await page.locator('[data-test-id="cron-list-tab-activity"]').click();
const sort = page.locator("select.cron-run-sort");
const sortMenu = page.locator("wa-dropdown", { has: page.locator(".cron-run-sort") });
const sort = page.getByRole("button", { name: "Sort Newest first", exact: true });
await sort.waitFor({ state: "visible" });
await sort.selectOption("asc");
expect(await sort.inputValue()).toBe("asc");
// Switching tabs recreates the select with the persisted non-first value.
await sort.click();
await sortMenu.locator('wa-dropdown-item[value="asc"]').click();
await page.getByRole("button", { name: "Sort Oldest first", exact: true }).waitFor();
// Switching tabs recreates the dropdown with the persisted non-first value.
await page.locator('[data-test-id="cron-list-tab-tasks"]').click();
await page.locator('[data-test-id="cron-list-tab-activity"]').click();
expect(await page.locator("select.cron-run-sort").inputValue()).toBe("asc");
await page.getByRole("button", { name: "Sort Oldest first", exact: true }).waitFor();
expect(
await sortMenu.locator('wa-dropdown-item[value="asc"]').getAttribute("aria-current"),
).toBe("true");
await page.locator('[data-test-id="cron-new-task"]').click();
const pickerValue = (selector: string) =>
+36 -19
View File
@@ -223,8 +223,8 @@ export function renderRunsSection(props: CronRunsSectionProps) {
.map((option) => option.label);
const statusSummary = summarizeSelection(selectedStatusLabels, t("cron.runs.allStatuses"));
const deliverySummary = summarizeSelection(selectedDeliveryLabels, t("cron.runs.allDelivery"));
// The sort select's .value binding commits before its options exist;
// selected attributes preserve non-first values.
const sortLabel =
props.runsSortDir === "asc" ? t("cron.runs.oldestFirst") : t("cron.runs.newestFirst");
return html`
<div class="cron-runs">
${props.conditionActivity ? renderConditionActivity(props.conditionActivity) : nothing}
@@ -273,23 +273,40 @@ export function renderRunsSection(props: CronRunsSectionProps) {
void props.onRunsFiltersChange({ cronRunsDeliveryStatuses: [] });
},
})}
<select
class="cron-run-sort"
aria-label=${t("cron.jobs.sort")}
title=${t("cron.jobs.sort")}
.value=${props.runsSortDir}
@change=${(e: Event) =>
props.onRunsFiltersChange({
cronRunsSortDir: (e.target as HTMLSelectElement).value as CronSortDir,
})}
>
<option value="desc" ?selected=${props.runsSortDir === "desc"}>
${t("cron.runs.newestFirst")}
</option>
<option value="asc" ?selected=${props.runsSortDir === "asc"}>
${t("cron.runs.oldestFirst")}
</option>
</select>
<div class="cron-filter-dropdown">
<wa-dropdown
class="cron-filter-dropdown__details"
placement="bottom-start"
@wa-select=${(event: CustomEvent<{ item: { value?: string } }>) => {
const value = event.detail.item.value;
if (value === "asc" || value === "desc") {
void props.onRunsFiltersChange({ cronRunsSortDir: value });
}
}}
>
<button
slot="trigger"
type="button"
class="btn btn--sm cron-filter-dropdown__trigger cron-run-sort"
aria-label=${`${t("cron.jobs.sort")} ${sortLabel}`}
>
<span>${sortLabel}</span>
${icon("chevronDown")}
</button>
<wa-dropdown-item value="desc" aria-current=${String(props.runsSortDir === "desc")}>
${t("cron.runs.newestFirst")}
<span slot="details" aria-hidden="true">
${props.runsSortDir === "desc" ? icon("check") : nothing}
</span>
</wa-dropdown-item>
<wa-dropdown-item value="asc" aria-current=${String(props.runsSortDir === "asc")}>
${t("cron.runs.oldestFirst")}
<span slot="details" aria-hidden="true">
${props.runsSortDir === "asc" ? icon("check") : nothing}
</span>
</wa-dropdown-item>
</wa-dropdown>
</div>
</div>
${runs.length === 0
? hasRunFilters
+5 -3
View File
@@ -355,9 +355,11 @@ describe("cron view selects", () => {
it("shows persisted non-first values in jobs filters and runs sort", () => {
const activity = renderView({ listTab: "activity", runsSortDir: "asc" });
const sort = getElement(activity, "select.cron-run-sort", HTMLSelectElement);
expect(sort.value).toBe("asc");
expect(sort.querySelector('option[value="asc"]')?.hasAttribute("selected")).toBe(true);
const sort = getElement(activity, ".cron-run-sort", HTMLButtonElement);
expect(sort.textContent).toContain("Oldest first");
expect(
activity.querySelector('wa-dropdown-item[value="asc"]')?.getAttribute("aria-current"),
).toBe("true");
const tasks = renderView({ jobsLastStatusFilter: "error" });
const lastStatus = getElement(
tasks,
-19
View File
@@ -1033,25 +1033,6 @@
}
}
/* Match the .btn--sm dropdown triggers beside it instead of the native look. */
.cron-run-sort {
flex: 0 0 auto;
width: auto;
padding: 6px 10px;
font-size: 12px;
font-weight: 500;
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-elevated);
cursor: var(--cursor-action);
}
.cron-run-sort:hover {
background: var(--bg-hover);
border-color: var(--border-strong);
}
.cron-runs__empty {
font-size: 13px;
}