mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
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:
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user