Files
gosentry/src/ui/job_dialog.go
T
mixeme 91679efa3c T4.3: Extract job_dialog.go; validate schedule via domain.Validate
Moves showJobDialog out of jobs_view.go into its own file and adds a
domain.Validate call on the schedule field so malformed cron expressions
are rejected with an error dialog before onSave is invoked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 23:19:00 +03:00

91 lines
3.2 KiB
Go

package ui
import (
"fmt"
"strings"
"gitea.mixdep.ru/mix/gosentry/src/app"
"gitea.mixdep.ru/mix/gosentry/src/domain"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
// showJobDialog opens a create/edit form for a single job. onSave is called
// with the populated job only when the user clicks Save and all fields pass
// validation.
func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
name := widget.NewEntry()
name.SetPlaceHolder("Nightly backup")
name.SetText(current.Name)
folderEntry := widget.NewEntry()
folderEntry.SetPlaceHolder("Maintenance")
folderEntry.SetText(current.Folder)
scheduleEntry := widget.NewEntry()
scheduleEntry.SetPlaceHolder("@every 1m")
scheduleEntry.SetText(current.Schedule)
commandEntry := widget.NewEntry()
commandEntry.SetPlaceHolder(`C:\Program Files\App\App.exe`)
commandEntry.SetText(current.Command)
argumentsEntry := widget.NewMultiLineEntry()
argumentsEntry.SetPlaceHolder(`D:\Local\Jobs\Auto.ffs_batch`)
argumentsEntry.SetText(current.Arguments)
successExitCodesEntry := widget.NewEntry()
successExitCodesEntry.SetPlaceHolder("0")
successExitCodesEntry.SetText(app.DisplaySuccessExitCodes(current.SuccessExitCodes))
startOnly := widget.NewCheck("Start only, do not wait for exit", nil)
startOnly.SetChecked(current.StartOnly)
enabled := widget.NewCheck("Enabled", nil)
enabled.SetChecked(current.Enabled)
form := dialog.NewForm(
title,
"Save",
"Cancel",
[]*widget.FormItem{
widget.NewFormItem("Name", name),
widget.NewFormItem("Folder", folderEntry),
widget.NewFormItem("Schedule", scheduleEntry),
widget.NewFormItem("Command", commandEntry),
widget.NewFormItem("Arguments", argumentsEntry),
widget.NewFormItem("Success exit codes", successExitCodesEntry),
widget.NewFormItem("", startOnly),
widget.NewFormItem("", enabled),
},
func(saved bool) {
if !saved {
return
}
if strings.TrimSpace(name.Text) == "" || strings.TrimSpace(scheduleEntry.Text) == "" || strings.TrimSpace(commandEntry.Text) == "" {
// These three fields are the minimum executable job definition.
// Folder is optional because ungrouped jobs are a supported workflow.
dialog.ShowError(fmt.Errorf("name, schedule, and command are required"), w)
return
}
if err := domain.Validate(strings.TrimSpace(scheduleEntry.Text)); err != nil {
dialog.ShowError(fmt.Errorf("invalid schedule: %w", err), w)
return
}
current.Name = strings.TrimSpace(name.Text)
current.Folder = strings.TrimSpace(folderEntry.Text)
current.Schedule = strings.TrimSpace(scheduleEntry.Text)
current.Command = strings.TrimSpace(commandEntry.Text)
current.Arguments = strings.TrimSpace(argumentsEntry.Text)
current.SuccessExitCodes = strings.TrimSpace(successExitCodesEntry.Text)
if current.SuccessExitCodes == "" {
current.SuccessExitCodes = "0"
}
current.StartOnly = startOnly.Checked
current.Enabled = enabled.Checked
// The dialog only edits durable configuration. Runtime status is
// initialized (new jobs) or updated (edits) by the caller against the
// runtime map, keyed by job ID.
onSave(current)
},
w,
)
form.Resize(fyne.NewSize(640, 460))
form.Show()
}