diff --git a/packages/ui/src/features/folder-picker/GitHubRepoPicker.tsx b/packages/ui/src/features/folder-picker/GitHubRepoPicker.tsx index 65e477ba25..05f5f8d172 100644 --- a/packages/ui/src/features/folder-picker/GitHubRepoPicker.tsx +++ b/packages/ui/src/features/folder-picker/GitHubRepoPicker.tsx @@ -43,6 +43,7 @@ export function GitHubRepoPicker({ repositories, isLoading, placeholder = "Select repository...", + size = "1", disabled = false, anchor, showSearchInput = true, @@ -55,6 +56,8 @@ export function GitHubRepoPicker({ hasMore: controlledHasMore, onLoadMore, }: GitHubRepoPickerProps) { + const buttonSize = size === "2" ? "lg" : "sm"; + const buttonTextClass = size === "2" ? "text-[13px]" : ""; const triggerRef = useRef(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(false); const [uncontrolledSearchQuery, setUncontrolledSearchQuery] = useState(""); @@ -91,7 +94,12 @@ export function GitHubRepoPicker({ if (isLoading && !showInlineLoadingState) { return ( - @@ -107,7 +115,12 @@ export function GitHubRepoPicker({ !hasActiveRemoteSearch ) { return ( - @@ -121,10 +134,10 @@ export function GitHubRepoPicker({ + {triggers.length === 0 ? ( + + + + + + No triggers + + This loop only runs when you start it from its page. Add a trigger + to run it automatically. + + + + ) : ( + triggers.map((trigger) => ( + updateTrigger(trigger.key, patch)} + onRemove={() => removeTrigger(trigger.key)} + /> + )) + )} + + ); } +function AddTriggerMenu({ + disabled, + onAdd, +}: { + disabled?: boolean; + onAdd: (type: LoopSchemas.LoopTriggerTypeEnum) => void; +}) { + return ( + + + + Add trigger + + } + /> + + {TRIGGER_TYPES.map((option) => ( + onAdd(option.type)} + render={ + + + + + + {option.label} + + {option.menuDescription} + + + + } + /> + ))} + + + ); +} + function TriggerCard({ trigger, triggerEndpointPath, @@ -148,75 +213,106 @@ function TriggerCard({ onChange: (patch: Partial) => void; onRemove: () => void; }) { - const isValid = isTriggerDraftValid(trigger); + const meta = triggerTypeMeta(trigger.type); + const Icon = meta.icon; + const invalidMessage = isTriggerDraftValid(trigger) + ? null + : trigger.type === "github" + ? "Pick a repository and at least one event to finish this trigger." + : "Set when this trigger fires."; return ( - - - {triggerTypeIcon(trigger.type)} - - { - const type = value as LoopSchemas.LoopTriggerTypeEnum; - onChange({ type, config: configForType(type) }); - }} - /> - + + + - - onChange({ enabled: checked })} - disabled={disabled} - aria-label={trigger.enabled ? "Disable trigger" : "Enable trigger"} - /> - - - + + + {meta.label} + + + {meta.subtitle} + + onChange({ enabled: checked })} + disabled={disabled} + aria-label={trigger.enabled ? "Disable trigger" : "Enable trigger"} + /> + + + - {!isValid ? ( - - This trigger is missing required fields. - - ) : null} + + {trigger.type === "schedule" ? ( + onChange({ config })} + /> + ) : null} - {trigger.type === "schedule" ? ( - onChange({ config })} - /> - ) : null} + {trigger.type === "github" ? ( + onChange({ config })} + /> + ) : null} - {trigger.type === "github" ? ( - onChange({ config })} - /> + {trigger.type === "api" ? ( + + ) : null} + + + {invalidMessage ? ( + + + {invalidMessage} + ) : null} + + ); +} - {trigger.type === "api" ? ( - - ) : null} +function SubField({ + label, + className, + children, +}: { + label: string; + className?: string; + children: ReactNode; +}) { + return ( + + {label} + {children} ); } @@ -303,93 +399,90 @@ function ScheduleTriggerFields({ setRecurring(next, time, weekday); }; - const timeInput = ( - { - if ( - !e.target.value || - frequency === "once" || - frequency === "hourly" || - frequency === "custom" - ) - return; - setRecurring(frequency, e.target.value, weekday); - }} - /> - ); - return ( - - - + + + - - - {frequency === "custom" ? ( - - {config.cron_expression} - - ) : null} + {frequency === "daily" || frequency === "weekdays" || - frequency === "weekly" - ? timeInput - : null} + frequency === "weekly" ? ( + + { + if (!e.target.value) return; + setRecurring(frequency, e.target.value, weekday); + }} + /> + + ) : null} {frequency === "weekly" ? ( - + setRecurring("weekly", time, value)} /> - + ) : null} {frequency === "once" ? ( - - onChange({ - run_at: e.target.value - ? new Date(e.target.value).toISOString() - : undefined, - }) - } - /> + + + onChange({ + run_at: e.target.value + ? new Date(e.target.value).toISOString() + : undefined, + }) + } + /> + ) : null} + {frequency === "custom" ? ( + + {config.cron_expression} + + ) : null} + {frequency !== "once" ? ( - - Timezone + onChange({ ...config, timezone: value })} /> - + ) : null} {nextRun && nextRunLabel ? ( + Next run - - Repository + + - + - - Events - + + {GITHUB_EVENT_OPTIONS.map((option) => ( toggleEvent(option.value, checked === true) } /> - {option.label} + + {option.label} + + {option.description} + + ))} - + ); } @@ -484,16 +608,21 @@ function ApiTriggerFields({ triggerEndpointPath: string | null; }) { return ( - - + + Fires on an authenticated POST from your own code. Authenticate with a project secret API key (phs_...) scoped to{" "} loop:write. The request body becomes the run's trigger context. {triggerEndpointPath ? ( - - + + POST {triggerEndpointPath} diff --git a/packages/ui/src/features/loops/loopFormTypes.ts b/packages/ui/src/features/loops/loopFormTypes.ts index 4825680804..84d2f08d34 100644 --- a/packages/ui/src/features/loops/loopFormTypes.ts +++ b/packages/ui/src/features/loops/loopFormTypes.ts @@ -108,6 +108,21 @@ export function defaultLoopScheduleTrigger(): LoopTriggerDraft { }; } +export function defaultLoopTriggerOfType( + type: LoopSchemas.LoopTriggerTypeEnum, +): LoopTriggerDraft { + if (type === "schedule") return defaultLoopScheduleTrigger(); + return { + key: nextDraftTriggerKey(), + type, + enabled: true, + config: + type === "github" + ? emptyLoopGithubTriggerConfig() + : emptyLoopApiTriggerConfig(), + }; +} + export function emptyLoopFormValues(): LoopFormValues { return { name: "", diff --git a/packages/ui/src/features/settings/SettingsOptionSelect.tsx b/packages/ui/src/features/settings/SettingsOptionSelect.tsx index 2f8a16c007..5f8eec01f1 100644 --- a/packages/ui/src/features/settings/SettingsOptionSelect.tsx +++ b/packages/ui/src/features/settings/SettingsOptionSelect.tsx @@ -21,6 +21,7 @@ interface SettingsOptionSelectProps { ariaLabel: string; placeholder?: string; className?: string; + size?: "sm" | "default" | "lg"; } export function SettingsOptionSelect({ @@ -31,6 +32,7 @@ export function SettingsOptionSelect({ ariaLabel, placeholder = "Select…", className, + size = "sm", }: SettingsOptionSelectProps) { const selectedLabel = options.find((opt) => opt.value === value)?.label ?? placeholder; @@ -42,10 +44,12 @@ export function SettingsOptionSelect({