Centralize app state ownership in controller

This commit is contained in:
Joe Julian
2026-03-29 11:22:17 -07:00
parent 01559a3a2b
commit b56401b5c6
5 changed files with 186 additions and 40 deletions
+51
View File
@@ -75,6 +75,35 @@ type State struct {
SearchQuery string
SelectedEntryID string
Dirty bool
StatusMessage string
ErrorMessage string
}
func (s *State) ShowSection(section Section) {
s.Section = section
s.CurrentPath = nil
s.SelectedEntryID = ""
}
func (s *State) SetSearchQuery(query string) {
s.SearchQuery = query
}
func (s *State) BeginNewEntry() {
s.SelectedEntryID = ""
s.StatusMessage = "new entry form ready"
s.ErrorMessage = ""
}
func (s *State) SetActionResult(label string, err error) {
if err != nil {
s.ErrorMessage = err.Error()
s.StatusMessage = ""
return
}
s.ErrorMessage = ""
s.StatusMessage = label + " complete"
}
func (s *State) VisibleEntries() ([]vault.Entry, error) {
@@ -467,6 +496,28 @@ func (s *State) NavigateToPath(path []string) {
s.SelectedEntryID = ""
}
func (s *State) DeleteCurrentGroup() error {
session, ok := s.Session.(MutableSession)
if !ok {
return fmt.Errorf("session is not mutable")
}
model, err := session.Current()
if err != nil {
return err
}
if err := model.DeleteGroup(s.CurrentPath); err != nil {
return err
}
session.Replace(model)
if len(s.CurrentPath) > 0 {
s.CurrentPath = append([]string(nil), s.CurrentPath[:len(s.CurrentPath)-1]...)
}
s.Dirty = true
return nil
}
func (s *State) Save() error {
session, ok := s.Session.(SaveableSession)
if !ok {
+112
View File
@@ -884,6 +884,87 @@ func TestChangeMasterKeyMarksStateDirty(t *testing.T) {
}
}
func TestShowSectionResetsPathAndSelection(t *testing.T) {
t.Parallel()
state := State{
Section: SectionEntries,
CurrentPath: []string{"Root", "Internet"},
SelectedEntryID: "git-server",
SearchQuery: "git",
}
state.ShowSection(SectionTemplates)
if state.Section != SectionTemplates {
t.Fatalf("Section = %q, want %q", state.Section, SectionTemplates)
}
if len(state.CurrentPath) != 0 {
t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath)
}
if state.SelectedEntryID != "" {
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
}
if state.SearchQuery != "git" {
t.Fatalf("SearchQuery = %q, want search preserved", state.SearchQuery)
}
}
func TestSetSearchQueryUpdatesControllerSearchState(t *testing.T) {
t.Parallel()
state := State{}
state.SetSearchQuery("lights")
if state.SearchQuery != "lights" {
t.Fatalf("SearchQuery = %q, want %q", state.SearchQuery, "lights")
}
}
func TestBeginNewEntryClearsSelectionAndSetsStatus(t *testing.T) {
t.Parallel()
state := State{
SelectedEntryID: "git-server",
ErrorMessage: "previous error",
}
state.BeginNewEntry()
if state.SelectedEntryID != "" {
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
}
if state.StatusMessage != "new entry form ready" {
t.Fatalf("StatusMessage = %q, want new entry form ready", state.StatusMessage)
}
if state.ErrorMessage != "" {
t.Fatalf("ErrorMessage = %q, want empty", state.ErrorMessage)
}
}
func TestSetActionResultTracksSuccessAndFailureMessages(t *testing.T) {
t.Parallel()
state := State{}
state.SetActionResult("save vault", nil)
if state.StatusMessage != "save vault complete" {
t.Fatalf("StatusMessage = %q, want save vault complete", state.StatusMessage)
}
if state.ErrorMessage != "" {
t.Fatalf("ErrorMessage = %q, want empty on success", state.ErrorMessage)
}
state.SetActionResult("save vault", errors.New("disk full"))
if state.StatusMessage != "" {
t.Fatalf("StatusMessage = %q, want empty on failure", state.StatusMessage)
}
if state.ErrorMessage != "disk full" {
t.Fatalf("ErrorMessage = %q, want disk full", state.ErrorMessage)
}
}
func TestEnterGroupAppendsPathAndClearsSelection(t *testing.T) {
t.Parallel()
@@ -920,6 +1001,37 @@ func TestNavigateToPathReplacesPathAndClearsSelection(t *testing.T) {
}
}
func TestDeleteCurrentGroupMovesToParentAndMarksDirty(t *testing.T) {
t.Parallel()
model := testVaultModel()
model.CreateGroup([]string{"Root"}, "Finance")
sess := &mutableStubSession{model: model}
state := State{
Session: sess,
CurrentPath: []string{"Root", "Finance"},
}
if err := state.DeleteCurrentGroup(); err != nil {
t.Fatalf("DeleteCurrentGroup() error = %v", err)
}
if !slices.Equal(state.CurrentPath, []string{"Root"}) {
t.Fatalf("CurrentPath = %v, want [Root]", state.CurrentPath)
}
if !state.Dirty {
t.Fatal("Dirty = false, want true after DeleteCurrentGroup")
}
got, err := state.ChildGroups()
if err != nil {
t.Fatalf("ChildGroups() error = %v", err)
}
if !slices.Equal(got, []string{"Home Assistant", "Internet"}) {
t.Fatalf("ChildGroups() = %v, want [Home Assistant Internet]", got)
}
}
func TestCreateGroupPersistsGroupAndMarksDirty(t *testing.T) {
t.Parallel()