From 91679efa3ce301996185b3962b071f3a069063f3 Mon Sep 17 00:00:00 2001 From: mixeme Date: Sun, 21 Jun 2026 23:19:00 +0300 Subject: [PATCH] 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 --- docs/REFACTORING.md | 2 +- src/ui/job_dialog.go | 90 ++++++++++++++++++++++++++++++++++++++++++++ src/ui/jobs_view.go | 71 ---------------------------------- 3 files changed, 91 insertions(+), 72 deletions(-) create mode 100644 src/ui/job_dialog.go diff --git a/docs/REFACTORING.md b/docs/REFACTORING.md index b398014..bc7e90b 100644 --- a/docs/REFACTORING.md +++ b/docs/REFACTORING.md @@ -270,7 +270,7 @@ Track progress here. Mark tasks complete as they land and pass review. ### 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.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.5 — Extract `settings_view.go` - [ ] T4.6 — Extract `tray.go`, `singleinstance.go`, `layout.go` diff --git a/src/ui/job_dialog.go b/src/ui/job_dialog.go new file mode 100644 index 0000000..1e79767 --- /dev/null +++ b/src/ui/job_dialog.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() +} diff --git a/src/ui/jobs_view.go b/src/ui/jobs_view.go index 6e0145c..452604f 100644 --- a/src/ui/jobs_view.go +++ b/src/ui/jobs_view.go @@ -356,77 +356,6 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) { 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 { indexes := make([]int, 0, len(jobs))