Improve entries navigation readability
This commit is contained in:
@@ -73,6 +73,11 @@ type uiSurface struct {
|
||||
Locked bool
|
||||
}
|
||||
|
||||
type emptyState struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
type sessionStatus interface {
|
||||
HasVault() bool
|
||||
IsLocked() bool
|
||||
@@ -1896,38 +1901,78 @@ func (u *ui) chooseExistingFileAction(target *widget.Editor) error {
|
||||
}
|
||||
|
||||
func (u *ui) listEmptyMessage() string {
|
||||
return u.listEmptyState().Body
|
||||
}
|
||||
|
||||
func (u *ui) listEmptyState() emptyState {
|
||||
if surface := u.sessionSurface(); surface.Locked {
|
||||
return "Unlock the vault to browse entries and groups."
|
||||
return emptyState{
|
||||
Title: "Vault locked",
|
||||
Body: "Unlock the vault to browse entries and groups.",
|
||||
}
|
||||
}
|
||||
query := strings.TrimSpace(u.search.Text())
|
||||
if query != "" {
|
||||
switch u.state.Section {
|
||||
case appstate.SectionAPITokens:
|
||||
return fmt.Sprintf("No API tokens match %q. Clear or refine the search.", query)
|
||||
return emptyState{
|
||||
Title: "No matching API tokens",
|
||||
Body: fmt.Sprintf("No API tokens match %q. Clear or refine Search vault to find a token by name, client, or expiration.", query),
|
||||
}
|
||||
case appstate.SectionAPIAudit:
|
||||
return fmt.Sprintf("No audit events match %q. Clear or refine the search.", query)
|
||||
return emptyState{
|
||||
Title: "No matching audit events",
|
||||
Body: fmt.Sprintf("No audit events match %q. Clear or refine Search vault to filter by token, decision, operation, or resource.", query),
|
||||
}
|
||||
case appstate.SectionTemplates:
|
||||
return fmt.Sprintf("No templates match %q. Clear or refine the search.", query)
|
||||
return emptyState{
|
||||
Title: "No matching templates",
|
||||
Body: fmt.Sprintf("No templates match %q. Clear or refine Search vault.", query),
|
||||
}
|
||||
case appstate.SectionRecycleBin:
|
||||
return fmt.Sprintf("No recycle-bin entries match %q. Clear or refine the search.", query)
|
||||
return emptyState{
|
||||
Title: "No matching deleted entries",
|
||||
Body: fmt.Sprintf("No recycle-bin entries match %q. Clear or refine Search vault to look across deleted titles, usernames, URLs, and paths.", query),
|
||||
}
|
||||
default:
|
||||
return fmt.Sprintf("No entries match %q. Clear or refine the search.", query)
|
||||
return emptyState{
|
||||
Title: "No matching entries",
|
||||
Body: fmt.Sprintf("No entries match %q in this view. Clear Search vault, broaden the query, or move to another group.", query),
|
||||
}
|
||||
}
|
||||
}
|
||||
switch u.state.Section {
|
||||
case appstate.SectionAPITokens:
|
||||
return "No API tokens yet. Issue one to grant scoped gRPC access to an external tool."
|
||||
return emptyState{
|
||||
Title: "No API tokens yet",
|
||||
Body: "Issue a token to grant scoped gRPC access to an external tool.",
|
||||
}
|
||||
case appstate.SectionAPIAudit:
|
||||
return "No API audit events yet. Approval prompts, denials, and token actions will appear here."
|
||||
return emptyState{
|
||||
Title: "No API audit events yet",
|
||||
Body: "Approval prompts, denials, and token actions will appear here.",
|
||||
}
|
||||
case appstate.SectionTemplates:
|
||||
return "Templates are not available in this build."
|
||||
return emptyState{
|
||||
Title: "Templates unavailable",
|
||||
Body: "Templates are not available in this build.",
|
||||
}
|
||||
case appstate.SectionRecycleBin:
|
||||
return "Recycle Bin is empty. Deleted entries will appear here until restored."
|
||||
return emptyState{
|
||||
Title: "Recycle Bin is empty",
|
||||
Body: "Deleted entries will appear here until restored.",
|
||||
}
|
||||
default:
|
||||
if len(u.displayPath()) > 0 {
|
||||
return "No entries in this group yet. Add one, search below this point, or open a subgroup."
|
||||
return emptyState{
|
||||
Title: "This group is empty",
|
||||
Body: "Add an entry here, search below this point, or open a subgroup.",
|
||||
}
|
||||
}
|
||||
return emptyState{
|
||||
Title: "No entries yet",
|
||||
Body: "Create or open a vault, then add an entry to get started.",
|
||||
}
|
||||
return "Create or open a vault, then add an entry to get started."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2990,9 +3035,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
|
||||
return u.apiAuditListPanel(gtx)
|
||||
}
|
||||
if len(u.visible) == 0 {
|
||||
lbl := material.Label(u.theme, unit.Sp(16), u.listEmptyMessage())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
}
|
||||
return material.List(u.theme, &u.list).Layout(gtx, len(u.visible), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
item := u.visible[i]
|
||||
@@ -3010,12 +3053,11 @@ func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.sectionBar(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.state.Section != appstate.SectionEntries {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.groupControlsDisclosure(gtx)
|
||||
return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, u.groupControlsDisclosure)
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -3034,37 +3076,38 @@ func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
func (u *ui) sectionBar(gtx layout.Context) layout.Dimensions {
|
||||
tabs := []struct {
|
||||
click *widget.Clickable
|
||||
label string
|
||||
active bool
|
||||
click *widget.Clickable
|
||||
label string
|
||||
compact string
|
||||
active bool
|
||||
}{
|
||||
{click: &u.showEntries, label: "Entries", active: u.state.Section == appstate.SectionEntries},
|
||||
{click: &u.showRecycle, label: "Recycle Bin", active: u.state.Section == appstate.SectionRecycleBin},
|
||||
{click: &u.showAPITokens, label: "API Tokens", active: u.state.Section == appstate.SectionAPITokens},
|
||||
{click: &u.showAPIAudit, label: "API Audit", active: u.state.Section == appstate.SectionAPIAudit},
|
||||
{click: &u.showEntries, label: "Entries", compact: "Entries", active: u.state.Section == appstate.SectionEntries},
|
||||
{click: &u.showRecycle, label: "Recycle Bin", compact: "Recycle", active: u.state.Section == appstate.SectionRecycleBin},
|
||||
{click: &u.showAPITokens, label: "API Tokens", compact: "Tokens", active: u.state.Section == appstate.SectionAPITokens},
|
||||
{click: &u.showAPIAudit, label: "API Audit", compact: "Audit", active: u.state.Section == appstate.SectionAPIAudit},
|
||||
}
|
||||
if u.mode == "phone" {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[0].click, tabs[0].label, tabs[0].active)
|
||||
return layout.Flex{Spacing: layout.SpaceBetween}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[0].click, tabs[0].compact, tabs[0].active)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[1].click, tabs[1].label, tabs[1].active)
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[1].click, tabs[1].compact, tabs[1].active)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[2].click, tabs[2].label, tabs[2].active)
|
||||
return layout.Flex{Spacing: layout.SpaceBetween}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[2].click, tabs[2].compact, tabs[2].active)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[3].click, tabs[3].label, tabs[3].active)
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return sectionTabButton(gtx, u.theme, tabs[3].click, tabs[3].compact, tabs[3].active)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
@@ -3096,56 +3139,100 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
||||
}
|
||||
return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
inset := unit.Dp(12)
|
||||
titleSize := unit.Sp(18)
|
||||
titleSize := unit.Sp(17)
|
||||
metaSize := unit.Sp(14)
|
||||
urlSize := unit.Sp(13)
|
||||
urlSize := unit.Sp(12)
|
||||
pathSize := unit.Sp(11)
|
||||
if u.mode == "phone" {
|
||||
inset = unit.Dp(10)
|
||||
titleSize = unit.Sp(16)
|
||||
metaSize = unit.Sp(13)
|
||||
urlSize = unit.Sp(12)
|
||||
inset = unit.Dp(9)
|
||||
titleSize = unit.Sp(15)
|
||||
metaSize = unit.Sp(12)
|
||||
urlSize = unit.Sp(11)
|
||||
pathSize = unit.Sp(10)
|
||||
}
|
||||
selected := item.ID == u.state.SelectedEntryID
|
||||
focused := u.isFocused(listFocusID(idx))
|
||||
titleColor := accentColor
|
||||
metaColor := color.NRGBA{R: 61, G: 60, B: 56, A: 255}
|
||||
secondaryColor := mutedColor
|
||||
dividerColor := color.NRGBA{R: 225, G: 219, B: 210, A: 255}
|
||||
if selected {
|
||||
titleColor = color.NRGBA{R: 19, G: 57, B: 43, A: 255}
|
||||
metaColor = color.NRGBA{R: 31, G: 53, B: 44, A: 255}
|
||||
secondaryColor = color.NRGBA{R: 72, G: 88, B: 80, A: 255}
|
||||
dividerColor = color.NRGBA{R: 173, G: 196, B: 184, A: 255}
|
||||
} else if focused {
|
||||
metaColor = color.NRGBA{R: 49, G: 74, B: 63, A: 255}
|
||||
secondaryColor = color.NRGBA{R: 86, G: 102, B: 95, A: 255}
|
||||
dividerColor = color.NRGBA{R: 190, G: 208, B: 199, A: 255}
|
||||
}
|
||||
row := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(inset).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
showPath := strings.TrimSpace(u.search.Text()) != "" || len(u.displayPath()) == 0 || u.state.Section == appstate.SectionRecycleBin
|
||||
hasUsername := strings.TrimSpace(item.Username) != ""
|
||||
hasURL := strings.TrimSpace(item.URL) != ""
|
||||
pathText := strings.Join(u.displayEntryPath(item.Path), " / ")
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, titleSize, item.Title)
|
||||
lbl.Color = accentColor
|
||||
lbl.Color = titleColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !hasUsername {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(3)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !hasUsername {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, metaSize, item.Username)
|
||||
lbl.Color = mutedColor
|
||||
lbl.Color = metaColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !hasURL {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(2)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !hasURL {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, urlSize, item.URL)
|
||||
lbl.Color = mutedColor
|
||||
lbl.Color = secondaryColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showPath {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(11), strings.Join(u.displayEntryPath(item.Path), " / "))
|
||||
lbl.Color = mutedColor
|
||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !showPath {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
lbl := material.Label(u.theme, pathSize, pathText)
|
||||
lbl.Color = secondaryColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(7)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
w := gtx.Constraints.Max.X
|
||||
if w < 1 {
|
||||
w = 1
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color.NRGBA{R: 232, G: 227, B: 219, A: 255}, clip.Rect{Max: image.Pt(w, 1)}.Op())
|
||||
paint.FillShape(gtx.Ops, dividerColor, clip.Rect{Max: image.Pt(w, 1)}.Op())
|
||||
return layout.Dimensions{Size: image.Pt(w, 1)}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
if item.ID == u.state.SelectedEntryID || u.isFocused(listFocusID(idx)) {
|
||||
if selected || focused {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Constraints.Min
|
||||
@@ -3155,18 +3242,18 @@ func (u *ui) entryRow(gtx layout.Context, click *widget.Clickable, idx int, item
|
||||
if size.Y == 0 {
|
||||
size.Y = gtx.Constraints.Max.Y
|
||||
}
|
||||
fillColor := selectedColor
|
||||
edgeColor := selectedEdge
|
||||
fillColor := color.NRGBA{R: 212, G: 228, B: 220, A: 255}
|
||||
edgeColor := color.NRGBA{R: 46, G: 106, B: 82, A: 255}
|
||||
if u.state.Section == appstate.SectionRecycleBin {
|
||||
fillColor = color.NRGBA{R: 245, G: 234, B: 226, A: 255}
|
||||
edgeColor = color.NRGBA{R: 144, G: 74, B: 49, A: 255}
|
||||
fillColor = color.NRGBA{R: 244, G: 229, B: 219, A: 255}
|
||||
edgeColor = color.NRGBA{R: 133, G: 65, B: 41, A: 255}
|
||||
}
|
||||
if u.isFocused(listFocusID(idx)) && item.ID != u.state.SelectedEntryID {
|
||||
fillColor = color.NRGBA{R: 235, G: 241, B: 238, A: 255}
|
||||
edgeColor = accentColor
|
||||
if focused && !selected {
|
||||
fillColor = color.NRGBA{R: 231, G: 239, B: 235, A: 255}
|
||||
edgeColor = color.NRGBA{R: 69, G: 118, B: 97, A: 255}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, fillColor, clip.Rect{Max: size}.Op())
|
||||
paint.FillShape(gtx.Ops, edgeColor, clip.Rect{Max: image.Pt(4, size.Y)}.Op())
|
||||
paint.FillShape(gtx.Ops, edgeColor, clip.Rect{Max: image.Pt(5, size.Y)}.Op())
|
||||
return layout.Dimensions{Size: size}
|
||||
}),
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -3794,7 +3881,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
|
||||
btn.TextSize = unit.Sp(11)
|
||||
if u.mode == "phone" {
|
||||
btn.TextSize = unit.Sp(10)
|
||||
btn.Inset = layout.Inset{Top: 4, Bottom: 4, Left: 7, Right: 7}
|
||||
btn.Inset = layout.Inset{Top: 3, Bottom: 3, Left: 6, Right: 6}
|
||||
} else {
|
||||
btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9}
|
||||
}
|
||||
@@ -3823,7 +3910,7 @@ func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
|
||||
return indices
|
||||
}()
|
||||
}
|
||||
if u.mode != "phone" || len(displayPath) <= 2 {
|
||||
if u.mode != "phone" || len(displayPath) <= 1 {
|
||||
crumbs := append([]string{"/"}, append([]string{}, displayPath...)...)
|
||||
indices := make([]int, 0, len(crumbs))
|
||||
indices = append(indices, 0)
|
||||
@@ -3832,8 +3919,11 @@ func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
|
||||
}
|
||||
return crumbs, indices
|
||||
}
|
||||
crumbs := []string{"/", "…", displayPath[len(displayPath)-2], displayPath[len(displayPath)-1]}
|
||||
indices := []int{0, len(displayPath) - 2, len(displayPath) - 1, len(displayPath)}
|
||||
if len(displayPath) == 2 {
|
||||
return []string{"/", displayPath[len(displayPath)-1]}, []int{0, len(displayPath)}
|
||||
}
|
||||
crumbs := []string{"/", "…", displayPath[len(displayPath)-1]}
|
||||
indices := []int{0, len(displayPath) - 1, len(displayPath)}
|
||||
return crumbs, indices
|
||||
}
|
||||
|
||||
@@ -4032,6 +4122,24 @@ func compactCard(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||
})
|
||||
}
|
||||
|
||||
func emptyStatePanel(gtx layout.Context, th *material.Theme, state emptyState) layout.Dimensions {
|
||||
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(th, unit.Sp(15), state.Title)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(th, unit.Sp(13), state.Body)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func outlinedFieldState(gtx layout.Context, focused bool, w layout.Widget) layout.Dimensions {
|
||||
appearance := fieldFocusAppearance(gtx.Metric, focused)
|
||||
size := gtx.Constraints.Min
|
||||
@@ -4214,6 +4322,10 @@ func sectionTabButton(gtx layout.Context, th *material.Theme, click *widget.Clic
|
||||
btn.TextSize = unit.Sp(10)
|
||||
btn.Inset = layout.Inset{Top: 4, Bottom: 4, Left: 8, Right: 8}
|
||||
}
|
||||
if gtx.Constraints.Max.X <= gtx.Dp(unit.Dp(220)) {
|
||||
btn.TextSize = unit.Sp(9)
|
||||
btn.Inset = layout.Inset{Top: 4, Bottom: 4, Left: 6, Right: 6}
|
||||
}
|
||||
if active {
|
||||
btn.Background = accentColor
|
||||
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||
|
||||
@@ -3805,6 +3805,86 @@ func TestUITemplateSectionEmptyStateStaysProductSpecific(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIListEmptyStateProvidesSectionSpecificGuidance(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("empty group", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "entry-1", Title: "Root Entry", Path: []string{"Root"}},
|
||||
},
|
||||
})
|
||||
u.showEntriesSection()
|
||||
u.setCurrentPath([]string{"Root", "Empty Group"})
|
||||
|
||||
got := u.listEmptyState()
|
||||
want := emptyState{
|
||||
Title: "This group is empty",
|
||||
Body: "Add an entry here, search below this point, or open a subgroup.",
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("listEmptyState() = %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("recycle search", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
u.showRecycleBinSection()
|
||||
u.search.SetText("orphaned")
|
||||
|
||||
got := u.listEmptyState()
|
||||
want := emptyState{
|
||||
Title: "No matching deleted entries",
|
||||
Body: `No recycle-bin entries match "orphaned". Clear or refine Search vault to look across deleted titles, usernames, URLs, and paths.`,
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("listEmptyState() = %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("api tokens", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
u.showAPITokensSection()
|
||||
|
||||
got := u.listEmptyState()
|
||||
want := emptyState{
|
||||
Title: "No API tokens yet",
|
||||
Body: "Issue a token to grant scoped gRPC access to an external tool.",
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("listEmptyState() = %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUIVisibleBreadcrumbsCompressesAggressivelyOnPhone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("phone", vault.Model{})
|
||||
|
||||
gotCrumbs, gotIndices := u.visibleBreadcrumbs([]string{"Root", "Infrastructure"})
|
||||
if !slices.Equal(gotCrumbs, []string{"/", "Infrastructure"}) {
|
||||
t.Fatalf("visibleBreadcrumbs() crumbs = %v, want [\"/\" Infrastructure]", gotCrumbs)
|
||||
}
|
||||
if !slices.Equal(gotIndices, []int{0, 2}) {
|
||||
t.Fatalf("visibleBreadcrumbs() indices = %v, want [0 2]", gotIndices)
|
||||
}
|
||||
|
||||
gotCrumbs, gotIndices = u.visibleBreadcrumbs([]string{"Root", "Infrastructure", "SSH"})
|
||||
if !slices.Equal(gotCrumbs, []string{"/", "…", "SSH"}) {
|
||||
t.Fatalf("visibleBreadcrumbs() deep crumbs = %v, want [\"/\" \"…\" SSH]", gotCrumbs)
|
||||
}
|
||||
if !slices.Equal(gotIndices, []int{0, 2, 3}) {
|
||||
t.Fatalf("visibleBreadcrumbs() deep indices = %v, want [0 2 3]", gotIndices)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUILocalLifecycleActionErrorsAreVisibleAndSpecific(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -434,13 +434,7 @@ func (u *ui) apiTokenListPanel(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
if len(tokens) == 0 {
|
||||
text := "No API tokens yet."
|
||||
if strings.TrimSpace(u.search.Text()) != "" {
|
||||
text = "No API tokens match the current filter."
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(14), text)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
return emptyStatePanel(gtx, u.theme, u.listEmptyState())
|
||||
}
|
||||
return material.List(u.theme, &u.list).Layout(gtx, len(tokens), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
return u.apiTokenRow(gtx, &u.apiTokenClicks[i], i, tokens[i])
|
||||
|
||||
Reference in New Issue
Block a user