forked from 2sys/shoutdotdev
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,
|
ClientSecret, CsrfToken, RedirectUrl, TokenResponse, TokenUrl,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
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};
|
use crate::{app_error::AppError, app_state::AppState, schema, settings::Settings};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ mod auth;
|
||||||
mod csrf;
|
mod csrf;
|
||||||
mod guards;
|
mod guards;
|
||||||
mod messages;
|
mod messages;
|
||||||
|
mod nav_state;
|
||||||
mod projects;
|
mod projects;
|
||||||
mod router;
|
mod router;
|
||||||
mod schema;
|
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,
|
auth,
|
||||||
csrf::generate_csrf_token,
|
csrf::generate_csrf_token,
|
||||||
guards,
|
guards,
|
||||||
|
nav_state::{Breadcrumb, NavState},
|
||||||
projects::Project,
|
projects::Project,
|
||||||
schema,
|
schema,
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
team_memberships::TeamMembership,
|
team_memberships::TeamMembership,
|
||||||
teams::Team,
|
teams::Team,
|
||||||
users::{CurrentUser, User},
|
users::CurrentUser,
|
||||||
v0_router,
|
v0_router,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -74,17 +75,24 @@ async fn teams_page(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.context("failed to load team memberships")
|
.context("failed to load team memberships")
|
||||||
.map(|memberships| memberships.into_iter().map(|(_, team)| team).collect())?;
|
.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)]
|
#[derive(Template)]
|
||||||
#[template(path = "teams.html")]
|
#[template(path = "teams.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
base_path: String,
|
base_path: String,
|
||||||
teams: Vec<Team>,
|
teams: Vec<Team>,
|
||||||
current_user: User,
|
nav_state: NavState,
|
||||||
}
|
}
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
base_path,
|
base_path,
|
||||||
current_user,
|
nav_state,
|
||||||
teams,
|
teams,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
|
@ -129,18 +137,25 @@ async fn new_team_page(
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
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)]
|
#[derive(Template)]
|
||||||
#[template(path = "new-team.html")]
|
#[template(path = "new-team.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
base_path: String,
|
base_path: String,
|
||||||
csrf_token: String,
|
csrf_token: String,
|
||||||
current_user: User,
|
nav_state: NavState,
|
||||||
}
|
}
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
base_path,
|
base_path,
|
||||||
csrf_token,
|
csrf_token,
|
||||||
current_user,
|
nav_state,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
))
|
))
|
||||||
|
@ -211,18 +226,24 @@ async fn projects_page(
|
||||||
base_path: String,
|
base_path: String,
|
||||||
csrf_token: String,
|
csrf_token: String,
|
||||||
keys: Vec<ApiKey>,
|
keys: Vec<ApiKey>,
|
||||||
|
nav_state: NavState,
|
||||||
projects: Vec<Project>,
|
projects: Vec<Project>,
|
||||||
team: Team,
|
|
||||||
current_user: User,
|
|
||||||
}
|
}
|
||||||
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id.clone())).await?;
|
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(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
base_path,
|
base_path,
|
||||||
csrf_token,
|
csrf_token,
|
||||||
current_user,
|
nav_state,
|
||||||
projects,
|
projects,
|
||||||
team,
|
|
||||||
keys: api_keys,
|
keys: api_keys,
|
||||||
}
|
}
|
||||||
.render()?,
|
.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">
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="{{ base_path }}/teams">Teams</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>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#">Projects</a>
|
<a
|
||||||
</li>
|
class="nav-link{% if nav_state.navbar_active_item == "channels" %} active{% endif %}"
|
||||||
<li class="nav-item">
|
{% if nav_state.navbar_active_item == "channels" %}aria-current="page"{% endif %}
|
||||||
<a class="nav-link" href="#">Channels</a>
|
href="{{ base_path }}/teams/{{ team_id.simple() }}/channels"
|
||||||
|
>
|
||||||
|
Channels
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 profile-menu">
|
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 profile-menu">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<nav class="container mt-4" aria-label="breadcrumb">
|
{% include "breadcrumbs.html" %}
|
||||||
<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>
|
|
||||||
<main class="mt-4">
|
<main class="mt-4">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -44,7 +38,7 @@
|
||||||
{% for project in projects %}
|
{% for project in projects %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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 }}
|
{{ project.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -59,7 +53,7 @@
|
||||||
<h1 class="mb-4">API Keys</h1>
|
<h1 class="mb-4">API Keys</h1>
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-3">
|
<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 }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<button class="btn btn-primary" type="submit">Generate Key</button>
|
<button class="btn btn-primary" type="submit">Generate Key</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Add table
Reference in a new issue