Add WebDAV lifecycle UI feedback
This commit is contained in:
@@ -465,9 +465,9 @@ func (u *ui) describeActionError(label string, err error) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if errors.Is(err, webdav.ErrConflict) || strings.Contains(err.Error(), webdav.ErrConflict.Error()) {
|
if errors.Is(err, webdav.ErrConflict) || strings.Contains(err.Error(), webdav.ErrConflict.Error()) {
|
||||||
return "Save conflict: the remote vault changed. Reopen it and retry the save."
|
return fmt.Sprintf("%s failed: remote vault changed on the server; reopen the remote vault and retry", label)
|
||||||
}
|
}
|
||||||
return err.Error()
|
return fmt.Sprintf("%s failed: %v", label, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) bannerSurface() uiBanner {
|
func (u *ui) bannerSurface() uiBanner {
|
||||||
@@ -728,6 +728,8 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(u.lifecycleControls),
|
layout.Rigid(u.lifecycleControls),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(u.feedbackBanner),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -758,6 +760,8 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||||
layout.Rigid(u.lifecycleControls),
|
layout.Rigid(u.lifecycleControls),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||||
|
layout.Rigid(u.feedbackBanner),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1295,6 +1299,27 @@ func detailLine(th *material.Theme, label, value string) layout.Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) feedbackBanner(gtx layout.Context) layout.Dimensions {
|
||||||
|
message := u.statusMessage
|
||||||
|
tone := color.NRGBA{R: 231, G: 239, B: 235, A: 255}
|
||||||
|
textColor := accentColor
|
||||||
|
if u.errorMessage != "" {
|
||||||
|
message = u.errorMessage
|
||||||
|
tone = color.NRGBA{R: 248, G: 226, B: 223, A: 255}
|
||||||
|
textColor = color.NRGBA{R: 140, G: 46, B: 34, A: 255}
|
||||||
|
}
|
||||||
|
if message == "" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return layout.Background{}.Layout(gtx, fill(tone), func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(13), message)
|
||||||
|
lbl.Color = textColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) passwordLine(label, value string) layout.Widget {
|
func (u *ui) passwordLine(label, value string) layout.Widget {
|
||||||
return func(gtx layout.Context) layout.Dimensions {
|
return func(gtx layout.Context) layout.Dimensions {
|
||||||
icon := u.eyeIcon
|
icon := u.eyeIcon
|
||||||
|
|||||||
@@ -478,6 +478,99 @@ func TestUIOpenRemoteAndSaveThroughConfiguredWebDAVTarget(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIOpenRemoteReportsTransportFailure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||||
|
url := server.URL
|
||||||
|
server.Close()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.masterPassword.SetText("correct horse battery staple")
|
||||||
|
u.remoteBaseURL.SetText(url)
|
||||||
|
u.remotePath.SetText("vaults/main.kdbx")
|
||||||
|
|
||||||
|
u.runAction("open remote vault", u.openRemoteAction)
|
||||||
|
|
||||||
|
if got := u.errorMessage; !strings.Contains(got, "open remote vault failed:") {
|
||||||
|
t.Fatalf("errorMessage = %q, want open remote vault failure", got)
|
||||||
|
}
|
||||||
|
if got := u.statusMessage; got != "" {
|
||||||
|
t.Fatalf("statusMessage = %q, want empty on remote open failure", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
||||||
|
model := vault.Model{
|
||||||
|
Entries: []vault.Entry{
|
||||||
|
{
|
||||||
|
ID: "vault-console",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Username: "dannyocean",
|
||||||
|
Password: "token-1",
|
||||||
|
URL: "https://vault.crew.example.invalid",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var putCount int
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if err := vault.SaveKDBXWithKey(&encoded, model, key); err != nil {
|
||||||
|
t.Fatalf("SaveKDBXWithKey() error = %v", err)
|
||||||
|
}
|
||||||
|
w.Header().Set("ETag", "\"v1\"")
|
||||||
|
_, _ = w.Write(encoded.Bytes())
|
||||||
|
case http.MethodPut:
|
||||||
|
putCount++
|
||||||
|
w.WriteHeader(http.StatusPreconditionFailed)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected method %s", r.Method)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.masterPassword.SetText("correct horse battery staple")
|
||||||
|
u.remoteBaseURL.SetText(server.URL)
|
||||||
|
u.remotePath.SetText("vaults/main.kdbx")
|
||||||
|
|
||||||
|
if err := u.openRemoteAction(); err != nil {
|
||||||
|
t.Fatalf("openRemoteAction() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := u.state.UpsertEntry(vault.Entry{
|
||||||
|
ID: "vault-console",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Username: "dannyocean",
|
||||||
|
Password: "token-2",
|
||||||
|
URL: "https://vault.crew.example.invalid",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("UpsertEntry() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.runAction("save vault", u.saveAction)
|
||||||
|
|
||||||
|
if got := u.errorMessage; !strings.Contains(got, "remote vault changed on the server") {
|
||||||
|
t.Fatalf("errorMessage = %q, want visible remote conflict guidance", got)
|
||||||
|
}
|
||||||
|
if got := u.statusMessage; got != "" {
|
||||||
|
t.Fatalf("statusMessage = %q, want empty after remote save conflict", got)
|
||||||
|
}
|
||||||
|
if !u.state.Dirty {
|
||||||
|
t.Fatal("Dirty = false, want true after remote save conflict")
|
||||||
|
}
|
||||||
|
if putCount != 1 {
|
||||||
|
t.Fatalf("remote PUT count = %d, want 1", putCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIMasterKeyInputSupportsKeyFileAndCompositeKeys(t *testing.T) {
|
func TestUIMasterKeyInputSupportsKeyFileAndCompositeKeys(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user