feat(desktop): support multiple cron delivery targets
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { cronEditorUpdates, jobIsScriptOnly, validateCronEditor } from './cron-job-model'
|
||||
import {
|
||||
cronEditorUpdates,
|
||||
jobIsScriptOnly,
|
||||
parseCronDeliveryTargets,
|
||||
toggleCronDeliveryTarget,
|
||||
validateCronEditor
|
||||
} from './cron-job-model'
|
||||
|
||||
describe('jobIsScriptOnly', () => {
|
||||
it('is true when no_agent is set and a script is present', () => {
|
||||
@@ -31,6 +37,28 @@ describe('validateCronEditor', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('cron delivery targets', () => {
|
||||
it('parses comma-separated targets and removes duplicates', () => {
|
||||
expect(parseCronDeliveryTargets('local, telegram,local')).toEqual(['local', 'telegram'])
|
||||
})
|
||||
|
||||
it('falls back to local for an empty stored value', () => {
|
||||
expect(parseCronDeliveryTargets('')).toEqual(['local'])
|
||||
})
|
||||
|
||||
it('adds a second target in the scheduler comma-separated format', () => {
|
||||
expect(toggleCronDeliveryTarget('local', 'origin', true)).toBe('local,origin')
|
||||
})
|
||||
|
||||
it('removes one target while keeping the other selection', () => {
|
||||
expect(toggleCronDeliveryTarget('local,origin', 'local', false)).toBe('origin')
|
||||
})
|
||||
|
||||
it('does not allow the final delivery target to be unchecked', () => {
|
||||
expect(toggleCronDeliveryTarget('origin', 'origin', false)).toBe('origin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cronEditorUpdates', () => {
|
||||
it('omits prompt when saving a script-only job with an empty prompt', () => {
|
||||
expect(
|
||||
|
||||
@@ -45,6 +45,29 @@ export interface CronEditorSaveValues {
|
||||
schedule: string
|
||||
}
|
||||
|
||||
export function parseCronDeliveryTargets(value: string): string[] {
|
||||
const targets = value
|
||||
.split(',')
|
||||
.map(target => target.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return targets.length > 0 ? [...new Set(targets)] : ['local']
|
||||
}
|
||||
|
||||
export function toggleCronDeliveryTarget(value: string, target: string, checked: boolean): string {
|
||||
const targets = parseCronDeliveryTargets(value)
|
||||
|
||||
if (checked) {
|
||||
return targets.includes(target) ? targets.join(',') : [...targets, target].join(',')
|
||||
}
|
||||
|
||||
if (!targets.includes(target) || targets.length === 1) {
|
||||
return targets.join(',')
|
||||
}
|
||||
|
||||
return targets.filter(candidate => candidate !== target).join(',')
|
||||
}
|
||||
|
||||
/** Build the API update payload, preserving an empty prompt on script-only jobs. */
|
||||
export function cronEditorUpdates(values: CronEditorSaveValues, options: { scriptOnlyJob: boolean }): CronJobUpdates {
|
||||
const updates: CronJobUpdates = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { PageLoader } from '@/components/page-loader'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -73,7 +74,13 @@ import {
|
||||
import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
|
||||
|
||||
import { BlueprintSlotControl, blueprintSlotHelp, cleanBlueprintFieldError, initialBlueprintValues } from './blueprints'
|
||||
import { cronEditorUpdates, jobIsScriptOnly, validateCronEditor } from './cron-job-model'
|
||||
import {
|
||||
cronEditorUpdates,
|
||||
jobIsScriptOnly,
|
||||
parseCronDeliveryTargets,
|
||||
toggleCronDeliveryTarget,
|
||||
validateCronEditor
|
||||
} from './cron-job-model'
|
||||
import { jobState, jobTitle, STATE_DOT } from './job-state'
|
||||
|
||||
const DEFAULT_DELIVER = 'local'
|
||||
@@ -778,10 +785,11 @@ function deliverTargetLabel(target: CronDeliveryTarget, c: Translations['cron'])
|
||||
return target.id !== 'local' && !target.home_target_set ? `${base} — ${c.deliverNeedsHomeChannel}` : base
|
||||
}
|
||||
|
||||
// The delivery-target dropdown, shared by the manual cron editor and the
|
||||
// blueprint form so both offer exactly the connected platforms (never a
|
||||
// hardcoded list). While the targets load, keep the current value selectable.
|
||||
function DeliverSelect({
|
||||
// The delivery-target checkbox group, shared by the manual cron editor and the
|
||||
// blueprint form. The scheduler accepts comma-separated targets, so users can
|
||||
// keep results local while also sending them to connected platforms. Preserve
|
||||
// selected targets missing from discovery so editing never drops a saved route.
|
||||
function DeliverCheckboxes({
|
||||
c,
|
||||
id,
|
||||
onChange,
|
||||
@@ -794,21 +802,34 @@ function DeliverSelect({
|
||||
targets: CronDeliveryTarget[]
|
||||
value: string
|
||||
}) {
|
||||
const options = targets.length > 0 ? targets : [{ home_env_var: null, home_target_set: true, id: value, name: value }]
|
||||
const selected = parseCronDeliveryTargets(value)
|
||||
const knownIds = new Set(targets.map(target => target.id))
|
||||
|
||||
const options = [
|
||||
...targets,
|
||||
...selected
|
||||
.filter(target => !knownIds.has(target))
|
||||
.map(target => ({ home_env_var: null, home_target_set: true, id: target, name: target }))
|
||||
]
|
||||
|
||||
return (
|
||||
<Select onValueChange={onChange} value={value}>
|
||||
<SelectTrigger className="h-9 rounded-md" id={id}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map(target => (
|
||||
<SelectItem key={target.id} value={target.id}>
|
||||
{deliverTargetLabel(target, c)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="grid gap-2 rounded-md border border-input px-3 py-2.5" id={id} role="group">
|
||||
{options.map((target, index) => {
|
||||
const checked = selected.includes(target.id)
|
||||
const checkboxId = `${id}-${index}`
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-2 text-sm" htmlFor={checkboxId} key={target.id}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
id={checkboxId}
|
||||
onCheckedChange={next => onChange(toggleCronDeliveryTarget(value, target.id, next === true))}
|
||||
/>
|
||||
<span>{deliverTargetLabel(target, c)}</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1041,7 +1062,7 @@ function CronEditorDialog({
|
||||
// Use the shared, backend-sourced delivery targets (same as the
|
||||
// manual editor) rather than the blueprint's static field.options,
|
||||
// so both dialogs offer exactly the connected platforms.
|
||||
<DeliverSelect
|
||||
<DeliverCheckboxes
|
||||
c={c}
|
||||
id={fieldId}
|
||||
onChange={next => setSlotValues(prev => ({ ...prev, [field.name]: next }))}
|
||||
@@ -1122,7 +1143,7 @@ function CronEditorDialog({
|
||||
</Field>
|
||||
|
||||
<Field htmlFor="cron-deliver" label={c.deliverLabel}>
|
||||
<DeliverSelect
|
||||
<DeliverCheckboxes
|
||||
c={c}
|
||||
id="cron-deliver"
|
||||
onChange={setDeliver}
|
||||
|
||||
Reference in New Issue
Block a user