Compare commits
5 Commits
cb6fbd05a3
...
a867ac4664
| Author | SHA1 | Date | |
|---|---|---|---|
| a867ac4664 | |||
| 1aab5367a8 | |||
| 5d435f1f1f | |||
| 7868a77c8a | |||
| 43ef58936b |
@@ -95,6 +95,17 @@ These features are product requirements, not “nice to have” ideas.
|
||||
- Phone should optimize for low tap count, not purity of mobile patterns.
|
||||
- The stacked phone layout is the current preferred phone direction.
|
||||
- Do not reintroduce the abandoned phone flow mode unless explicitly requested.
|
||||
- Keep the product feeling like the same application on desktop, Android phone,
|
||||
and Android tablet.
|
||||
- Platform adaptation is allowed for layout and spacing, not for changing the
|
||||
user's mental model of the workflow.
|
||||
- Use the same action names, the same primary next steps, and the same workflow
|
||||
order across platforms unless there is a hard platform constraint.
|
||||
- Treat workflow prominence and reachability as product behavior, not visual
|
||||
polish. A feature is not parity-complete if it technically exists but is
|
||||
harder to discover or reach on one platform.
|
||||
- Prefer shared workflow decisions with platform-specific presentation, rather
|
||||
than platform-specific workflow branches.
|
||||
- Make all test strings `Heist Movie` themed. Use characters, crews, casinos,
|
||||
vaults, and locations from heist movies so test fixtures stay obviously fake
|
||||
and consistent with the product theme.
|
||||
@@ -117,6 +128,14 @@ These features are product requirements, not “nice to have” ideas.
|
||||
implement the minimum code to satisfy them,
|
||||
verify with `go test ./...` and relevant lint checks,
|
||||
and commit each completed behavior.
|
||||
- For cross-platform UI work, behavior tests must cover workflow parity, not
|
||||
just feature or label parity.
|
||||
- For lifecycle, open, unlock, sync, and other primary flows, tests should
|
||||
assert the same conceptual next step across desktop, phone, and tablet
|
||||
layouts.
|
||||
- When Android or phone UX is part of the slice, verify real reachability on an
|
||||
emulator or device for the exact flow being changed. Do not count “the same
|
||||
buttons exist somewhere on screen” as sufficient validation.
|
||||
- Only stop before the requirements are satisfied if the work is genuinely blocked by a missing decision, missing external dependency, or a hard technical constraint that cannot be resolved within the repo.
|
||||
- If blocked, state the blocker concretely and stop only at that point.
|
||||
|
||||
|
||||
@@ -11,6 +11,36 @@ The product is not complete until the global exit criteria at the end of this fi
|
||||
These items came from a hands-on emulator and desktop walkthrough.
|
||||
They should be treated as usability work, not just polish.
|
||||
|
||||
### Cross-Platform Workflow Parity
|
||||
|
||||
These items are required to keep desktop, Android phone, and Android tablet
|
||||
feeling like the same application rather than three related UIs.
|
||||
|
||||
- Workflow parity:
|
||||
define canonical workflows for open, unlock, set up remote sync, use remote
|
||||
sync, browse entries, and edit entries.
|
||||
- Workflow parity:
|
||||
ensure desktop, phone, and tablet use the same action names and the same
|
||||
primary next steps for those workflows.
|
||||
- Workflow parity:
|
||||
remove or reduce platform-specific workflow exceptions where the same user
|
||||
intent currently takes a different route on different form factors.
|
||||
- Testing:
|
||||
add cross-mode behavior tests that assert workflow order and action
|
||||
prominence, not just label presence.
|
||||
- Testing:
|
||||
add explicit lifecycle/open-screen tests for reachability of the primary
|
||||
action on desktop, phone, and tablet layouts.
|
||||
- Testing:
|
||||
add explicit remote-sync workflow tests that prove setup, settings, use, and
|
||||
removal are reachable from the same primary affordance family across modes.
|
||||
- Android verification:
|
||||
validate changed lifecycle/open/sync workflows on the emulator or a device,
|
||||
including with the on-screen keyboard visible.
|
||||
- Android verification:
|
||||
treat “present but below the fold or behind an unexpected branch” as a parity
|
||||
failure, not as acceptable platform variation.
|
||||
|
||||
### Primary Workflow Changes
|
||||
|
||||
These should remain in the main user flow rather than being hidden behind a settings gear.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -281,6 +282,7 @@ type ui struct {
|
||||
detailList widget.List
|
||||
apiPolicyList widget.List
|
||||
lifecycleList widget.List
|
||||
syncDialogList widget.List
|
||||
phonePanelList widget.List
|
||||
securityDialogList widget.List
|
||||
remotePrefsDialogList widget.List
|
||||
@@ -628,6 +630,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
lifecycleList: widget.List{
|
||||
List: layout.List{Axis: layout.Vertical},
|
||||
},
|
||||
syncDialogList: widget.List{
|
||||
List: layout.List{Axis: layout.Vertical},
|
||||
},
|
||||
phonePanelList: widget.List{
|
||||
List: layout.List{Axis: layout.Vertical},
|
||||
},
|
||||
@@ -1421,6 +1426,7 @@ func (u *ui) openAdvancedSyncDialog() {
|
||||
u.syncDialogOpen = true
|
||||
u.syncMenuOpen = false
|
||||
u.showSyncPassword = false
|
||||
u.syncDialogList.Position = layout.Position{}
|
||||
u.syncDialogPurpose = syncDialogPurposeAdvanced
|
||||
u.syncSourceMode = u.syncDefaultSourceMode
|
||||
u.syncDirection = u.syncDefaultDirection
|
||||
@@ -1435,6 +1441,7 @@ func (u *ui) openRemoteSyncSetupDialog() {
|
||||
u.syncDialogOpen = true
|
||||
u.syncMenuOpen = false
|
||||
u.showSyncPassword = false
|
||||
u.syncDialogList.Position = layout.Position{}
|
||||
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
|
||||
u.syncSourceMode = syncSourceRemote
|
||||
u.syncDirection = syncDirectionPush
|
||||
@@ -2319,6 +2326,34 @@ func normalizeRemoteCredentialURL(raw string) string {
|
||||
return raw
|
||||
}
|
||||
|
||||
func remoteCredentialURLMatches(candidate, target string) bool {
|
||||
candidate = normalizeRemoteCredentialURL(candidate)
|
||||
target = normalizeRemoteCredentialURL(target)
|
||||
if candidate == "" || target == "" {
|
||||
return false
|
||||
}
|
||||
if candidate == target {
|
||||
return true
|
||||
}
|
||||
candidateURL, err := url.Parse(candidate)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
targetURL, err := url.Parse(target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !strings.EqualFold(candidateURL.Hostname(), targetURL.Hostname()) {
|
||||
return false
|
||||
}
|
||||
candidatePath := strings.TrimRight(candidateURL.EscapedPath(), "/")
|
||||
targetPath := strings.TrimRight(targetURL.EscapedPath(), "/")
|
||||
if candidatePath == "" || candidatePath == "/" || targetPath == "" || targetPath == "/" {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(targetPath, candidatePath) || strings.HasPrefix(candidatePath, targetPath)
|
||||
}
|
||||
|
||||
func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry {
|
||||
if sanitizeSyncSourceMode(u.syncSourceMode) != syncSourceRemote {
|
||||
return nil
|
||||
@@ -2346,7 +2381,7 @@ func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry {
|
||||
matches = append(matches, entry)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if normalizeRemoteCredentialURL(entry.URL) != baseURL {
|
||||
if !remoteCredentialURLMatches(entry.URL, baseURL) {
|
||||
continue
|
||||
}
|
||||
appendMatch(entry)
|
||||
@@ -2364,7 +2399,7 @@ func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if normalizeRemoteCredentialURL(profile.BaseURL) != baseURL {
|
||||
if !remoteCredentialURLMatches(profile.BaseURL, baseURL) {
|
||||
continue
|
||||
}
|
||||
if remotePath != "" && strings.TrimSpace(profile.Path) != remotePath && strings.TrimSpace(record.Path) != remotePath {
|
||||
@@ -2729,6 +2764,23 @@ func (u *ui) remoteSyncSetupShortcutLabel() string {
|
||||
return "Set Up Remote Sync"
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuActionLabels() []string {
|
||||
labels := []string{"Open Advanced Sync"}
|
||||
if u.shouldShowRemoteSyncSetupShortcut() {
|
||||
labels = append(labels, u.remoteSyncSetupShortcutLabel())
|
||||
}
|
||||
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||
labels = append(labels, u.directRemoteSyncShortcutLabel())
|
||||
}
|
||||
if u.shouldShowRemoteSyncSettingsShortcut() {
|
||||
labels = append(labels, u.remoteSyncSettingsShortcutLabel())
|
||||
}
|
||||
if u.shouldShowRemoveRemoteSyncShortcut() {
|
||||
labels = append(labels, u.removeRemoteSyncShortcutLabel())
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func remoteBindingSuffix(baseURL, path, username string) string {
|
||||
sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username)))
|
||||
return hex.EncodeToString(sum[:8])
|
||||
@@ -5152,7 +5204,7 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
if len(u.syncRemoteCredentialClicks) < len(matchingCredentials) {
|
||||
u.syncRemoteCredentialClicks = make([]widget.Clickable, len(matchingCredentials))
|
||||
}
|
||||
return material.List(u.theme, &u.lifecycleList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
|
||||
return material.List(u.theme, &u.syncDialogList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(20), u.syncDialogTitle())
|
||||
@@ -5628,6 +5680,38 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
||||
}),
|
||||
}
|
||||
if u.shouldShowRemoteSyncSetupShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.shouldShowRemoteSyncSettingsShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.shouldShowRemoveRemoteSyncShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.hasOpenVault() && len(profiles) > 0 && len(credentials) > 0 {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
@@ -6986,43 +7070,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
|
||||
return children
|
||||
}()...)
|
||||
}
|
||||
if !u.shouldShowDirectRemoteSyncShortcut() && !u.shouldShowRemoteSyncSetupShortcut() && !u.shouldShowRemoteSyncSettingsShortcut() && !u.shouldShowRemoveRemoteSyncShortcut() {
|
||||
return crumbBar(gtx)
|
||||
}
|
||||
children := []layout.FlexChild{
|
||||
layout.Rigid(crumbBar),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
}
|
||||
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel())
|
||||
}))
|
||||
}
|
||||
if u.shouldShowRemoteSyncSetupShortcut() {
|
||||
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
}
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel())
|
||||
}))
|
||||
}
|
||||
if u.shouldShowRemoteSyncSettingsShortcut() {
|
||||
if u.shouldShowDirectRemoteSyncShortcut() || u.shouldShowRemoteSyncSetupShortcut() {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
}
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel())
|
||||
}))
|
||||
}
|
||||
if u.shouldShowRemoveRemoteSyncShortcut() {
|
||||
if u.shouldShowDirectRemoteSyncShortcut() || u.shouldShowRemoteSyncSetupShortcut() || u.shouldShowRemoteSyncSettingsShortcut() {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
}
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel())
|
||||
}))
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
return crumbBar(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
|
||||
|
||||
+132
-1
@@ -746,6 +746,20 @@ func TestUILifecycleControlsWithSelectedRecentVaultDoesNotPanic(t *testing.T) {
|
||||
_ = u.lifecycleControls(gtx)
|
||||
}
|
||||
|
||||
func TestUIShouldPrioritizeLifecyclePrimaryActionsOnPhone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
phone := newUIWithSession("phone", &session.Manager{})
|
||||
if !phone.shouldPrioritizeLifecyclePrimaryActions() {
|
||||
t.Fatal("phone.shouldPrioritizeLifecyclePrimaryActions() = false, want true")
|
||||
}
|
||||
|
||||
desktop := newUIWithSession("desktop", &session.Manager{})
|
||||
if desktop.shouldPrioritizeLifecyclePrimaryActions() {
|
||||
t.Fatal("desktop.shouldPrioritizeLifecyclePrimaryActions() = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIRecentVaultListWithSelectedRecentVaultDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -5953,6 +5967,55 @@ func TestUIRemoteSetupShortcutHasParityAcrossModes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUISyncMenuActionLabelsIncludeRemoteSetupForUnboundVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{{
|
||||
ID: "entry-1",
|
||||
Title: "Mint Console",
|
||||
Path: []string{"Crew", "Signals"},
|
||||
}},
|
||||
})
|
||||
u.state.Section = appstate.SectionEntries
|
||||
|
||||
got := u.syncMenuActionLabels()
|
||||
if !slices.Contains(got, "Set Up Remote Sync") {
|
||||
t.Fatalf("syncMenuActionLabels() = %v, want Set Up Remote Sync", got)
|
||||
}
|
||||
if slices.Contains(got, "Use Remote Sync") {
|
||||
t.Fatalf("syncMenuActionLabels() = %v, want no Use Remote Sync without saved binding", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUISyncMenuActionLabelsIncludeSavedRemoteActionsForBoundVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{{
|
||||
ID: "remote-creds-1",
|
||||
Title: "Bellagio WebDAV Sign-In",
|
||||
Username: "linuscaldwell",
|
||||
Path: []string{"Crew", "Signals"},
|
||||
}},
|
||||
RemoteProfiles: []vault.RemoteProfile{{
|
||||
ID: "bellagio-webdav",
|
||||
Name: "Bellagio Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/bellagio/keepass.kdbx",
|
||||
}},
|
||||
})
|
||||
u.state.Section = appstate.SectionEntries
|
||||
|
||||
got := u.syncMenuActionLabels()
|
||||
for _, want := range []string{"Use Remote Sync", "Remote Sync Settings", "Stop Using Remote Sync"} {
|
||||
if !slices.Contains(got, want) {
|
||||
t.Fatalf("syncMenuActionLabels() = %v, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIShouldHideRemoteSyncSetupShortcutWhenSavedBindingExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -5991,7 +6054,13 @@ func TestUIRemoteSyncSetupShortcutLabelUsesClearLanguage(t *testing.T) {
|
||||
func TestUILifecycleRemoteSyncActionLabelUsesSetupLanguageWithoutSavedBinding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
dir := t.TempDir()
|
||||
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
|
||||
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
|
||||
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
|
||||
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||
})
|
||||
u.vaultPath.SetText("/vaults/bellagio.kdbx")
|
||||
|
||||
if !u.shouldShowLifecycleRemoteSyncAction() {
|
||||
@@ -6732,6 +6801,68 @@ func TestUIAdvancedSyncMatchingRemoteCredentialEntriesUsesBaseURL(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAdvancedSyncMatchingRemoteCredentialEntriesUsesMatchingHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "entry-1", Title: "Mint WebDAV", Username: "charliecroker", URL: "https://dav.example.invalid", Path: []string{"Crew", "Signals"}},
|
||||
{ID: "entry-2", Title: "Bellagio WebDAV Sign-In", Username: "linuscaldwell", URL: "https://dav.example.invalid/remote.php/dav", Path: []string{"Crew", "Signals"}},
|
||||
{ID: "entry-3", Title: "Bank Console", Username: "stevefrezelli", URL: "https://insidejob.example.invalid", Path: []string{"Crew", "Signals"}},
|
||||
},
|
||||
})
|
||||
u.syncSourceMode = syncSourceRemote
|
||||
u.syncRemoteBaseURL.SetText("https://dav.example.invalid/remote.php/dav")
|
||||
|
||||
got := u.matchingAdvancedSyncRemoteCredentialEntries()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len(matchingAdvancedSyncRemoteCredentialEntries()) = %d, want 2", len(got))
|
||||
}
|
||||
gotIDs := []string{got[0].ID, got[1].ID}
|
||||
slices.Sort(gotIDs)
|
||||
if !slices.Equal(gotIDs, []string{"entry-1", "entry-2"}) {
|
||||
t.Fatalf("matchingAdvancedSyncRemoteCredentialEntries() ids = %v, want [entry-1 entry-2]", gotIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIRemoteSyncSetupMatchingCredentialsUsesMatchingHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "entry-1", Title: "Mint WebDAV", Username: "charliecroker", URL: "https://dav.example.invalid", Path: []string{"Crew", "Signals"}},
|
||||
{ID: "entry-2", Title: "Bellagio WebDAV Sign-In", Username: "linuscaldwell", URL: "https://dav.example.invalid/remote.php/dav", Path: []string{"Crew", "Signals"}},
|
||||
},
|
||||
})
|
||||
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
|
||||
u.syncSourceMode = syncSourceRemote
|
||||
u.syncDirection = syncDirectionPush
|
||||
u.syncRemoteBaseURL.SetText("https://dav.example.invalid/remote.php/dav")
|
||||
|
||||
got := u.matchingAdvancedSyncRemoteCredentialEntries()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len(matchingAdvancedSyncRemoteCredentialEntries()) = %d, want 2 in remote setup flow", len(got))
|
||||
}
|
||||
gotIDs := []string{got[0].ID, got[1].ID}
|
||||
slices.Sort(gotIDs)
|
||||
if !slices.Equal(gotIDs, []string{"entry-1", "entry-2"}) {
|
||||
t.Fatalf("matchingAdvancedSyncRemoteCredentialEntries() ids = %v, want [entry-1 entry-2] in remote setup flow", gotIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIOpenRemoteSyncSetupDialogResetsDialogScrollPosition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
u.syncDialogList.Position = layout.Position{First: 2, Offset: 48, BeforeEnd: true}
|
||||
|
||||
u.openRemoteSyncSetupDialog()
|
||||
|
||||
if got := u.syncDialogList.Position; got != (layout.Position{}) {
|
||||
t.Fatalf("syncDialogList.Position = %#v, want zero position after opening setup dialog", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAdvancedSyncMatchingRemoteCredentialEntriesUsesSavedBindingForCurrentVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+102
-92
@@ -22,6 +22,92 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
busy := u.lifecycleBusy()
|
||||
showLocalChooser := u.showLocalVaultChooser()
|
||||
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
|
||||
advancedSection := func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(u.lifecycleAdvancedDisclosure),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.lifecycleAdvancedHidden {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.lifecycleMode == "remote" {
|
||||
return 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(u.theme, unit.Sp(13), "Vault settings")
|
||||
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), u.lifecycleSecuritySettingsSummary())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings")
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
primaryActionsSection := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := "Open Vault"
|
||||
if busy {
|
||||
label = "Opening Vault..."
|
||||
}
|
||||
if busy {
|
||||
return passiveTonedButton(gtx, u.theme, label)
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.openVault, label)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.lifecycleRemoteSyncAction, u.lifecycleRemoteSyncActionLabel())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Need a fresh database instead?")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return passiveSectionTab(gtx, u.theme, "Create New Vault", false)
|
||||
}
|
||||
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
|
||||
}),
|
||||
)
|
||||
}
|
||||
selectedVaultSection := func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || selectedLocalPath == "" {
|
||||
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 u.selectedLocalVaultCard(gtx, selectedLocalPath)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
|
||||
@@ -119,106 +205,30 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
return keyFileSelector(u.theme, &u.keyFilePath, &u.pickKeyFile)(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.lifecycleAdvancedDisclosure(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || u.lifecycleAdvancedHidden {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.lifecycleMode == "remote" {
|
||||
return 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(u.theme, unit.Sp(13), "Vault settings")
|
||||
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), u.lifecycleSecuritySettingsSummary())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings")
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.shouldPrioritizeLifecyclePrimaryActions() {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(primaryActionsSection),
|
||||
layout.Rigid(selectedVaultSection),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(advancedSection),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := "Open Vault"
|
||||
if busy {
|
||||
label = "Opening Vault..."
|
||||
}
|
||||
if busy {
|
||||
return passiveTonedButton(gtx, u.theme, label)
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.openVault, label)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return tonedButton(gtx, u.theme, &u.lifecycleRemoteSyncAction, u.lifecycleRemoteSyncActionLabel())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Need a fresh database instead?")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy {
|
||||
return passiveSectionTab(gtx, u.theme, "Create New Vault", false)
|
||||
}
|
||||
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || selectedLocalPath == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if busy || selectedLocalPath == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return u.selectedLocalVaultCard(gtx, selectedLocalPath)
|
||||
}),
|
||||
layout.Rigid(advancedSection),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(primaryActionsSection),
|
||||
layout.Rigid(selectedVaultSection),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) shouldPrioritizeLifecyclePrimaryActions() bool {
|
||||
return u.mode == "phone"
|
||||
}
|
||||
|
||||
func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions {
|
||||
heading := u.selectedRemoteCardHeading()
|
||||
primary := u.selectedRemoteCardPrimaryText()
|
||||
|
||||
Reference in New Issue
Block a user