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>
This commit is contained in:
+1
-1
@@ -270,7 +270,7 @@ Track progress here. Mark tasks complete as they land and pass review.
|
|||||||
### Phase 4 — Carve up the GUI
|
### Phase 4 — Carve up the GUI
|
||||||
- [x] T4.1 — Rename `gui` → `ui`; split app.go into run.go + mainwindow.go _(required Fyne v2.5.3→v2.6.3 upgrade for `fyne.Do`)_
|
- [x] T4.1 — Rename `gui` → `ui`; split app.go into run.go + mainwindow.go _(required Fyne v2.5.3→v2.6.3 upgrade for `fyne.Do`)_
|
||||||
- [x] T4.2 — Extract `jobs_view.go`
|
- [x] T4.2 — Extract `jobs_view.go`
|
||||||
- [ ] T4.3 — Extract `job_dialog.go`
|
- [x] T4.3 — Extract `job_dialog.go`
|
||||||
- [ ] T4.4 — Extract `history_view.go`
|
- [ ] T4.4 — Extract `history_view.go`
|
||||||
- [ ] T4.5 — Extract `settings_view.go`
|
- [ ] T4.5 — Extract `settings_view.go`
|
||||||
- [ ] T4.6 — Extract `tray.go`, `singleinstance.go`, `layout.go`
|
- [ ] T4.6 — Extract `tray.go`, `singleinstance.go`, `layout.go`
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -356,77 +356,6 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
|||||||
return panel, refreshView
|
return panel, refreshView
|
||||||
}
|
}
|
||||||
|
|
||||||
// showJobDialog opens a create/edit form dialog.
|
|
||||||
// It will be moved to job_dialog.go in T4.3.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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 now. 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
func filteredJobIndexes(jobs []job, folder string) []int {
|
func filteredJobIndexes(jobs []job, folder string) []int {
|
||||||
indexes := make([]int, 0, len(jobs))
|
indexes := make([]int, 0, len(jobs))
|
||||||
|
|||||||
Reference in New Issue
Block a user