simplify nav state management, sort of

This commit is contained in:
Brent Schroeter 2025-02-26 13:10:46 -08:00
parent b262d63c02
commit d593d56ef5
7 changed files with 162 additions and 26 deletions

View file

@ -17,7 +17,7 @@ use oauth2::{
ClientSecret, CsrfToken, RedirectUrl, TokenResponse, TokenUrl,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, span, trace_span, Level};
use tracing::{debug, trace_span};
use crate::{app_error::AppError, app_state::AppState, schema, settings::Settings};

View file

@ -5,6 +5,7 @@ mod auth;
mod csrf;
mod guards;
mod messages;
mod nav_state;
mod projects;
mod router;
mod schema;

90
src/nav_state.rs Normal file
View file

@ -0,0 +1,90 @@
use uuid::Uuid;
use crate::{projects::Project, teams::Team};
#[derive(Clone, Debug, Default)]
pub struct Breadcrumb {
pub href: String,
pub label: String,
}
// TODO: This is a very quick, dirty, and awkward approach to storing
// navigation state. It can and should be scrapped and replaced when time
// allows.
#[derive(Clone, Debug, Default)]
pub struct NavState {
pub base_path: String,
pub breadcrumbs: Vec<Breadcrumb>,
pub team_id: Option<Uuid>,
pub navbar_active_item: String,
}
impl NavState {
pub fn new() -> Self {
Self::default()
}
pub fn set_base_path(mut self, base_path: &str) -> Self {
self.base_path = base_path.to_string();
self
}
pub fn push_team(mut self, team: &Team) -> Self {
self.team_id = Some(team.id.clone());
self.navbar_active_item = "teams".to_string();
self.breadcrumbs.push(Breadcrumb {
href: format!("{}/teams", self.base_path),
label: "Teams".to_string(),
});
self.breadcrumbs.push(Breadcrumb {
href: format!("{}/teams/{}", self.base_path, team.id.clone().simple()),
label: team.name.clone(),
});
self
}
pub fn push_project(mut self, project: &Project) -> Result<Self, anyhow::Error> {
let team_id = self.team_id.ok_or(anyhow::anyhow!(
"NavState.push_project() called out of order"
))?;
self.navbar_active_item = "projects".to_string();
self.breadcrumbs.push(Breadcrumb {
href: format!("{}/teams/{}/projects", self.base_path, team_id),
label: "Projects".to_string(),
});
self.breadcrumbs.push(Breadcrumb {
href: format!(
"{}/teams/{}/projects/{}",
self.base_path,
team_id,
project.id.clone().simple()
),
label: project.name.clone(),
});
Ok(self)
}
/**
* Add a breadcrumb with an href treated as a child of the previous
* breadcrumb's path (or of the base_path if no breadcrumbs exist).
*/
pub fn push_slug(mut self, breadcrumb: Breadcrumb) -> Self {
let starting_path = self
.breadcrumbs
.iter()
.last()
.map(|breadcrumb| breadcrumb.href.clone())
.unwrap_or(self.base_path.clone());
self.breadcrumbs.push(Breadcrumb {
href: format!("{}/{}", starting_path, breadcrumb.href),
label: breadcrumb.label,
});
self
}
pub fn set_navbar_active_item(mut self, value: &str) -> Self {
self.navbar_active_item = value.to_string();
self
}
}

View file

