Merge branch 'seg09-search' into merge-main-09-seg09-search
This commit is contained in:
@@ -161,6 +161,19 @@ func (s *State) entriesForSection(model vault.Model) []vault.Entry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s State) SearchPathContext(entry vault.Entry) string {
|
||||||
|
path := slices.Clone(entry.Path)
|
||||||
|
switch s.Section {
|
||||||
|
case SectionTemplates:
|
||||||
|
if len(path) == 0 || path[0] != "Templates" {
|
||||||
|
path = append([]string{"Templates"}, path...)
|
||||||
|
}
|
||||||
|
case SectionRecycleBin:
|
||||||
|
path = append([]string{"Recycle Bin"}, path...)
|
||||||
|
}
|
||||||
|
return strings.Join(path, " / ")
|
||||||
|
}
|
||||||
|
|
||||||
func entriesInPath(entries []vault.Entry, path []string) []vault.Entry {
|
func entriesInPath(entries []vault.Entry, path []string) []vault.Entry {
|
||||||
var out []vault.Entry
|
var out []vault.Entry
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
|
|||||||
@@ -117,6 +117,142 @@ func TestVisibleEntriesUsesRecycleBinSection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVisibleEntriesUsesGlobalSearchWithinTemplateSection(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
state := State{
|
||||||
|
Session: stubSession{
|
||||||
|
model: vault.Model{
|
||||||
|
Templates: []vault.Entry{
|
||||||
|
{ID: "tpl-1", Title: "Website Login", URL: "https://accounts.example.com", Path: []string{"Templates", "Web"}},
|
||||||
|
{ID: "tpl-2", Title: "SSH Login", URL: "ssh://infra.internal", Path: []string{"Templates", "Infra"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Section: SectionTemplates,
|
||||||
|
CurrentPath: []string{"Templates", "Web"},
|
||||||
|
SearchQuery: "infra",
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := state.VisibleEntries()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VisibleEntries() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got) != 1 || got[0].ID != "tpl-2" {
|
||||||
|
t.Fatalf("VisibleEntries() = %#v, want global template search result tpl-2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisibleEntriesResetToCurrentTemplatePathAfterClearingSearch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
state := State{
|
||||||
|
Session: stubSession{
|
||||||
|
model: vault.Model{
|
||||||
|
Templates: []vault.Entry{
|
||||||
|
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}},
|
||||||
|
{ID: "tpl-2", Title: "Email Login", Path: []string{"Templates", "Web"}},
|
||||||
|
{ID: "tpl-3", Title: "SSH Login", Path: []string{"Templates", "Infra"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Section: SectionTemplates,
|
||||||
|
CurrentPath: []string{"Templates", "Web"},
|
||||||
|
SearchQuery: "ssh",
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := state.VisibleEntries()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VisibleEntries() with search error = %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].ID != "tpl-3" {
|
||||||
|
t.Fatalf("VisibleEntries() with search = %#v, want tpl-3", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SearchQuery = ""
|
||||||
|
got, err = state.VisibleEntries()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VisibleEntries() after clearing search error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("len(VisibleEntries()) after clearing search = %d, want 2", len(got))
|
||||||
|
}
|
||||||
|
if titles := []string{got[0].Title, got[1].Title}; !slices.Equal(titles, []string{"Email Login", "Website Login"}) {
|
||||||
|
t.Fatalf("VisibleEntries() after clearing search titles = %v, want [Email Login Website Login]", titles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisibleEntriesUsesGlobalSearchWithinRecycleBin(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
state := State{
|
||||||
|
Session: stubSession{
|
||||||
|
model: vault.Model{
|
||||||
|
RecycleBin: []vault.Entry{
|
||||||
|
{ID: "deleted-1", Title: "Deleted Dynadot", Path: []string{"Root", "Internet"}},
|
||||||
|
{ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Section: SectionRecycleBin,
|
||||||
|
CurrentPath: []string{"Root", "Internet"},
|
||||||
|
SearchQuery: "climate",
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := state.VisibleEntries()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VisibleEntries() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got) != 1 || got[0].ID != "deleted-2" {
|
||||||
|
t.Fatalf("VisibleEntries() = %#v, want global recycle-bin search result deleted-2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchPathContextIncludesSectionRoots(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
section Section
|
||||||
|
entry vault.Entry
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "entries use direct path",
|
||||||
|
section: SectionEntries,
|
||||||
|
entry: vault.Entry{Path: []string{"Root", "Internet"}},
|
||||||
|
want: "Root / Internet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "templates retain templates root",
|
||||||
|
section: SectionTemplates,
|
||||||
|
entry: vault.Entry{Path: []string{"Templates", "Web"}},
|
||||||
|
want: "Templates / Web",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recycle bin prefixes root label",
|
||||||
|
section: SectionRecycleBin,
|
||||||
|
entry: vault.Entry{Path: []string{"Root", "Internet"}},
|
||||||
|
want: "Recycle Bin / Root / Internet",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
state := State{Section: tt.section}
|
||||||
|
if got := state.SearchPathContext(tt.entry); got != tt.want {
|
||||||
|
t.Fatalf("SearchPathContext(%v) = %q, want %q", tt.entry.Path, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestChildGroupsUsesCurrentModelAndCurrentPath(t *testing.T) {
|
func TestChildGroupsUsesCurrentModelAndCurrentPath(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,14 @@ func (u *ui) filteredTitles() []string {
|
|||||||
return titles
|
return titles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) visiblePathContexts() []string {
|
||||||
|
paths := make([]string, 0, len(u.visible))
|
||||||
|
for _, item := range u.visible {
|
||||||
|
paths = append(paths, u.state.SearchPathContext(item))
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) selectedEntry() (entry, bool) {
|
func (u *ui) selectedEntry() (entry, bool) {
|
||||||
for _, item := range u.visible {
|
for _, item := range u.visible {
|
||||||
if item.ID == u.state.SelectedEntryID {
|
if item.ID == u.state.SelectedEntryID {
|
||||||
@@ -862,7 +870,7 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
|||||||
if strings.TrimSpace(u.search.Text()) == "" {
|
if strings.TrimSpace(u.search.Text()) == "" {
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
lbl := material.Label(u.theme, unit.Sp(11), strings.Join(item.Path, " / "))
|
lbl := material.Label(u.theme, unit.Sp(11), u.state.SearchPathContext(item))
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -43,6 +43,88 @@ func TestUIFiltersUsingVaultModelPathsAndSearch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUISearchBehaviorIsConsistentAcrossDesktopAndPhoneLayouts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
modes := []string{"desktop", "phone"}
|
||||||
|
for _, mode := range modes {
|
||||||
|
mode := mode
|
||||||
|
t.Run(mode, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel(mode, vault.Model{
|
||||||
|
Entries: []vault.Entry{
|
||||||
|
{ID: "entry-1", Title: "Git Server", URL: "https://git.julianfamily.org", Path: []string{"Root", "Internet"}},
|
||||||
|
{ID: "entry-2", Title: "HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
|
||||||
|
},
|
||||||
|
Templates: []vault.Entry{
|
||||||
|
{ID: "tpl-1", Title: "Website Login", URL: "https://accounts.example.com", Path: []string{"Templates", "Web"}},
|
||||||
|
{ID: "tpl-2", Title: "SSH Login", URL: "ssh://infra.internal", Path: []string{"Templates", "Infra"}},
|
||||||
|
},
|
||||||
|
RecycleBin: []vault.Entry{
|
||||||
|
{ID: "deleted-1", Title: "Deleted Dynadot", URL: "https://dynadot.example.com", Path: []string{"Root", "Internet"}},
|
||||||
|
{ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
u.showEntriesSection()
|
||||||
|
u.currentPath = []string{"Root", "Internet"}
|
||||||
|
u.search.SetText("climate")
|
||||||
|
u.filter()
|
||||||
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"HVAC"}) {
|
||||||
|
t.Fatalf("entries filteredTitles() = %v, want [HVAC]", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.showTemplatesSection()
|
||||||
|
u.currentPath = []string{"Templates", "Web"}
|
||||||
|
u.search.SetText("infra")
|
||||||
|
u.filter()
|
||||||
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"SSH Login"}) {
|
||||||
|
t.Fatalf("templates filteredTitles() = %v, want [SSH Login]", got)
|
||||||
|
}
|
||||||
|
if got := u.visiblePathContexts(); !slices.Equal(got, []string{"Templates / Infra"}) {
|
||||||
|
t.Fatalf("templates visiblePathContexts() = %v, want [Templates / Infra]", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.showRecycleBinSection()
|
||||||
|
u.search.SetText("climate")
|
||||||
|
u.filter()
|
||||||
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Deleted HVAC"}) {
|
||||||
|
t.Fatalf("recycle filteredTitles() = %v, want [Deleted HVAC]", got)
|
||||||
|
}
|
||||||
|
if got := u.visiblePathContexts(); !slices.Equal(got, []string{"Recycle Bin / Root / Home"}) {
|
||||||
|
t.Fatalf("recycle visiblePathContexts() = %v, want [Recycle Bin / Root / Home]", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIClearingSearchResetsToCurrentSectionListing(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel("desktop", vault.Model{
|
||||||
|
Templates: []vault.Entry{
|
||||||
|
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}},
|
||||||
|
{ID: "tpl-2", Title: "Email Login", Path: []string{"Templates", "Web"}},
|
||||||
|
{ID: "tpl-3", Title: "SSH Login", Path: []string{"Templates", "Infra"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
u.showTemplatesSection()
|
||||||
|
u.currentPath = []string{"Templates", "Web"}
|
||||||
|
u.search.SetText("ssh")
|
||||||
|
u.filter()
|
||||||
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"SSH Login"}) {
|
||||||
|
t.Fatalf("filteredTitles() with search = %v, want [SSH Login]", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.search.SetText("")
|
||||||
|
u.filter()
|
||||||
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Email Login", "Website Login"}) {
|
||||||
|
t.Fatalf("filteredTitles() after clearing search = %v, want [Email Login Website Login]", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIChildGroupsComeFromVaultModel(t *testing.T) {
|
func TestUIChildGroupsComeFromVaultModel(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user