Clarify group navigation and management
This commit is contained in:
@@ -219,6 +219,8 @@ type ui struct {
|
||||
exportAttachment widget.Clickable
|
||||
restoreHistory widget.Clickable
|
||||
generatePassword widget.Clickable
|
||||
goToRootGroup widget.Clickable
|
||||
goToParentGroup widget.Clickable
|
||||
createGroup widget.Clickable
|
||||
moveGroup widget.Clickable
|
||||
renameGroup widget.Clickable
|
||||
@@ -1506,6 +1508,29 @@ func (u *ui) displayEntryPath(path []string) []string {
|
||||
return append([]string(nil), path[1:]...)
|
||||
}
|
||||
|
||||
func (u *ui) currentGroupDisplayName() string {
|
||||
displayPath := u.displayPath()
|
||||
if len(displayPath) == 0 {
|
||||
return "Vault root (/)"
|
||||
}
|
||||
return strings.Join(displayPath, " / ")
|
||||
}
|
||||
|
||||
func (u *ui) parentGroupDisplayName() string {
|
||||
displayPath := u.displayPath()
|
||||
if len(displayPath) <= 1 {
|
||||
return "Vault root (/)"
|
||||
}
|
||||
return strings.Join(displayPath[:len(displayPath)-1], " / ")
|
||||
}
|
||||
|
||||
func (u *ui) createGroupLabel() string {
|
||||
if len(u.displayPath()) == 0 {
|
||||
return "Create Top-Level Group"
|
||||
}
|
||||
return "Create Subgroup"
|
||||
}
|
||||
|
||||
func pathHasPrefix(path, prefix []string) bool {
|
||||
if len(prefix) > len(path) {
|
||||
return false
|
||||
@@ -3765,49 +3790,109 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
|
||||
if len(u.groupClicks) < len(groups) {
|
||||
u.groupClicks = make([]widget.Clickable, len(groups))
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
|
||||
children := []layout.FlexChild{
|
||||
displayPath := u.displayPath()
|
||||
atRoot := len(displayPath) == 0
|
||||
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 {
|
||||
label := "Groups"
|
||||
if len(u.displayPath()) > 0 {
|
||||
label = "Subgroups"
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "BROWSE GROUPS")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Root anchors the vault, current group sets the listing, and child groups open deeper paths.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Root", "Vault root (/)")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Current Group", u.currentGroupDisplayName())),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if atRoot {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(detailLine(u.theme, "Parent Group", u.parentGroupDisplayName())),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if atRoot {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.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 {
|
||||
for u.goToRootGroup.Clicked(gtx) {
|
||||
root := u.hiddenVaultRoot()
|
||||
if root == "" {
|
||||
u.setCurrentPath(nil)
|
||||
} else {
|
||||
u.setCurrentPath([]string{root})
|
||||
}
|
||||
u.filter()
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.goToRootGroup, "Back to Root")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
for u.goToParentGroup.Clicked(gtx) {
|
||||
u.setCurrentPath(u.currentPath[:len(u.currentPath)-1])
|
||||
u.filter()
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.goToParentGroup, "Up One Group")
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := "CHILD GROUPS"
|
||||
if atRoot {
|
||||
label = "TOP-LEVEL GROUPS"
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(12), label)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
}
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
maxGroupListHeight := 200
|
||||
if u.mode == "phone" {
|
||||
maxGroupListHeight = 112
|
||||
}
|
||||
maxY := gtx.Dp(unit.Dp(maxGroupListHeight))
|
||||
if gtx.Constraints.Max.Y > maxY {
|
||||
gtx.Constraints.Max.Y = maxY
|
||||
}
|
||||
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
}
|
||||
return material.List(u.theme, &u.groupList).Layout(gtx, len(groups), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
idx := i
|
||||
name := groups[i]
|
||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
for u.groupClicks[idx].Clicked(gtx) {
|
||||
u.state.EnterGroup(name)
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.filter()
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if len(groups) == 0 {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "No child groups here yet.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}
|
||||
maxGroupListHeight := 200
|
||||
if u.mode == "phone" {
|
||||
maxGroupListHeight = 112
|
||||
}
|
||||
maxY := gtx.Dp(unit.Dp(maxGroupListHeight))
|
||||
if gtx.Constraints.Max.Y > maxY {
|
||||
gtx.Constraints.Max.Y = maxY
|
||||
}
|
||||
if gtx.Constraints.Min.Y > gtx.Constraints.Max.Y {
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
}
|
||||
return material.List(u.theme, &u.groupList).Layout(gtx, len(groups), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
idx := i
|
||||
name := groups[i]
|
||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
for u.groupClicks[idx].Clicked(gtx) {
|
||||
u.state.EnterGroup(name)
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.filter()
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.groupClicks[idx], "Open "+name)
|
||||
})
|
||||
})
|
||||
})
|
||||
}))
|
||||
return children
|
||||
}()...)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func detailLine(th *material.Theme, label, value string) layout.Widget {
|
||||
|
||||
@@ -1420,6 +1420,37 @@ func TestUIGroupControlsCanBeCollapsed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIGroupNavigationLabelsDistinguishRootCurrentAndParent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Groups: [][]string{{"Root"}, {"Root", "Infrastructure"}, {"Root", "Infrastructure", "Prod"}},
|
||||
})
|
||||
u.showEntriesSection()
|
||||
|
||||
if got := u.currentGroupDisplayName(); got != "Vault root (/)" {
|
||||
t.Fatalf("currentGroupDisplayName() at root = %q, want %q", got, "Vault root (/)")
|
||||
}
|
||||
if got := u.parentGroupDisplayName(); got != "Vault root (/)" {
|
||||
t.Fatalf("parentGroupDisplayName() at root = %q, want %q", got, "Vault root (/)")
|
||||
}
|
||||
if got := u.createGroupLabel(); got != "Create Top-Level Group" {
|
||||
t.Fatalf("createGroupLabel() at root = %q, want %q", got, "Create Top-Level Group")
|
||||
}
|
||||
|
||||
u.setCurrentPath([]string{"Root", "Infrastructure", "Prod"})
|
||||
|
||||
if got := u.currentGroupDisplayName(); got != "Infrastructure / Prod" {
|
||||
t.Fatalf("currentGroupDisplayName() = %q, want %q", got, "Infrastructure / Prod")
|
||||
}
|
||||
if got := u.parentGroupDisplayName(); got != "Infrastructure" {
|
||||
t.Fatalf("parentGroupDisplayName() = %q, want %q", got, "Infrastructure")
|
||||
}
|
||||
if got := u.createGroupLabel(); got != "Create Subgroup" {
|
||||
t.Fatalf("createGroupLabel() = %q, want %q", got, "Create Subgroup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIParentGroupShowsDescendantEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2965,6 +2996,7 @@ func TestUILoadingDetailMessageUsesSelectedVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
u.lifecycleMode = "local"
|
||||
u.vaultPath.SetText("/home/julian/vaults/main.kdbx")
|
||||
u.loadingMessage = "Open vault..."
|
||||
|
||||
|
||||
+30
-2
@@ -593,16 +593,38 @@ func (u *ui) groupControls(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
deletable, deleteReason := u.currentGroupDeletionState()
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "GROUP MANAGEMENT")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Create, rename, move, and delete groups without interrupting the browsing path above.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "CREATE")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
help := "Add a new top-level group."
|
||||
if len(u.displayPath()) > 0 {
|
||||
help = "Add a subgroup inside " + u.currentGroupDisplayName() + "."
|
||||
}
|
||||
lbl := material.Label(u.theme, unit.Sp(12), help)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(labeledEditor(u.theme, "Create Group / Subgroup", &u.groupName, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.createGroup, "Create Group")
|
||||
return tonedButton(gtx, u.theme, &u.createGroup, u.createGroupLabel())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -621,6 +643,12 @@ func (u *ui) groupControls(gtx layout.Context) layout.Dimensions {
|
||||
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(u.theme, unit.Sp(12), "Rename or relocate only the selected group. Browsing stays in the separate group navigator.")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.renameGroup, "Rename Current Group")
|
||||
@@ -726,7 +754,7 @@ func (u *ui) groupControlsDisclosure(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Group tools")
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Group management")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user