From 2acfc47b7fe2d5542681876137755c47f582ae97 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 20 Aug 2026 21:10:49 -0700 Subject: [PATCH] fix(ui): expose all automation schedule filters (#126962) * fix(ui): expose all automation schedule filters Amp-Thread-ID: https://ampcode.com/threads/T-01a021f4-b547-7788-a916-d4a94cbd3e3b * refactor(ui): derive cron filters from protocol Amp-Thread-ID: https://ampcode.com/threads/T-01a021f4-b547-7788-a916-d4a94cbd3e3b --------- Co-authored-by: Amp --- src/gateway/server-methods/cron.ts | 17 ++--------------- ui/src/api/types.ts | 1 + ui/src/lib/cron/index.ts | 2 +- ui/src/pages/cron/view.test.ts | 18 +++++++++++++++--- ui/src/pages/cron/view.ts | 21 ++++++++++++++------- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index 72694926dd44..9cb082f92cdb 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -2,6 +2,7 @@ import { parseBoolean } from "@openclaw/normalization-core/boolean-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { + type CronListParams, ErrorCodes, errorShape, GatewayErrorDetailCodes, @@ -488,21 +489,7 @@ export const cronHandlers: GatewayRequestHandlers = { if (!assertValidParams(params, validateCronListParams, "cron.list", respond)) { return; } - const p = params as { - includeDisabled?: boolean; - limit?: number; - offset?: number; - query?: string; - enabled?: "all" | "enabled" | "disabled"; - scheduleKind?: "all" | "at" | "every" | "cron"; - lastRunStatus?: "all" | "ok" | "error" | "skipped" | "unknown"; - trigger?: "all" | "conditional" | "unconditional"; - sortBy?: "nextRunAtMs" | "updatedAtMs" | "name"; - sortDir?: "asc" | "desc"; - agentId?: string; - compact?: boolean; - includeDeliveryPreviews?: boolean; - }; + const p = params as CronListParams; const callerScope = readCronCallerScope(client); const requestedAgentId = p.agentId ? normalizeAgentId(p.agentId) : undefined; if (callerScope && requestedAgentId && requestedAgentId !== callerScope.agentId) { diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 5714752c8473..da6395a5f8e8 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -555,6 +555,7 @@ export type { export type CronRunStatus = NonNullable; export type CronDeliveryStatus = NonNullable; export type CronJobsEnabledFilter = NonNullable; +export type CronJobsScheduleKindFilter = NonNullable; export type CronJobsTriggerFilter = NonNullable; export type CronJobsSortBy = NonNullable; export type CronRunScope = NonNullable; diff --git a/ui/src/lib/cron/index.ts b/ui/src/lib/cron/index.ts index 0b8c9f5f1476..a508b1578846 100644 --- a/ui/src/lib/cron/index.ts +++ b/ui/src/lib/cron/index.ts @@ -7,6 +7,7 @@ import type { CronJob, CronDeliveryStatus, CronJobsEnabledFilter, + CronJobsScheduleKindFilter, CronJobsTriggerFilter, CronJobsListResult, CronJobsSortBy, @@ -191,7 +192,6 @@ export type CronFieldKey = export type CronFieldErrors = Partial>; -export type CronJobsScheduleKindFilter = "all" | "at" | "every" | "cron" | "on-exit" | "stream"; export type CronJobsLastStatusFilter = "all" | CronRunStatus | "unknown"; type CronRunsLoadStatus = "ok" | "error" | "skipped"; diff --git a/ui/src/pages/cron/view.test.ts b/ui/src/pages/cron/view.test.ts index 22486a8ad64a..1ce5428867eb 100644 --- a/ui/src/pages/cron/view.test.ts +++ b/ui/src/pages/cron/view.test.ts @@ -122,9 +122,21 @@ describe("cron view list pane", () => { '[data-test-id="cron-jobs-schedule-filter"]', HTMLSelectElement, ); - scheduleFilter.value = "cron"; - scheduleFilter.dispatchEvent(new Event("change", { bubbles: true })); - expect(onJobsFiltersChange).toHaveBeenCalledWith({ cronJobsScheduleKindFilter: "cron" }); + expect(Array.from(scheduleFilter.options, (option) => option.value)).toEqual([ + "all", + "at", + "every", + "cron", + "on-exit", + "stream", + ]); + for (const scheduleKind of ["on-exit", "stream"] as const) { + scheduleFilter.value = scheduleKind; + scheduleFilter.dispatchEvent(new Event("change", { bubbles: true })); + expect(onJobsFiltersChange).toHaveBeenCalledWith({ + cronJobsScheduleKindFilter: scheduleKind, + }); + } const lastStatusFilter = getElement( container, diff --git a/ui/src/pages/cron/view.ts b/ui/src/pages/cron/view.ts index 4a6744deb8c1..165bee9b9c04 100644 --- a/ui/src/pages/cron/view.ts +++ b/ui/src/pages/cron/view.ts @@ -17,6 +17,7 @@ import type { CronStatus, CronDeliveryStatus, CronJobsEnabledFilter, + CronJobsScheduleKindFilter, CronJobsTriggerFilter, CronRunsStatusValue, CronJobsSortBy, @@ -51,7 +52,6 @@ import type { CronFieldKey, CronFormState, CronJobsLastStatusFilter, - CronJobsScheduleKindFilter, } from "../../lib/cron/index.ts"; import { formatUiExternalText } from "../../lib/format-error.ts"; import { formatRelativeTimestamp, formatMs } from "../../lib/format.ts"; @@ -448,6 +448,15 @@ const ENABLED_TABS: Array<{ value: CronJobsEnabledFilter; labelKey: string }> = { value: "disabled", labelKey: "cron.tabs.paused" }, ]; +const SCHEDULE_KIND_FILTER_LABELS: Record = { + all: "cron.jobs.all", + at: "cron.form.at", + every: "cron.form.every", + cron: "cron.form.cronOption", + "on-exit": "cron.form.repeatOnExit", + stream: "cron.form.repeatStream", +}; + function renderListView(props: CronProps) { const hasAdvancedJobsFilters = props.jobsScheduleKindFilter !== "all" || @@ -646,12 +655,10 @@ function renderJobsFilterPopover(props: CronProps, active: boolean) { label: t("cron.jobs.schedule"), value: props.jobsScheduleKindFilter, testId: "cron-jobs-schedule-filter", - options: [ - { value: "all", label: t("cron.jobs.all") }, - { value: "at", label: t("cron.form.at") }, - { value: "every", label: t("cron.form.every") }, - { value: "cron", label: t("cron.form.cronOption") }, - ], + options: Object.entries(SCHEDULE_KIND_FILTER_LABELS).map(([value, labelKey]) => ({ + value, + label: t(labelKey), + })), })} ${renderJobsFilter(props, "cronJobsLastStatusFilter", { label: t("cron.jobs.lastRun"),