Fix entry and group navigation workflows

This commit is contained in:
Joe Julian
2026-04-01 10:24:57 -07:00
parent a4a5ad1579
commit 4eda211666
8 changed files with 389 additions and 8 deletions
+89 -5
View File
@@ -96,6 +96,27 @@ func (m Model) EntriesInPath(path []string) []Entry {
return entries
}
func (m Model) EntriesUnderPath(path []string) []Entry {
var entries []Entry
for _, entry := range m.Entries {
if !hasPathPrefix(entry.Path, path) {
continue
}
entries = append(entries, entry)
}
slices.SortFunc(entries, func(a, b Entry) int {
switch {
case a.Title < b.Title:
return -1
case a.Title > b.Title:
return 1
default:
return 0
}
})
return entries
}
func (m Model) Search(query string) []SearchResult {
query = strings.TrimSpace(strings.ToLower(query))
if query == "" {
@@ -256,13 +277,14 @@ func (m *Model) RestoreEntryVersion(id string, historyIndex int) error {
}
func (m *Model) CreateGroup(parent []string, name string) {
groupPath := append(append([]string(nil), parent...), name)
for _, existing := range m.Groups {
if slices.Equal(existing, groupPath) {
return
groupPath := append([]string(nil), parent...)
for _, part := range splitGroupPath(name) {
groupPath = append(groupPath, part)
if groupPathExists(m.Groups, groupPath) {
continue
}
m.Groups = append(m.Groups, append([]string(nil), groupPath...))
}
m.Groups = append(m.Groups, groupPath)
}
func (m *Model) RenameGroup(path []string, newName string) error {
@@ -310,6 +332,47 @@ func (m *Model) MoveEntry(id string, path []string) error {
return ErrEntryNotFound
}
func (m *Model) MoveGroup(path, parent []string) error {
if len(path) == 0 {
return ErrEntryNotFound
}
if hasPathPrefix(parent, path) {
return ErrEntryNotFound
}
groupName := path[len(path)-1]
newPath := append(append([]string(nil), parent...), groupName)
moved := false
for i := range m.Entries {
if !hasPathPrefix(m.Entries[i].Path, path) {
continue
}
m.Entries[i].Path = append(append([]string(nil), newPath...), m.Entries[i].Path[len(path):]...)
moved = true
}
for i := range m.Templates {
if !hasPathPrefix(m.Templates[i].Path, path) {
continue
}
m.Templates[i].Path = append(append([]string(nil), newPath...), m.Templates[i].Path[len(path):]...)
moved = true
}
for i := range m.Groups {
if !hasPathPrefix(m.Groups[i], path) {
continue
}
m.Groups[i] = append(append([]string(nil), newPath...), m.Groups[i][len(path):]...)
moved = true
}
if !moved {
return ErrEntryNotFound
}
if !groupPathExists(m.Groups, newPath) {
m.Groups = append(m.Groups, append([]string(nil), newPath...))
}
return nil
}
func (m *Model) MoveTemplate(id string, path []string) error {
for i := range m.Templates {
if m.Templates[i].ID != id {
@@ -349,6 +412,27 @@ func hasPathPrefix(path, prefix []string) bool {
return slices.Equal(path[:len(prefix)], prefix)
}
func splitGroupPath(name string) []string {
var parts []string
for _, part := range strings.Split(name, "/") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
parts = append(parts, part)
}
return parts
}
func groupPathExists(groups [][]string, path []string) bool {
for _, existing := range groups {
if slices.Equal(existing, path) {
return true
}
}
return false
}
func mergeEntryTemplate(template, overrides Entry) Entry {
entry := cloneEntry(template)
+51
View File
@@ -41,6 +41,19 @@ func TestEntriesInPathReturnsOnlyDirectEntries(t *testing.T) {
}
}
func TestEntriesUnderPathReturnsDescendantEntries(t *testing.T) {
t.Parallel()
model := testModel()
got := model.EntriesUnderPath([]string{"Crew"})
if len(got) != 3 {
t.Fatalf("len(EntriesUnderPath(Crew)) = %d, want 3", len(got))
}
if got[0].Title != "Bellagio" || got[1].Title != "Surveillance Console" || got[2].Title != "Vault Console" {
t.Fatalf("EntriesUnderPath(Crew) titles = %q, %q, %q", got[0].Title, got[1].Title, got[2].Title)
}
}
func TestSearchReturnsMatchesWithFullPathContext(t *testing.T) {
model := testModel()
@@ -246,6 +259,22 @@ func TestCreateGroupMakesItVisibleAsChildGroup(t *testing.T) {
}
}
func TestCreateGroupSupportsNestedRelativePath(t *testing.T) {
t.Parallel()
model := testModel()
model.CreateGroup([]string{"Crew"}, "Infrastructure / Prod")
got := model.ChildGroups([]string{"Crew"})
if !slices.Equal(got, []string{"Home Assistant", "Infrastructure", "Internet"}) {
t.Fatalf("ChildGroups(Crew) = %v, want [Home Assistant Infrastructure Internet]", got)
}
got = model.ChildGroups([]string{"Crew", "Infrastructure"})
if !slices.Equal(got, []string{"Prod"}) {
t.Fatalf("ChildGroups(Crew/Infrastructure) = %v, want [Prod]", got)
}
}
func TestRenameGroupMovesEntriesAndKeepsHierarchy(t *testing.T) {
model := testModel()
@@ -276,6 +305,28 @@ func TestMoveEntryChangesItsPath(t *testing.T) {
}
}
func TestMoveGroupMovesEntriesAndNestedGroups(t *testing.T) {
t.Parallel()
model := testModel()
model.CreateGroup([]string{"Crew", "Internet"}, "Infrastructure")
if err := model.MoveGroup([]string{"Crew", "Internet"}, []string{"Tricia"}); err != nil {
t.Fatalf("MoveGroup() error = %v", err)
}
got := model.EntriesInPath([]string{"Tricia", "Internet"})
if len(got) != 2 {
t.Fatalf("len(EntriesInPath(Tricia/Internet)) = %d, want 2", len(got))
}
if len(model.EntriesInPath([]string{"Crew", "Internet"})) != 0 {
t.Fatal("EntriesInPath(Crew/Internet) should be empty after move")
}
gotGroups := model.ChildGroups([]string{"Tricia", "Internet"})
if !slices.Equal(gotGroups, []string{"Infrastructure"}) {
t.Fatalf("ChildGroups(Tricia/Internet) = %v, want [Infrastructure]", gotGroups)
}
}
func TestDeleteEmptyGroupRemovesItFromNavigation(t *testing.T) {
model := testModel()