@stigmer/react 3.5.3 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/AgentPicker.d.ts +10 -1
- package/agent/AgentPicker.d.ts.map +1 -1
- package/agent/AgentPicker.js +2 -2
- package/agent/AgentPicker.js.map +1 -1
- package/billing/CreditLedgerTable.d.ts.map +1 -1
- package/billing/CreditLedgerTable.js +2 -7
- package/billing/CreditLedgerTable.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +22 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts +53 -0
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
- package/channel/ChannelTemplatesDialog.js +150 -0
- package/channel/ChannelTemplatesDialog.js.map +1 -0
- package/channel/index.d.ts +8 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +4 -0
- package/channel/index.js.map +1 -1
- package/channel/providers.d.ts +12 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +14 -2
- package/channel/providers.js.map +1 -1
- package/channel/templatePresentation.d.ts +37 -0
- package/channel/templatePresentation.d.ts.map +1 -0
- package/channel/templatePresentation.js +69 -0
- package/channel/templatePresentation.js.map +1 -0
- package/channel/useChannelTemplateList.d.ts +64 -0
- package/channel/useChannelTemplateList.d.ts.map +1 -0
- package/channel/useChannelTemplateList.js +69 -0
- package/channel/useChannelTemplateList.js.map +1 -0
- package/channel/useChannelTemplateReadiness.d.ts +53 -0
- package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
- package/channel/useChannelTemplateReadiness.js +42 -0
- package/channel/useChannelTemplateReadiness.js.map +1 -0
- package/composer/ComposerToolbar.d.ts +6 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +9 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -2
- package/composer/SessionComposer.js.map +1 -1
- package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
- package/dashboard/useDashboardFailedRuns.js +2 -0
- package/dashboard/useDashboardFailedRuns.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +14 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +10 -1
- package/index.js.map +1 -1
- package/internal/Pagination.d.ts +15 -0
- package/internal/Pagination.d.ts.map +1 -0
- package/internal/Pagination.js +20 -0
- package/internal/Pagination.js.map +1 -0
- package/library/useExportResource.d.ts +4 -3
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +1 -1
- package/library/useExportResource.js.map +1 -1
- package/models/ModelSelector.d.ts +26 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +27 -6
- package/models/ModelSelector.js.map +1 -1
- package/models/index.d.ts +2 -0
- package/models/index.d.ts.map +1 -1
- package/models/index.js +1 -0
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +10 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +3 -0
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +27 -0
- package/models/service-tier.d.ts.map +1 -0
- package/models/service-tier.js +34 -0
- package/models/service-tier.js.map +1 -0
- package/organization/OrgProvider.d.ts +11 -4
- package/organization/OrgProvider.d.ts.map +1 -1
- package/organization/OrgProvider.js +28 -4
- package/organization/OrgProvider.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/useDeleteResource.d.ts +1 -1
- package/resource-detail/useDeleteResource.d.ts.map +1 -1
- package/resource-detail/useDeleteResource.js +5 -0
- package/resource-detail/useDeleteResource.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +6 -2
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/schedule/CadenceField.d.ts +31 -0
- package/schedule/CadenceField.d.ts.map +1 -0
- package/schedule/CadenceField.js +116 -0
- package/schedule/CadenceField.js.map +1 -0
- package/schedule/ScheduleDetailView.d.ts +81 -0
- package/schedule/ScheduleDetailView.d.ts.map +1 -0
- package/schedule/ScheduleDetailView.js +648 -0
- package/schedule/ScheduleDetailView.js.map +1 -0
- package/schedule/ScheduleForm.d.ts +46 -0
- package/schedule/ScheduleForm.d.ts.map +1 -0
- package/schedule/ScheduleForm.js +214 -0
- package/schedule/ScheduleForm.js.map +1 -0
- package/schedule/ScheduleRowActions.d.ts +20 -0
- package/schedule/ScheduleRowActions.d.ts.map +1 -0
- package/schedule/ScheduleRowActions.js +54 -0
- package/schedule/ScheduleRowActions.js.map +1 -0
- package/schedule/ScheduleRunsTable.d.ts +72 -0
- package/schedule/ScheduleRunsTable.d.ts.map +1 -0
- package/schedule/ScheduleRunsTable.js +161 -0
- package/schedule/ScheduleRunsTable.js.map +1 -0
- package/schedule/TimeZoneField.d.ts +32 -0
- package/schedule/TimeZoneField.d.ts.map +1 -0
- package/schedule/TimeZoneField.js +104 -0
- package/schedule/TimeZoneField.js.map +1 -0
- package/schedule/cadence.d.ts +95 -0
- package/schedule/cadence.d.ts.map +1 -0
- package/schedule/cadence.js +242 -0
- package/schedule/cadence.js.map +1 -0
- package/schedule/index.d.ts +37 -0
- package/schedule/index.d.ts.map +1 -0
- package/schedule/index.js +19 -0
- package/schedule/index.js.map +1 -0
- package/schedule/scheduleColumns.d.ts +26 -0
- package/schedule/scheduleColumns.d.ts.map +1 -0
- package/schedule/scheduleColumns.js +77 -0
- package/schedule/scheduleColumns.js.map +1 -0
- package/schedule/scheduleListFn.d.ts +50 -0
- package/schedule/scheduleListFn.d.ts.map +1 -0
- package/schedule/scheduleListFn.js +65 -0
- package/schedule/scheduleListFn.js.map +1 -0
- package/schedule/scheduleState.d.ts +43 -0
- package/schedule/scheduleState.d.ts.map +1 -0
- package/schedule/scheduleState.js +67 -0
- package/schedule/scheduleState.js.map +1 -0
- package/schedule/useCreateSchedule.d.ts +57 -0
- package/schedule/useCreateSchedule.d.ts.map +1 -0
- package/schedule/useCreateSchedule.js +61 -0
- package/schedule/useCreateSchedule.js.map +1 -0
- package/schedule/useResumeSchedule.d.ts +30 -0
- package/schedule/useResumeSchedule.d.ts.map +1 -0
- package/schedule/useResumeSchedule.js +45 -0
- package/schedule/useResumeSchedule.js.map +1 -0
- package/schedule/useSchedule.d.ts +39 -0
- package/schedule/useSchedule.d.ts.map +1 -0
- package/schedule/useSchedule.js +44 -0
- package/schedule/useSchedule.js.map +1 -0
- package/schedule/useScheduleCount.d.ts +38 -0
- package/schedule/useScheduleCount.d.ts.map +1 -0
- package/schedule/useScheduleCount.js +30 -0
- package/schedule/useScheduleCount.js.map +1 -0
- package/schedule/useScheduleList.d.ts +48 -0
- package/schedule/useScheduleList.d.ts.map +1 -0
- package/schedule/useScheduleList.js +43 -0
- package/schedule/useScheduleList.js.map +1 -0
- package/schedule/useScheduleRuns.d.ts +44 -0
- package/schedule/useScheduleRuns.d.ts.map +1 -0
- package/schedule/useScheduleRuns.js +48 -0
- package/schedule/useScheduleRuns.js.map +1 -0
- package/schedule/useSetScheduleEnabled.d.ts +31 -0
- package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
- package/schedule/useSetScheduleEnabled.js +53 -0
- package/schedule/useSetScheduleEnabled.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +41 -0
- package/schedule/useTriggerSchedule.d.ts.map +1 -0
- package/schedule/useTriggerSchedule.js +62 -0
- package/schedule/useTriggerSchedule.js.map +1 -0
- package/schedule/useUpdateScheduleSpec.d.ts +49 -0
- package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
- package/schedule/useUpdateScheduleSpec.js +70 -0
- package/schedule/useUpdateScheduleSpec.js.map +1 -0
- package/search/useResourceCount.d.ts +2 -1
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +6 -1
- package/search/useResourceCount.js.map +1 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +1 -0
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +11 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -0
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/src/agent/AgentPicker.tsx +13 -1
- package/src/billing/CreditLedgerTable.tsx +3 -42
- package/src/channel/AgentChannelsPanel.tsx +43 -1
- package/src/channel/ChannelTemplatesDialog.tsx +433 -0
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
- package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
- package/src/channel/__tests__/providers.test.ts +9 -0
- package/src/channel/__tests__/templatePresentation.test.ts +96 -0
- package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
- package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
- package/src/channel/index.ts +15 -0
- package/src/channel/providers.ts +26 -2
- package/src/channel/templatePresentation.ts +82 -0
- package/src/channel/useChannelTemplateList.ts +123 -0
- package/src/channel/useChannelTemplateReadiness.ts +69 -0
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
- package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
- package/src/dashboard/useDashboardFailedRuns.ts +2 -0
- package/src/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +70 -0
- package/src/internal/Pagination.tsx +72 -0
- package/src/library/useExportResource.ts +4 -3
- package/src/models/ModelSelector.tsx +100 -3
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +16 -0
- package/src/models/service-tier.ts +49 -0
- package/src/organization/OrgProvider.tsx +29 -4
- package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
- package/src/resource-detail/useDeleteResource.ts +11 -1
- package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
- package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/schedule/CadenceField.tsx +365 -0
- package/src/schedule/ScheduleDetailView.tsx +1651 -0
- package/src/schedule/ScheduleForm.tsx +615 -0
- package/src/schedule/ScheduleRowActions.tsx +124 -0
- package/src/schedule/ScheduleRunsTable.tsx +424 -0
- package/src/schedule/TimeZoneField.tsx +199 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +683 -0
- package/src/schedule/__tests__/cadence.test.ts +278 -0
- package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
- package/src/schedule/__tests__/scheduleCreation.test.tsx +537 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +399 -0
- package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
- package/src/schedule/__tests__/scheduleState.test.ts +133 -0
- package/src/schedule/cadence.ts +298 -0
- package/src/schedule/index.ts +69 -0
- package/src/schedule/scheduleColumns.tsx +118 -0
- package/src/schedule/scheduleListFn.ts +108 -0
- package/src/schedule/scheduleState.ts +96 -0
- package/src/schedule/useCreateSchedule.ts +88 -0
- package/src/schedule/useResumeSchedule.ts +67 -0
- package/src/schedule/useSchedule.ts +67 -0
- package/src/schedule/useScheduleCount.ts +57 -0
- package/src/schedule/useScheduleList.ts +83 -0
- package/src/schedule/useScheduleRuns.ts +93 -0
- package/src/schedule/useSetScheduleEnabled.ts +84 -0
- package/src/schedule/useTriggerSchedule.ts +90 -0
- package/src/schedule/useUpdateScheduleSpec.ts +108 -0
- package/src/search/__tests__/useResourceCount.test.tsx +89 -0
- package/src/search/useResourceCount.ts +8 -2
- package/src/session/useNewSessionFlow.ts +1 -0
- package/src/session/useSessionConversation.ts +12 -0
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/workflow/WorkflowDashboard.tsx +1 -0
- package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
- package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
- package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
- package/src/workflow/starter-workflow-yaml.ts +2 -1
- package/src/workflow/useWorkflowExecutionList.ts +13 -2
- package/styles.css +1 -1
- package/workflow/CanvasTransitionEdge.d.ts +1 -1
- package/workflow/WorkflowDashboard.d.ts.map +1 -1
- package/workflow/WorkflowDashboard.js +1 -0
- package/workflow/WorkflowDashboard.js.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
- package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.js +87 -9
- package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.js +31 -5
- package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
- package/workflow/starter-workflow-yaml.d.ts +1 -1
- package/workflow/starter-workflow-yaml.d.ts.map +1 -1
- package/workflow/starter-workflow-yaml.js +2 -1
- package/workflow/starter-workflow-yaml.js.map +1 -1
- package/workflow/useWorkflowExecutionList.d.ts +10 -0
- package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionList.js +3 -2
- package/workflow/useWorkflowExecutionList.js.map +1 -1
|
@@ -57,6 +57,14 @@ export interface UseResourceCollectionOptions<TData = SearchResult> {
|
|
|
57
57
|
readonly columns?: readonly WorkbenchColumnDef<TData>[];
|
|
58
58
|
/** Enable row selection state tracking. @default false */
|
|
59
59
|
readonly enableSelection?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Extracts a stable unique ID from an item, used as the TanStack row
|
|
62
|
+
* id. Provide it whenever the row type does not carry a top-level
|
|
63
|
+
* `id` (e.g. full resource protos, where the id lives at
|
|
64
|
+
* `metadata.id`). When omitted, falls back to `SearchResult.id`, then
|
|
65
|
+
* the row index.
|
|
66
|
+
*/
|
|
67
|
+
readonly getItemId?: (item: TData) => string;
|
|
60
68
|
/**
|
|
61
69
|
* Opaque token that forces a background refetch whenever its value
|
|
62
70
|
* changes. Use it to re-read the list after an out-of-band mutation
|
|
@@ -157,6 +165,7 @@ export function useResourceCollection<TData = SearchResult>(
|
|
|
157
165
|
onSortChange,
|
|
158
166
|
columns = [],
|
|
159
167
|
enableSelection = false,
|
|
168
|
+
getItemId,
|
|
160
169
|
refetchToken,
|
|
161
170
|
} = options;
|
|
162
171
|
|
|
@@ -247,7 +256,8 @@ export function useResourceCollection<TData = SearchResult>(
|
|
|
247
256
|
onRowSelectionChange: enableSelection ? setRowSelection : undefined,
|
|
248
257
|
getCoreRowModel: getCoreRowModel(),
|
|
249
258
|
getSortedRowModel: getSortedRowModel(),
|
|
250
|
-
getRowId: (row) =>
|
|
259
|
+
getRowId: (row, index) =>
|
|
260
|
+
getItemId?.(row) || (row as SearchResult).id || String(index),
|
|
251
261
|
manualPagination: true,
|
|
252
262
|
manualSorting: true,
|
|
253
263
|
enableRowSelection: enableSelection,
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useId } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import {
|
|
6
|
+
cadenceToCron,
|
|
7
|
+
describeCadence,
|
|
8
|
+
formatTime,
|
|
9
|
+
validateCron,
|
|
10
|
+
WEEKDAY_LABELS,
|
|
11
|
+
type CadenceKind,
|
|
12
|
+
type CadencePreset,
|
|
13
|
+
} from "./cadence.js";
|
|
14
|
+
|
|
15
|
+
/** Props for {@link CadenceField}. */
|
|
16
|
+
export interface CadenceFieldProps {
|
|
17
|
+
/** The current cadence. */
|
|
18
|
+
readonly value: CadencePreset;
|
|
19
|
+
/** Called with the new cadence on every edit. */
|
|
20
|
+
readonly onChange: (preset: CadencePreset) => void;
|
|
21
|
+
/** Time zone name, echoed into the summary line. */
|
|
22
|
+
readonly timeZone?: string;
|
|
23
|
+
/** Prevents interaction when `true`. */
|
|
24
|
+
readonly disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const KIND_OPTIONS: readonly { kind: CadenceKind; label: string }[] = [
|
|
28
|
+
{ kind: "hourly", label: "Hourly" },
|
|
29
|
+
{ kind: "daily", label: "Daily" },
|
|
30
|
+
{ kind: "weekly", label: "Weekly" },
|
|
31
|
+
{ kind: "monthly", label: "Monthly" },
|
|
32
|
+
{ kind: "custom", label: "Custom cron" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Human-friendly cadence builder for schedules.
|
|
37
|
+
*
|
|
38
|
+
* Presents preset cadences (hourly / daily / weekly / monthly) with
|
|
39
|
+
* plain time-and-day inputs, generating the 5-field cron expression by
|
|
40
|
+
* construction — the user never writes cron unless they choose the
|
|
41
|
+
* Custom escape hatch, where the raw expression is validated instantly
|
|
42
|
+
* with the platform's own lexical rules ({@link validateCron}).
|
|
43
|
+
*
|
|
44
|
+
* A live plain-English summary ({@link describeCadence}) always states
|
|
45
|
+
* what was chosen. It describes the preset, not a computed fire-time
|
|
46
|
+
* forecast — the authoritative next fire time is published by the
|
|
47
|
+
* server as `status.next_fire_at` after apply.
|
|
48
|
+
*
|
|
49
|
+
* Controlled component over {@link CadencePreset}; convert to the spec
|
|
50
|
+
* string with {@link cadenceToCron} at submit time.
|
|
51
|
+
*/
|
|
52
|
+
export function CadenceField({
|
|
53
|
+
value,
|
|
54
|
+
onChange,
|
|
55
|
+
timeZone,
|
|
56
|
+
disabled,
|
|
57
|
+
}: CadenceFieldProps) {
|
|
58
|
+
const groupId = useId();
|
|
59
|
+
|
|
60
|
+
const switchKind = useCallback(
|
|
61
|
+
(kind: CadenceKind) => {
|
|
62
|
+
if (kind === value.kind) return;
|
|
63
|
+
onChange(presetForKind(kind, value));
|
|
64
|
+
},
|
|
65
|
+
[value, onChange],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const cronError =
|
|
69
|
+
value.kind === "custom" && value.cron.trim() !== ""
|
|
70
|
+
? validateCron(value.cron.trim())
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="space-y-2">
|
|
75
|
+
{/* Preset selector — segmented radio group, matching ScopeToggle's pattern */}
|
|
76
|
+
<div
|
|
77
|
+
role="radiogroup"
|
|
78
|
+
aria-label="Cadence"
|
|
79
|
+
aria-disabled={disabled || undefined}
|
|
80
|
+
className={cn(
|
|
81
|
+
"inline-flex flex-wrap rounded-md bg-muted p-0.5",
|
|
82
|
+
disabled && "pointer-events-none opacity-50",
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{KIND_OPTIONS.map((option) => {
|
|
86
|
+
const isSelected = value.kind === option.kind;
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
key={option.kind}
|
|
90
|
+
type="button"
|
|
91
|
+
role="radio"
|
|
92
|
+
aria-checked={isSelected}
|
|
93
|
+
tabIndex={isSelected ? 0 : -1}
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
onClick={() => switchKind(option.kind)}
|
|
96
|
+
onKeyDown={(e) => {
|
|
97
|
+
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
const idx = KIND_OPTIONS.findIndex((o) => o.kind === value.kind);
|
|
100
|
+
const delta = e.key === "ArrowRight" ? 1 : -1;
|
|
101
|
+
const next =
|
|
102
|
+
KIND_OPTIONS[
|
|
103
|
+
(idx + delta + KIND_OPTIONS.length) % KIND_OPTIONS.length
|
|
104
|
+
];
|
|
105
|
+
switchKind(next.kind);
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
className={cn(
|
|
109
|
+
"inline-flex cursor-pointer items-center rounded-sm px-2.5 py-1 text-xs font-medium transition-colors",
|
|
110
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
111
|
+
isSelected
|
|
112
|
+
? "bg-background text-foreground shadow-sm"
|
|
113
|
+
: "text-muted-foreground hover:text-foreground",
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{option.label}
|
|
117
|
+
</button>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Per-preset inputs */}
|
|
123
|
+
{value.kind === "hourly" && (
|
|
124
|
+
<div className="flex items-center gap-2 text-xs text-foreground">
|
|
125
|
+
<label htmlFor={`${groupId}-minute`}>At minute</label>
|
|
126
|
+
<input
|
|
127
|
+
id={`${groupId}-minute`}
|
|
128
|
+
type="number"
|
|
129
|
+
min={0}
|
|
130
|
+
max={59}
|
|
131
|
+
value={value.minute}
|
|
132
|
+
onChange={(e) =>
|
|
133
|
+
onChange({
|
|
134
|
+
kind: "hourly",
|
|
135
|
+
minute: clampInt(e.target.value, 0, 59, value.minute),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
disabled={disabled}
|
|
139
|
+
className={numberInputClasses}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{value.kind === "daily" && (
|
|
145
|
+
<TimeInput
|
|
146
|
+
id={`${groupId}-time`}
|
|
147
|
+
hour={value.hour}
|
|
148
|
+
minute={value.minute}
|
|
149
|
+
disabled={disabled}
|
|
150
|
+
onChange={(hour, minute) => onChange({ kind: "daily", hour, minute })}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{value.kind === "weekly" && (
|
|
155
|
+
<div className="space-y-2">
|
|
156
|
+
<div
|
|
157
|
+
role="group"
|
|
158
|
+
aria-label="Days of week"
|
|
159
|
+
className="flex flex-wrap gap-1"
|
|
160
|
+
>
|
|
161
|
+
{WEEKDAY_LABELS.map((label, day) => {
|
|
162
|
+
const isOn = value.days.includes(day);
|
|
163
|
+
// The last selected day cannot be removed — a weekly
|
|
164
|
+
// schedule with zero days is not expressible in cron.
|
|
165
|
+
const isLastSelected = isOn && value.days.length === 1;
|
|
166
|
+
return (
|
|
167
|
+
<button
|
|
168
|
+
key={label}
|
|
169
|
+
type="button"
|
|
170
|
+
aria-pressed={isOn}
|
|
171
|
+
aria-label={label}
|
|
172
|
+
title={label}
|
|
173
|
+
disabled={disabled || isLastSelected}
|
|
174
|
+
onClick={() =>
|
|
175
|
+
onChange({
|
|
176
|
+
...value,
|
|
177
|
+
days: isOn
|
|
178
|
+
? value.days.filter((d) => d !== day)
|
|
179
|
+
: [...value.days, day].sort((a, b) => a - b),
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
className={cn(
|
|
183
|
+
"inline-flex h-7 w-9 items-center justify-center rounded-md border text-xs font-medium transition-colors",
|
|
184
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
185
|
+
"disabled:pointer-events-none",
|
|
186
|
+
isOn
|
|
187
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
188
|
+
: "border-input bg-background text-muted-foreground hover:text-foreground",
|
|
189
|
+
isLastSelected && "opacity-70",
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{label.slice(0, 3)}
|
|
193
|
+
</button>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
</div>
|
|
197
|
+
<TimeInput
|
|
198
|
+
id={`${groupId}-time`}
|
|
199
|
+
hour={value.hour}
|
|
200
|
+
minute={value.minute}
|
|
201
|
+
disabled={disabled}
|
|
202
|
+
onChange={(hour, minute) => onChange({ ...value, hour, minute })}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{value.kind === "monthly" && (
|
|
208
|
+
<div className="space-y-2">
|
|
209
|
+
<div className="flex items-center gap-2 text-xs text-foreground">
|
|
210
|
+
<label htmlFor={`${groupId}-day`}>On day</label>
|
|
211
|
+
<input
|
|
212
|
+
id={`${groupId}-day`}
|
|
213
|
+
type="number"
|
|
214
|
+
min={1}
|
|
215
|
+
max={31}
|
|
216
|
+
value={value.day}
|
|
217
|
+
onChange={(e) =>
|
|
218
|
+
onChange({
|
|
219
|
+
...value,
|
|
220
|
+
day: clampInt(e.target.value, 1, 31, value.day),
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
disabled={disabled}
|
|
224
|
+
className={numberInputClasses}
|
|
225
|
+
/>
|
|
226
|
+
<span className="text-muted-foreground">of every month</span>
|
|
227
|
+
</div>
|
|
228
|
+
{value.day >= 29 && (
|
|
229
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
230
|
+
Months without day {value.day} are skipped — February never
|
|
231
|
+
fires a day-{value.day} schedule
|
|
232
|
+
{value.day === 29 ? " except in leap years" : ""}.
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
<TimeInput
|
|
236
|
+
id={`${groupId}-time`}
|
|
237
|
+
hour={value.hour}
|
|
238
|
+
minute={value.minute}
|
|
239
|
+
disabled={disabled}
|
|
240
|
+
onChange={(hour, minute) => onChange({ ...value, hour, minute })}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{value.kind === "custom" && (
|
|
246
|
+
<div className="space-y-1">
|
|
247
|
+
<input
|
|
248
|
+
type="text"
|
|
249
|
+
aria-label="Cron expression"
|
|
250
|
+
placeholder="e.g. 0 9 * * MON-FRI"
|
|
251
|
+
value={value.cron}
|
|
252
|
+
onChange={(e) => onChange({ kind: "custom", cron: e.target.value })}
|
|
253
|
+
disabled={disabled}
|
|
254
|
+
className={cn(
|
|
255
|
+
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 font-mono text-xs text-foreground",
|
|
256
|
+
"placeholder:font-sans placeholder:text-muted-foreground",
|
|
257
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
258
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
259
|
+
cronError && "border-destructive",
|
|
260
|
+
)}
|
|
261
|
+
/>
|
|
262
|
+
{cronError ? (
|
|
263
|
+
<p className="text-[0.65rem] text-destructive" role="alert">
|
|
264
|
+
{cronError}
|
|
265
|
+
</p>
|
|
266
|
+
) : (
|
|
267
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
268
|
+
5 fields (minute hour day-of-month month day-of-week) or
|
|
269
|
+
@hourly, @daily, @weekly, @monthly, @yearly.
|
|
270
|
+
</p>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Plain-English summary of the chosen cadence */}
|
|
276
|
+
{!(value.kind === "custom" && (cronError || value.cron.trim() === "")) && (
|
|
277
|
+
<p className="text-xs text-muted-foreground" data-testid="cadence-summary">
|
|
278
|
+
{describeCadence(value, timeZone)}
|
|
279
|
+
</p>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Private helpers
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
const numberInputClasses = cn(
|
|
290
|
+
"w-16 rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground",
|
|
291
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
292
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
function TimeInput({
|
|
296
|
+
id,
|
|
297
|
+
hour,
|
|
298
|
+
minute,
|
|
299
|
+
disabled,
|
|
300
|
+
onChange,
|
|
301
|
+
}: {
|
|
302
|
+
readonly id: string;
|
|
303
|
+
readonly hour: number;
|
|
304
|
+
readonly minute: number;
|
|
305
|
+
readonly disabled?: boolean;
|
|
306
|
+
readonly onChange: (hour: number, minute: number) => void;
|
|
307
|
+
}) {
|
|
308
|
+
return (
|
|
309
|
+
<div className="flex items-center gap-2 text-xs text-foreground">
|
|
310
|
+
<label htmlFor={id}>At</label>
|
|
311
|
+
<input
|
|
312
|
+
id={id}
|
|
313
|
+
type="time"
|
|
314
|
+
value={formatTime(hour, minute)}
|
|
315
|
+
onChange={(e) => {
|
|
316
|
+
const [h, m] = e.target.value.split(":").map(Number);
|
|
317
|
+
if (Number.isInteger(h) && Number.isInteger(m)) onChange(h, m);
|
|
318
|
+
}}
|
|
319
|
+
disabled={disabled}
|
|
320
|
+
className={cn(
|
|
321
|
+
"rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground",
|
|
322
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
323
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
324
|
+
)}
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Next preset when the user switches kind, carrying the time of day
|
|
332
|
+
* across so "Daily at 09:30" → Weekly keeps 09:30. Switching to Custom
|
|
333
|
+
* prefills the generated cron from the outgoing preset — the escape
|
|
334
|
+
* hatch starts from what the user already built, not a blank box.
|
|
335
|
+
*/
|
|
336
|
+
function presetForKind(kind: CadenceKind, previous: CadencePreset): CadencePreset {
|
|
337
|
+
const hour = "hour" in previous ? previous.hour : 9;
|
|
338
|
+
const minute = "minute" in previous ? previous.minute : 0;
|
|
339
|
+
switch (kind) {
|
|
340
|
+
case "hourly":
|
|
341
|
+
return { kind, minute };
|
|
342
|
+
case "daily":
|
|
343
|
+
return { kind, hour, minute };
|
|
344
|
+
case "weekly":
|
|
345
|
+
return { kind, days: [1], hour, minute };
|
|
346
|
+
case "monthly":
|
|
347
|
+
return { kind, day: 1, hour, minute };
|
|
348
|
+
case "custom":
|
|
349
|
+
return {
|
|
350
|
+
kind,
|
|
351
|
+
cron: previous.kind === "custom" ? previous.cron : cadenceToCron(previous),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function clampInt(
|
|
357
|
+
raw: string,
|
|
358
|
+
min: number,
|
|
359
|
+
max: number,
|
|
360
|
+
fallback: number,
|
|
361
|
+
): number {
|
|
362
|
+
const n = Number(raw);
|
|
363
|
+
if (!Number.isInteger(n)) return fallback;
|
|
364
|
+
return Math.min(max, Math.max(min, n));
|
|
365
|
+
}
|