@ -23,12 +23,13 @@ use crate::{
auth,
csrf::generate_csrf_token,
guards,
nav_state::{Breadcrumb, NavState},
projects::Project,
schema,
settings::Settings,
team_memberships::TeamMembership,
teams::Team,
users::{CurrentUser, User},
users::CurrentUser,
v0_router,
};
@ -74,17 +75,24 @@ async fn teams_page(
.unwrap()
.context("failed to load team memberships")
.map(|memberships| memberships.into_iter().map(|(_, team)| team).collect())?;
let nav_state = NavState::new()
.set_base_path(&base_path)
.push_slug(Breadcrumb {
href: "teams".to_string(),
label: "New Team".to_string(),
})
.set_navbar_active_item("teams");
#[derive(Template)]
#[template(path = "teams.html")]
struct ResponseTemplate {
base_path: String,
teams: Vec<Team>,
current_user: User,
nav_state: NavState,
}
Ok(Html(
ResponseTemplate {
base_path,
current_user,
nav_state,
teams,
}
.render()?,
@ -129,18 +137,25 @@ async fn new_team_page(
) -> Result<impl IntoResponse, AppError> {
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
let nav_state = NavState::new()
.set_base_path(&base_path)
.push_slug(Breadcrumb {
href: "new-team".to_string(),
label: "New Team".to_string(),
})
.set_navbar_active_item("teams");
#[derive(Template)]
#[template(path = "new-team.html")]
struct ResponseTemplate {
base_path: String,
csrf_token: String,
current_user: User,
nav_state: NavState,
}
Ok(Html(
ResponseTemplate {
base_path,
csrf_token,
current_user,
nav_state,
}
.render()?,
))
@ -211,18 +226,24 @@ async fn projects_page(
base_path: String,
csrf_token: String,
keys: Vec<ApiKey>,
nav_state: NavState,
projects: Vec<Project>,
team: Team,
current_user: User,
}
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id.clone())).await?;
let nav_state = NavState::new()
.set_base_path(&base_path)
.push_team(&team)
.push_slug(Breadcrumb {
href: "projects".to_string(),
label: "Projects".to_string(),
})
.set_navbar_active_item("projects");
Ok(Html(
ResponseTemplate {
base_path,
csrf_token,
current_user,
nav_state,
projects,
team,
keys: api_keys,
}
.render()?,

View file

@ -0,0 +1,10 @@
<nav class="container mt-4" aria-label="breadcrumb">
<ol class="breadcrumb">
{% for breadcrumb in nav_state.breadcrumbs.iter().rev().skip(1).rev() %}
<li class="breadcrumb-item"><a href="{{ breadcrumb.href }}">{{ breadcrumb.label }}</a></li>
{% endfor %}
{% if let Some(breadcrumb) = nav_state.breadcrumbs.iter().last() %}
<li class="breadcrumb-item active" aria-current="page">{{ breadcrumb.label }}</li>
{% endif %}
</ol>
</nav>

View file

@ -15,14 +15,34 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ base_path }}/teams">Teams</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Projects</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Channels</a>
<a
class="nav-link{% if nav_state.navbar_active_item == "teams" %} active{% endif %}"
{% if nav_state.navbar_active_item == "teams" %}aria-current="page"{% endif %}
href="{{ base_path }}/teams"
>
Teams
</a>
</li>
{% if let Some(team_id) = nav_state.team_id %}
<li class="nav-item">
<a
class="nav-link{% if nav_state.navbar_active_item == "projects" %} active{% endif %}"
{% if nav_state.navbar_active_item == "projects" %}aria-current="page"{% endif %}
href="{{ base_path }}/teams/{{ team_id.simple() }}/projects"
>
Projects
</a>
</li>
<li class="nav-item">
<a
class="nav-link{% if nav_state.navbar_active_item == "channels" %} active{% endif %}"
{% if nav_state.navbar_active_item == "channels" %}aria-current="page"{% endif %}
href="{{ base_path }}/teams/{{ team_id.simple() }}/channels"
>
Channels
</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 profile-menu">
<li class="nav-item dropdown">

View file

@ -1,13 +1,7 @@
{% extends "base.html" %}
{% block main %}
<nav class="container mt-4" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ base_path }}/teams">Teams</a></li>
<li class="breadcrumb-item"><a href="{{ base_path }}/teams/{{ team.id }}">{{ team.name }}</a></li>
<li class="breadcrumb-item active" aria-current="page">Projects</li>
</ol>
</nav>
{% include "breadcrumbs.html" %}
<main class="mt-4">
<div class="container">
<div class="row">
@ -44,7 +38,7 @@
{% for project in projects %}
<tr>
<td>
<a href="{{ base_path }}/teams/{{ team.id.simple() }}/projects/{{ project.id.simple() }}">
<a href="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/projects/{{ project.id.simple() }}">
{{ project.name }}
</a>
</td>
@ -59,7 +53,7 @@
<h1 class="mb-4">API Keys</h1>
</section>
<section class="mb-3">
<form method="post" action="{{ base_path }}/teams/{{ team.id }}/new-api-key">
<form method="post" action="{{ base_path }}/teams/{{ nav_state.team_id.unwrap().simple() }}/new-api-key">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="btn btn-primary" type="submit">Generate Key</button>
</form>