feat: show a confirmation banner after a browser write lands
commit
24c02f9feat: show a confirmation banner after a browser write lands
Saving settings, posting a comment, and signing out all redirected silently, so nothing on the destination page confirmed the action took effect. Each now appends a one-time query flag (saved, commented, signed_out) that the destination page turns into a success banner.
feat: thread the request query string into the web renderer feat: add a shared notice_banner helper and .notice-success style Assisted-by: Claude:claude-sonnet-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-ents-server/src/http.rs
@@ -42,9 +42,11 @@
let host = header_value(&headers, "Host");
let cookie = header_value(&headers, "Cookie");
let referer = header_value(&headers, "Referer");
+ let query = (!query_string.is_empty()).then_some(query_string.as_str());
return crate::web::render(
&state,
&path_info,
+ query,
host.as_deref(),
cookie.as_deref(),
referer.as_deref(),
crates/git-ents-server/src/web/mod.rs
@@ -57,6 +57,7 @@
pub(crate) async fn render(
state: &AppState,
path: &str,
+ query: Option<&str>,
host: Option<&str>,
cookie: Option<&str>,
referer: Option<&str>,
@@ -72,11 +73,13 @@
None => write::issue_challenge(&state.challenges).ok(),
};
let next = referer_path(referer);
+ let signed_out = query_flag(query, "signed_out");
return login_page(
session.as_ref(),
challenge.as_deref(),
None,
next.as_deref(),
+ signed_out,
)
.into_response();
}
@@ -90,16 +93,7 @@
}
if let Some((repo, rel, rest)) = resolve_repo(&state.data_dir, &segments) {
- return route(
- &repo,
- &rel,
- rest,
- host,
- session,
- editing_enabled(state),
- &state.live_runs,
- )
- .await;
+ return route(state, &repo, &rel, rest, query, host, session).await;
}
not_found().into_response()
@@ -172,8 +166,14 @@
),
Err(error) => {
let challenge = write::issue_challenge(&state.challenges).ok();
- login_page(None, challenge.as_deref(), Some(&error), next.as_deref())
- .into_response()
+ login_page(
+ None,
+ challenge.as_deref(),
+ Some(&error),
+ next.as_deref(),
+ false,
+ )
+ .into_response()
}
};
}
@@ -194,7 +194,7 @@
return redirect("/login", None);
}
write::logout(&state.sessions, cookie);
- return redirect("/login", Some(cleared_cookie(secure)));
+ return redirect("/login?signed_out=1", Some(cleared_cookie(secure)));
}
let Some((repo, rel, rest)) = resolve_repo(&state.data_dir, &segments) else {
@@ -297,7 +297,7 @@
.await;
match result {
- Ok(Ok(())) => redirect(&back, None),
+ Ok(Ok(())) => redirect(&format!("{back}?saved=1"), None),
Ok(Err(error)) => edit_error(&back, &error).into_response(),
Err(_join) => edit_error(&back, "the edit did not complete").into_response(),
}
@@ -377,7 +377,7 @@
.await;
match result {
- Ok(Ok(())) => redirect(&back, None),
+ Ok(Ok(())) => redirect(&format!("{back}?commented=1"), None),
Ok(Err(error)) => edit_error(&back, &error).into_response(),
Err(_join) => edit_error(&back, "the comment did not complete").into_response(),
}
@@ -404,6 +404,22 @@
}
}
+/// Whether `query` (an unparsed `key=value&...` string) carries `key=1` — the
+/// confirmation flag a redirect appends after a write lands, so the page it
+/// sends the visitor back to can show a one-time success notice.
+fn query_flag(query: Option<&str>, key: &str) -> bool {
+ query
+ .into_iter()
+ .flat_map(|q| q.split('&'))
+ .any(|pair| pair == format!("{key}=1"))
+}
+
+/// A one-time success banner for a redirect target, e.g. after a settings save
+/// or a comment post lands.
+pub(super) fn notice_banner(message: &str) -> Markup {
+ html! { p.notice.notice-success { (message) } }
+}
+
/// A `303 See Other` redirect to `location`, optionally setting a cookie.
fn redirect(location: &str, set_cookie: Option<String>) -> Response {
let mut builder = Response::builder()
@@ -451,14 +467,16 @@
///
/// @relation(web.server-rendered, web.tabs)
async fn route(
+ state: &AppState,
repo: &Path,
rel: &str,
rest: &[&str],
+ query: Option<&str>,
host: Option<&str>,
session: Option<write::SessionSnapshot>,
- editing: bool,
- live_runs: &git_effect::engine::LiveRegistry,
) -> Response {
+ let editing = editing_enabled(state);
+ let live_runs = &state.live_runs;
let meta = gather_meta(repo, rel).await;
let auth = resolve_auth(repo, session).await;
match rest.split_first() {
@@ -475,6 +493,7 @@
auth.as_ref(),
editing,
pages::BlobView::Rendered,
+ query_flag(query, "commented"),
)
.await
}
@@ -486,6 +505,7 @@
auth.as_ref(),
editing,
pages::BlobView::Source,
+ query_flag(query, "commented"),
)
.await
}
@@ -508,9 +528,15 @@
Some((&"issues", &[])) => pages::issues_page(repo, &meta, auth.as_ref())
.await
.into_response(),
- Some((&"settings", &[])) => pages::settings_page(repo, &meta, auth.as_ref(), editing)
- .await
- .into_response(),
+ Some((&"settings", &[])) => pages::settings_page(
+ repo,
+ &meta,
+ auth.as_ref(),
+ editing,
+ query_flag(query, "saved"),
+ )
+ .await
+ .into_response(),
_ => not_found().into_response(),
}
}
@@ -794,12 +820,16 @@
challenge: Option<&str>,
error: Option<&str>,
next: Option<&str>,
+ signed_out: bool,
) -> Markup {
page(
"Sign in",
html! {
(account_strip(session))
div.page-header { h1.page-title { "Sign in" } }
+ @if signed_out && session.is_none() {
+ (notice_banner("Signed out."))
+ }
@if let Some(s) = session {
p { "Signed in as " strong { (s.label) } "." }
p.muted { "Edits you make in the browser are attributed to your member key." }
crates/git-ents-server/src/web/pages.rs
@@ -555,6 +555,7 @@
auth: Option<&super::Auth>,
editing: bool,
view: BlobView,
+ commented: bool,
) -> Response {
let rel = &meta.rel;
let Some(path) = browse_path(sub).filter(|p| !p.is_empty()) else {
@@ -606,6 +607,9 @@
}
}
(body)
+ @if commented {
+ (super::notice_banner("Comment posted."))
+ }
(comments_card(&comments, comment_form(rel, &path, auth, editing)))
},
)
@@ -1259,6 +1263,7 @@
meta: &RepoMeta,
auth: Option<&super::Auth>,
editing: bool,
+ saved: bool,
) -> Markup {
let members = component::load::<git_member::members::Member>(repo).await;
let checks = component::load::<git_effect::Effect>(repo).await;
@@ -1275,6 +1280,9 @@
"The repository's configuration on " code { "refs/meta/config" }
" and " code { "refs/meta/members" } "."
}
+ @if saved {
+ (super::notice_banner("Settings saved."))
+ }
(settings_auth_banner(auth, editing))
div.card {
crates/git-ents-server/src/web/style.css
@@ -304,6 +304,9 @@
.release-foot { display: flex; align-items: center; padding: .7rem 1.1rem; border-top: 1px solid var(--color-border); font-family: var(--font-mono); font-size: .74rem; }
.release-foot .sha { margin-left: auto; color: var(--color-text-muted); display: inline-flex; align-items: center; gap: .35rem; }
+.notice { font-size: .9rem; font-weight: 600; padding: .6rem 1.1rem; border-radius: var(--radius-sm); margin-bottom: 1.25rem; }
+.notice-success { color: var(--s-func); background: color-mix(in srgb, var(--s-func) 12%, transparent); border: 1px solid color-mix(in srgb, var(--s-func) 30%, transparent); }
+
.shell-note { color: var(--color-text-muted); font-size: .95rem; max-width: 44rem; margin-bottom: 1.5rem; }
.card .shell-note { padding: .7rem 1.1rem; margin-bottom: 0; }
.shell-note code { font-family: var(--font-mono); font-size: .85em; background: var(--color-code-bg); padding: .1rem .35rem; border-radius: 5px; }