git-ents.gitmain
⌘K
foforge
commit 24c02f9
feat: 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

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

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; }