Fix hidden root section restore

This commit is contained in:
Joe Julian
2026-03-30 09:24:41 -07:00
parent b2c26622e8
commit 88151dc780
2 changed files with 253 additions and 189 deletions
+224 -189
View File
@@ -26,8 +26,8 @@ import (
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"git.julianfamily.org/keepassgo/api" "git.julianfamily.org/keepassgo/api"
"git.julianfamily.org/keepassgo/apiaudit"
"git.julianfamily.org/keepassgo/apiapproval" "git.julianfamily.org/keepassgo/apiapproval"
"git.julianfamily.org/keepassgo/apiaudit"
"git.julianfamily.org/keepassgo/apitokens" "git.julianfamily.org/keepassgo/apitokens"
"git.julianfamily.org/keepassgo/appstate" "git.julianfamily.org/keepassgo/appstate"
"git.julianfamily.org/keepassgo/clipboard" "git.julianfamily.org/keepassgo/clipboard"
@@ -119,181 +119,182 @@ const (
) )
type ui struct { type ui struct {
mode string mode string
theme *material.Theme theme *material.Theme
search widget.Editor search widget.Editor
vaultPath widget.Editor vaultPath widget.Editor
saveAsPath widget.Editor saveAsPath widget.Editor
remoteBaseURL widget.Editor remoteBaseURL widget.Editor
remotePath widget.Editor remotePath widget.Editor
remoteUsername widget.Editor remoteUsername widget.Editor
remotePassword widget.Editor remotePassword widget.Editor
masterPassword widget.Editor masterPassword widget.Editor
keyFilePath widget.Editor keyFilePath widget.Editor
apiTokenName widget.Editor apiTokenName widget.Editor
apiTokenClientName widget.Editor apiTokenClientName widget.Editor
apiTokenExpiresAt widget.Editor apiTokenExpiresAt widget.Editor
apiPolicyOperation widget.Editor apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor apiPolicyEntryID widget.Editor
securityCipher widget.Editor securityCipher widget.Editor
securityKDF widget.Editor securityKDF widget.Editor
entryID widget.Editor entryID widget.Editor
entryTitle widget.Editor entryTitle widget.Editor
entryUsername widget.Editor entryUsername widget.Editor
entryPassword widget.Editor entryPassword widget.Editor
entryURL widget.Editor entryURL widget.Editor
entryNotes widget.Editor entryNotes widget.Editor
entryTags widget.Editor entryTags widget.Editor
entryPath widget.Editor entryPath widget.Editor
entryFields widget.Editor entryFields widget.Editor
customFieldKeys []widget.Editor customFieldKeys []widget.Editor
customFieldValues []widget.Editor customFieldValues []widget.Editor
historyIndex widget.Editor historyIndex widget.Editor
groupName widget.Editor groupName widget.Editor
passwordProfile widget.Editor passwordProfile widget.Editor
attachmentName widget.Editor attachmentName widget.Editor
attachmentPath widget.Editor attachmentPath widget.Editor
exportAttachmentPath widget.Editor exportAttachmentPath widget.Editor
list widget.List list widget.List
detailList widget.List groupList widget.List
lifecycleList widget.List detailList widget.List
copyUser widget.Clickable lifecycleList widget.List
copyPass widget.Clickable copyUser widget.Clickable
copyURL widget.Clickable copyPass widget.Clickable
lockVault widget.Clickable copyURL widget.Clickable
unlockVault widget.Clickable lockVault widget.Clickable
createVault widget.Clickable unlockVault widget.Clickable
openVault widget.Clickable createVault widget.Clickable
saveVault widget.Clickable openVault widget.Clickable
saveAsVault widget.Clickable saveVault widget.Clickable
openRemote widget.Clickable saveAsVault widget.Clickable
changeMasterKey widget.Clickable openRemote widget.Clickable
synchronizeVault widget.Clickable changeMasterKey widget.Clickable
toggleSyncMenu widget.Clickable synchronizeVault widget.Clickable
openAdvancedSync widget.Clickable toggleSyncMenu widget.Clickable
openSecuritySettings widget.Clickable openAdvancedSync widget.Clickable
closeAdvancedSync widget.Clickable openSecuritySettings widget.Clickable
closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable closeSecuritySettings widget.Clickable
runAdvancedSync widget.Clickable runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable saveSecuritySettings widget.Clickable
editEntry widget.Clickable editEntry widget.Clickable
cancelEdit widget.Clickable cancelEdit widget.Clickable
pickVaultPath widget.Clickable pickVaultPath widget.Clickable
pickKeyFile widget.Clickable pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable pickSyncLocalPath widget.Clickable
addEntry widget.Clickable addEntry widget.Clickable
saveEntry widget.Clickable saveEntry widget.Clickable
duplicateEntry widget.Clickable duplicateEntry widget.Clickable
deleteEntry widget.Clickable deleteEntry widget.Clickable
restoreEntry widget.Clickable restoreEntry widget.Clickable
saveTemplate widget.Clickable saveTemplate widget.Clickable
deleteTemplate widget.Clickable deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable instantiateTemplate widget.Clickable
addAttachment widget.Clickable addAttachment widget.Clickable
replaceAttachment widget.Clickable replaceAttachment widget.Clickable
removeAttachment widget.Clickable removeAttachment widget.Clickable
exportAttachment widget.Clickable exportAttachment widget.Clickable
restoreHistory widget.Clickable restoreHistory widget.Clickable
generatePassword widget.Clickable generatePassword widget.Clickable
createGroup widget.Clickable createGroup widget.Clickable
renameGroup widget.Clickable renameGroup widget.Clickable
deleteGroup widget.Clickable deleteGroup widget.Clickable
confirmDeleteGroup widget.Clickable confirmDeleteGroup widget.Clickable
cancelDeleteGroup widget.Clickable cancelDeleteGroup widget.Clickable
addCustomField widget.Clickable addCustomField widget.Clickable
toggleGroupControls widget.Clickable toggleGroupControls widget.Clickable
togglePasswordInline widget.Clickable togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable toggleSyncPassword widget.Clickable
showEntries widget.Clickable showEntries widget.Clickable
showTemplates widget.Clickable showTemplates widget.Clickable
showRecycle widget.Clickable showRecycle widget.Clickable
showAPITokens widget.Clickable showAPITokens widget.Clickable
showAPIAudit widget.Clickable showAPIAudit widget.Clickable
showLocalLifecycle widget.Clickable showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable showSyncLocal widget.Clickable
showSyncRemote widget.Clickable showSyncRemote widget.Clickable
showSyncPull widget.Clickable showSyncPull widget.Clickable
showSyncPush widget.Clickable showSyncPush widget.Clickable
allowApproval widget.Clickable allowApproval widget.Clickable
denyApproval widget.Clickable denyApproval widget.Clickable
cancelApproval widget.Clickable cancelApproval widget.Clickable
approvalPermanent widget.Bool approvalPermanent widget.Bool
rememberRemoteAuth widget.Bool rememberRemoteAuth widget.Bool
apiPolicyAllow widget.Bool apiPolicyAllow widget.Bool
apiPolicyGroupScopeW widget.Bool apiPolicyGroupScopeW widget.Bool
apiTokenDisabled widget.Bool apiTokenDisabled widget.Bool
entryClicks []widget.Clickable entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable apiPolicyRemoves []widget.Clickable
apiAuditClicks []widget.Clickable apiAuditClicks []widget.Clickable
historyClicks []widget.Clickable historyClicks []widget.Clickable
attachmentClicks []widget.Clickable attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable breadcrumbs []widget.Clickable
groupClicks []widget.Clickable groupClicks []widget.Clickable
recentVaultClicks []widget.Clickable recentVaultClicks []widget.Clickable
recentRemoteClicks []widget.Clickable recentRemoteClicks []widget.Clickable
removeCustomFields []widget.Clickable removeCustomFields []widget.Clickable
state appstate.State state appstate.State
visible []entry visible []entry
currentPath []string currentPath []string
syncedPath []string syncedPath []string
selectedHistoryIndex int selectedHistoryIndex int
showPassword bool showPassword bool
togglePassword widget.Clickable togglePassword widget.Clickable
copyAPITokenSecret widget.Clickable copyAPITokenSecret widget.Clickable
issueAPIToken widget.Clickable issueAPIToken widget.Clickable
saveAPIToken widget.Clickable saveAPIToken widget.Clickable
rotateAPIToken widget.Clickable rotateAPIToken widget.Clickable
disableAPIToken widget.Clickable disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable deleteAPIToken widget.Clickable
addAPIPolicyRule widget.Clickable addAPIPolicyRule widget.Clickable
phoneSplit widget.Float phoneSplit widget.Float
splitDrag gesture.Drag splitDrag gesture.Drag
splitBase float32 splitBase float32
splitStartY float32 splitStartY float32
phoneSpan int phoneSpan int
eyeIcon *widget.Icon eyeIcon *widget.Icon
eyeOffIcon *widget.Icon eyeOffIcon *widget.Icon
copyIcon *widget.Icon copyIcon *widget.Icon
expandMoreIcon *widget.Icon expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon expandLessIcon *widget.Icon
chevronDownIcon *widget.Icon chevronDownIcon *widget.Icon
clipboardWriter clipboard.Writer clipboardWriter clipboard.Writer
loadingMessage string loadingMessage string
lifecycleMode string lifecycleMode string
syncSourceMode syncSourceMode syncSourceMode syncSourceMode
syncDirection syncDirection syncDirection syncDirection
syncLocalPath widget.Editor syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor syncRemotePath widget.Editor
syncRemoteUsername widget.Editor syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor syncRemotePassword widget.Editor
syncDialogOpen bool syncDialogOpen bool
syncMenuOpen bool syncMenuOpen bool
securityDialogOpen bool securityDialogOpen bool
showSyncPassword bool showSyncPassword bool
keyboardFocus focusID keyboardFocus focusID
defaultSaveAsPath string defaultSaveAsPath string
recentVaultsPath string recentVaultsPath string
uiPreferencesPath string uiPreferencesPath string
recentRemotesPath string recentRemotesPath string
editingEntry bool editingEntry bool
groupControlsHidden bool groupControlsHidden bool
recentVaults []string recentVaults []string
recentRemotes []recentRemoteRecord recentRemotes []recentRemoteRecord
recentVaultGroups map[string][]string recentVaultGroups map[string][]string
deleteGroupPath []string deleteGroupPath []string
apiPolicyGroupScope bool apiPolicyGroupScope bool
apiTokenSecret string apiTokenSecret string
selectedAuditIndex int selectedAuditIndex int
statusExpiresAt time.Time statusExpiresAt time.Time
now func() time.Time now func() time.Time
apiHost *api.Host apiHost *api.Host
auditLog *apiaudit.Log auditLog *apiaudit.Log
grpcAddress string grpcAddress string
} }
var ( var (
@@ -375,6 +376,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
list: widget.List{ list: widget.List{
List: layout.List{Axis: layout.Vertical}, List: layout.List{Axis: layout.Vertical},
}, },
groupList: widget.List{
List: layout.List{Axis: layout.Vertical},
},
detailList: widget.List{ detailList: widget.List{
List: layout.List{Axis: layout.Vertical}, List: layout.List{Axis: layout.Vertical},
}, },
@@ -508,7 +512,9 @@ func (u *ui) selectedAttachmentNames() []string {
func (u *ui) showEntriesSection() { func (u *ui) showEntriesSection() {
u.resetPasswordPeek() u.resetPasswordPeek()
preservedPath := append([]string(nil), u.currentPath...)
u.state.ShowSection(appstate.SectionEntries) u.state.ShowSection(appstate.SectionEntries)
u.restoreEntriesPath(preservedPath)
u.filter() u.filter()
} }
@@ -1213,6 +1219,28 @@ func (u *ui) restoreRecentRemoteGroup(baseURL, path string) {
u.enterHiddenVaultRoot() u.enterHiddenVaultRoot()
} }
func (u *ui) restoreEntriesPath(path []string) {
if len(path) == 0 {
u.enterHiddenVaultRoot()
return
}
model, err := u.state.Session.Current()
if err != nil {
u.enterHiddenVaultRoot()
return
}
root := u.hiddenVaultRoot()
if len(path) == 1 && root != "" && path[0] == root {
u.setCurrentPath(path)
return
}
if len(model.EntriesInPath(path)) > 0 || len(model.ChildGroups(path)) > 0 || hasExactGroup(model, path) {
u.setCurrentPath(path)
return
}
u.enterHiddenVaultRoot()
}
func (u *ui) displayPath() []string { func (u *ui) displayPath() []string {
path := append([]string(nil), u.currentPath...) path := append([]string(nil), u.currentPath...)
root := u.hiddenVaultRoot() root := u.hiddenVaultRoot()
@@ -3033,21 +3061,28 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
}), }),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
} }
for i, group := range groups { children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
idx := i const maxGroupListHeight = 160
name := group maxY := gtx.Dp(unit.Dp(maxGroupListHeight))
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { if gtx.Constraints.Max.Y > maxY {
for u.groupClicks[idx].Clicked(gtx) { gtx.Constraints.Max.Y = maxY
u.state.EnterGroup(name)
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.filter()
}
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
}))
if i < len(groups)-1 {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
} }
} 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)
})
})
}))
return children return children
}()...) }()...)
} }
+29
View File
@@ -2198,6 +2198,35 @@ func TestUIAutoEntersSingleVaultRootGroupAndDisplaysSlashRoot(t *testing.T) {
} }
} }
func TestUIShowEntriesSectionRestoresHiddenRootAfterLeavingEntries(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{ID: "1", Title: "Vault Console", Path: []string{"keepass", "Crew", "Internet"}},
{ID: "2", Title: "Home Assistant", Path: []string{"keepass", "Crew", "Home"}},
},
})
u.showEntriesSection()
if got := u.currentPath; !slices.Equal(got, []string{"keepass"}) {
t.Fatalf("currentPath after initial entries section = %v, want [keepass]", got)
}
u.showAPITokensSection()
u.showEntriesSection()
if got := u.currentPath; !slices.Equal(got, []string{"keepass"}) {
t.Fatalf("currentPath after returning to entries = %v, want [keepass]", got)
}
if got := u.displayPath(); len(got) != 0 {
t.Fatalf("displayPath() after returning to entries = %v, want root slash path", got)
}
if got := u.childGroups(); !slices.Equal(got, []string{"Crew"}) {
t.Fatalf("childGroups() after returning to entries = %v, want [Crew]", got)
}
}
func TestUINoteRecentVaultDeduplicatesAndOrdersMostRecentFirst(t *testing.T) { func TestUINoteRecentVaultDeduplicatesAndOrdersMostRecentFirst(t *testing.T) {
t.Parallel() t.Parallel()