mirror of
https://github.com/RWejlgaard/org.git
synced 2026-09-02 01:15:35 +00:00
feat: add state form model and commit logic
This commit is contained in:
parent
080e8b0ee5
commit
d44b8c32eb
4 changed files with 228 additions and 2 deletions
|
|
@ -57,7 +57,8 @@ type uiModel struct {
|
|||
captureCursor int // Store cursor position when entering capture mode
|
||||
datepicker datepicker.Model
|
||||
dateTextFocused bool // within set-date modes: text field focused vs. calendar
|
||||
addingDoneState bool // within add-state flow: create a done state
|
||||
addingDoneState bool // within add-state flow: create a done state (legacy; removed in state-form task)
|
||||
stateForm stateForm // backing state for the add/edit state form
|
||||
}
|
||||
|
||||
func InitialModel(orgFile *model.OrgFile, cfg *config.Config, captureMode bool, captureText string) uiModel {
|
||||
|
|
|
|||
|
|
@ -874,6 +874,9 @@ func (m *uiModel) viewSettingsKeybindings() string {
|
|||
// modeSettingsAddTag is a special mode for adding tags
|
||||
const modeSettingsAddTag viewMode = 100
|
||||
|
||||
// modeSettingsStateForm is the add/edit state form mode.
|
||||
const modeSettingsStateForm viewMode = 102
|
||||
|
||||
// modeSettingsAddState is a special mode for adding states
|
||||
const modeSettingsAddState viewMode = 101
|
||||
|
||||
|
|
|
|||
116
internal/ui/stateform.go
Normal file
116
internal/ui/stateform.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
)
|
||||
|
||||
// stateForm backs the add/edit state form (modeSettingsStateForm).
|
||||
// field: 0 = name, 1 = color, 2 = done.
|
||||
type stateForm struct {
|
||||
editing bool
|
||||
index int // index into config.States.States when editing
|
||||
name textinput.Model
|
||||
color textinput.Model
|
||||
done bool
|
||||
field int
|
||||
}
|
||||
|
||||
// startStateForm initializes the form and switches into the form mode.
|
||||
// editing=false adds a new state; editing=true edits config.States.States[index].
|
||||
func (m *uiModel) startStateForm(editing bool, index int) {
|
||||
name := textinput.New()
|
||||
name.Placeholder = "STATE NAME"
|
||||
name.CharLimit = 32
|
||||
name.Width = 30
|
||||
|
||||
color := textinput.New()
|
||||
color.Placeholder = "0-255"
|
||||
color.CharLimit = 3
|
||||
color.Width = 12
|
||||
|
||||
sf := stateForm{editing: editing, index: index, name: name, color: color}
|
||||
if editing {
|
||||
st := m.config.States.States[index]
|
||||
sf.name.SetValue(st.Name)
|
||||
sf.color.SetValue(st.Color)
|
||||
sf.done = st.Done
|
||||
sf.field = 1 // name is read-only when editing
|
||||
} else {
|
||||
sf.color.SetValue("99")
|
||||
sf.field = 0
|
||||
}
|
||||
|
||||
m.stateForm = sf
|
||||
m.focusStateFormField()
|
||||
m.mode = modeSettingsStateForm
|
||||
}
|
||||
|
||||
// stateFormMinField is the lowest navigable field: name (0) when adding,
|
||||
// color (1) when editing (name is read-only).
|
||||
func (m *uiModel) stateFormMinField() int {
|
||||
if m.stateForm.editing {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// moveStateFormField shifts the focused field by delta, clamped to [min, 2].
|
||||
func (m *uiModel) moveStateFormField(delta int) {
|
||||
f := m.stateForm.field + delta
|
||||
if min := m.stateFormMinField(); f < min {
|
||||
f = min
|
||||
}
|
||||
if f > 2 {
|
||||
f = 2
|
||||
}
|
||||
m.stateForm.field = f
|
||||
m.focusStateFormField()
|
||||
}
|
||||
|
||||
// focusStateFormField focuses the active text field (none for the Done field).
|
||||
func (m *uiModel) focusStateFormField() {
|
||||
m.stateForm.name.Blur()
|
||||
m.stateForm.color.Blur()
|
||||
switch m.stateForm.field {
|
||||
case 0:
|
||||
m.stateForm.name.Focus()
|
||||
case 1:
|
||||
m.stateForm.color.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
// applyStateForm commits the form to config. It mutates config only (no Save).
|
||||
// Returns false (without mutating) if a new state's name is empty.
|
||||
func (m *uiModel) applyStateForm() bool {
|
||||
sf := &m.stateForm
|
||||
color := strings.TrimSpace(sf.color.Value())
|
||||
|
||||
if sf.editing {
|
||||
st := &m.config.States.States[sf.index]
|
||||
if color != "" {
|
||||
st.Color = color
|
||||
}
|
||||
st.Done = sf.done
|
||||
m.setStatus(fmt.Sprintf("Updated state '%s' (saved)", st.Name))
|
||||
return true
|
||||
}
|
||||
|
||||
name := strings.ToUpper(strings.TrimSpace(sf.name.Value()))
|
||||
if name == "" {
|
||||
m.setStatus("State name required")
|
||||
return false
|
||||
}
|
||||
if color == "" {
|
||||
color = "99"
|
||||
}
|
||||
if sf.done {
|
||||
m.config.AddDoneState(name, color)
|
||||
} else {
|
||||
m.config.AddState(name, color)
|
||||
}
|
||||
m.setStatus(fmt.Sprintf("Added state '%s' (saved)", name))
|
||||
return true
|
||||
}
|
||||
106
internal/ui/stateform_test.go
Normal file
106
internal/ui/stateform_test.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/rwejlgaard/org/internal/config"
|
||||
)
|
||||
|
||||
func newTestModel(states []config.StateConfig) *uiModel {
|
||||
return &uiModel{
|
||||
config: &config.Config{States: config.StatesConfig{States: states}},
|
||||
}
|
||||
}
|
||||
|
||||
func inputWith(val string) textinput.Model {
|
||||
ti := textinput.New()
|
||||
ti.SetValue(val)
|
||||
return ti
|
||||
}
|
||||
|
||||
func TestApplyStateFormAddsPlainState(t *testing.T) {
|
||||
m := newTestModel(nil)
|
||||
m.stateForm = stateForm{editing: false, name: inputWith("review"), color: inputWith("202"), done: false}
|
||||
|
||||
if !m.applyStateForm() {
|
||||
t.Fatalf("expected commit to succeed")
|
||||
}
|
||||
if len(m.config.States.States) != 1 {
|
||||
t.Fatalf("expected 1 state, got %d", len(m.config.States.States))
|
||||
}
|
||||
s := m.config.States.States[0]
|
||||
if s.Name != "REVIEW" || s.Color != "202" || s.Done {
|
||||
t.Fatalf("got %+v, want {REVIEW 202 false}", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyStateFormAddsDoneState(t *testing.T) {
|
||||
m := newTestModel(nil)
|
||||
m.stateForm = stateForm{editing: false, name: inputWith("cancelled"), color: inputWith("240"), done: true}
|
||||
|
||||
m.applyStateForm()
|
||||
|
||||
s := m.config.States.States[0]
|
||||
if !s.Done {
|
||||
t.Fatalf("expected Done=true, got %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyStateFormEditUpdatesColorAndDone(t *testing.T) {
|
||||
m := newTestModel([]config.StateConfig{{Name: "TODO", Color: "202"}})
|
||||
m.stateForm = stateForm{editing: true, index: 0, name: inputWith("TODO"), color: inputWith("50"), done: true}
|
||||
|
||||
if !m.applyStateForm() {
|
||||
t.Fatalf("expected commit to succeed")
|
||||
}
|
||||
s := m.config.States.States[0]
|
||||
if s.Name != "TODO" || s.Color != "50" || !s.Done {
|
||||
t.Fatalf("got %+v, want {TODO 50 true}", s)
|
||||
}
|
||||
if len(m.config.States.States) != 1 {
|
||||
t.Fatalf("edit must not add a state, got %d", len(m.config.States.States))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyStateFormEmptyNameRejected(t *testing.T) {
|
||||
m := newTestModel(nil)
|
||||
m.stateForm = stateForm{editing: false, name: inputWith(" "), color: inputWith("202")}
|
||||
|
||||
if m.applyStateForm() {
|
||||
t.Fatalf("expected commit to fail on empty name")
|
||||
}
|
||||
if len(m.config.States.States) != 0 {
|
||||
t.Fatalf("expected no state added, got %d", len(m.config.States.States))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveStateFormFieldClampsForEdit(t *testing.T) {
|
||||
m := newTestModel([]config.StateConfig{{Name: "TODO", Color: "202"}})
|
||||
m.startStateForm(true, 0)
|
||||
|
||||
if m.stateForm.field != 1 {
|
||||
t.Fatalf("edit form should start on color (field 1), got %d", m.stateForm.field)
|
||||
}
|
||||
m.moveStateFormField(-5)
|
||||
if m.stateForm.field != 1 {
|
||||
t.Fatalf("edit form must not move above color, got %d", m.stateForm.field)
|
||||
}
|
||||
m.moveStateFormField(5)
|
||||
if m.stateForm.field != 2 {
|
||||
t.Fatalf("field should clamp to 2 (done), got %d", m.stateForm.field)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveStateFormFieldAddStartsAtName(t *testing.T) {
|
||||
m := newTestModel(nil)
|
||||
m.startStateForm(false, 0)
|
||||
|
||||
if m.stateForm.field != 0 {
|
||||
t.Fatalf("add form should start on name (field 0), got %d", m.stateForm.field)
|
||||
}
|
||||
m.moveStateFormField(-5)
|
||||
if m.stateForm.field != 0 {
|
||||
t.Fatalf("add form must not move above name, got %d", m.stateForm.field)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue