simplify nav state management, sort of
This commit is contained in:
parent
b262d63c02
commit
d593d56ef5
7 changed files with 162 additions and 26 deletions
|
@ -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};
|
||||
|
||||
|
|
|
@ -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
90
src/nav_state.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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()?,
|
||||
|
|
10
templates/breadcrumbs.html
Normal file
10
templates/breadcrumbs.html
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue