forked from 2sys/shoutdotdev
refactor nav state data structures
This commit is contained in:
parent
29960e0b4e
commit
bef9cc4cca
15 changed files with 323 additions and 189 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2641,6 +2641,7 @@ dependencies = [
|
|||
"futures",
|
||||
"lettre",
|
||||
"oauth2",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"regex",
|
||||
"reqwest 0.12.14",
|
||||
|
|
|
@ -20,6 +20,7 @@ dotenvy = "0.15.7"
|
|||
futures = "0.3.31"
|
||||
lettre = { version = "0.11.12", features = ["tokio1", "serde", "tracing", "tokio1-native-tls"] }
|
||||
oauth2 = "4.4.2"
|
||||
percent-encoding = "2.3.1"
|
||||
rand = "0.8.5"
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
|
|
|
@ -11,6 +11,7 @@ use oauth2::basic::BasicClient;
|
|||
use crate::{
|
||||
app_error::AppError,
|
||||
email::{Mailer, SmtpOptions},
|
||||
nav::NavbarBuilder,
|
||||
sessions::PgStore,
|
||||
settings::Settings,
|
||||
};
|
||||
|
@ -19,8 +20,9 @@ use crate::{
|
|||
pub struct App {
|
||||
pub db_pool: Pool,
|
||||
pub mailer: Mailer,
|
||||
pub reqwest_client: reqwest::Client,
|
||||
pub navbar_template: NavbarBuilder,
|
||||
pub oauth_client: BasicClient,
|
||||
pub reqwest_client: reqwest::Client,
|
||||
pub session_store: PgStore,
|
||||
pub settings: Settings,
|
||||
}
|
||||
|
@ -53,6 +55,7 @@ impl App {
|
|||
Ok(Self {
|
||||
db_pool,
|
||||
mailer,
|
||||
navbar_template: NavbarBuilder::default().with_base_path(&settings.base_path),
|
||||
oauth_client,
|
||||
reqwest_client,
|
||||
session_store,
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
|||
csrf::generate_csrf_token,
|
||||
email::{MailSender as _, Mailer},
|
||||
guards,
|
||||
nav_state::{Breadcrumb, NavState},
|
||||
nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_CHANNELS},
|
||||
schema::channels,
|
||||
settings::Settings,
|
||||
users::CurrentUser,
|
||||
|
@ -69,6 +69,7 @@ pub fn new_router() -> Router<AppState> {
|
|||
|
||||
async fn channels_page(
|
||||
State(Settings { base_path, .. }): State<Settings>,
|
||||
State(navbar_template): State<NavbarBuilder>,
|
||||
DbConn(db_conn): DbConn,
|
||||
Path(team_id): Path<Uuid>,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
|
@ -88,28 +89,29 @@ async fn channels_page(
|
|||
};
|
||||
|
||||
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
||||
let nav_state = NavState::new()
|
||||
.set_base_path(&base_path)
|
||||
.push_team(&team)
|
||||
.push_slug(Breadcrumb {
|
||||
href: "channels".to_string(),
|
||||
label: "Channels".to_string(),
|
||||
})
|
||||
.set_navbar_active_item("channels");
|
||||
#[derive(Template)]
|
||||
#[template(path = "channels.html")]
|
||||
struct ResponseTemplate {
|
||||
base_path: String,
|
||||
breadcrumbs: BreadcrumbTrail,
|
||||
channels: Vec<Channel>,
|
||||
csrf_token: String,
|
||||
nav_state: NavState,
|
||||
navbar: Navbar,
|
||||
}
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
|
||||
.with_i18n_slug("en")
|
||||
.push_slug("Teams", "teams")
|
||||
.push_slug(&team.name, &team.id.simple().to_string())
|
||||
.push_slug("Channels", "channels"),
|
||||
base_path,
|
||||
channels,
|
||||
csrf_token,
|
||||
nav_state,
|
||||
navbar: navbar_template
|
||||
.with_param("team_id", &team.id.simple().to_string())
|
||||
.with_active_item(NAVBAR_ITEM_CHANNELS)
|
||||
.build(),
|
||||
}
|
||||
.render()?,
|
||||
)
|
||||
|
@ -167,6 +169,7 @@ async fn post_new_channel(
|
|||
|
||||
async fn channel_page(
|
||||
State(Settings { base_path, .. }): State<Settings>,
|
||||
State(navbar_template): State<NavbarBuilder>,
|
||||
DbConn(db_conn): DbConn,
|
||||
Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
|
@ -195,18 +198,6 @@ async fn channel_page(
|
|||
};
|
||||
|
||||
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
||||
let nav_state = NavState::new()
|
||||
.set_base_path(&base_path)
|
||||
.push_team(&team)
|
||||
.push_slug(Breadcrumb {
|
||||
href: "channels".to_string(),
|
||||
label: "Channels".to_string(),
|
||||
})
|
||||
.push_slug(Breadcrumb {
|
||||
href: channel.id.simple().to_string(),
|
||||
label: channel.name.clone(),
|
||||
})
|
||||
.set_navbar_active_item("channels");
|
||||
|
||||
match channel.backend_config {
|
||||
BackendConfig::Email(_) => {
|
||||
|
@ -214,16 +205,26 @@ async fn channel_page(
|
|||
#[template(path = "channel-email.html")]
|
||||
struct ResponseTemplate {
|
||||
base_path: String,
|
||||
breadcrumbs: BreadcrumbTrail,
|
||||
channel: Channel,
|
||||
csrf_token: String,
|
||||
nav_state: NavState,
|
||||
navbar: Navbar,
|
||||
}
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
|
||||
.with_i18n_slug("en")
|
||||
.push_slug("Teams", "teams")
|
||||
.push_slug(&team.name, &team.id.simple().to_string())
|
||||
.push_slug("Channels", "channels")
|
||||
.push_slug(&channel.name, &channel.id.simple().to_string()),
|
||||
base_path,
|
||||
channel,
|
||||
csrf_token,
|
||||
nav_state,
|
||||
navbar: navbar_template
|
||||
.with_param("team_id", &team.id.simple().to_string())
|
||||
.with_active_item(NAVBAR_ITEM_CHANNELS)
|
||||
.build(),
|
||||
}
|
||||
.render()?,
|
||||
))
|
||||
|
|
|
@ -25,7 +25,7 @@ mod guards;
|
|||
mod messages;
|
||||
mod middleware;
|
||||
mod migrations;
|
||||
mod nav_state;
|
||||
mod nav;
|
||||
mod projects;
|
||||
mod projects_router;
|
||||
mod router;
|
||||
|
|
230
src/nav.rs
Normal file
230
src/nav.rs
Normal file
|
@ -0,0 +1,230 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use axum::extract::FromRef;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
pub const NAVBAR_ITEM_TEAMS: &str = "teams";
|
||||
pub const NAVBAR_ITEM_PROJECTS: &str = "projects";
|
||||
pub const NAVBAR_ITEM_CHANNELS: &str = "channels";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BreadcrumbTrail {
|
||||
base_path: String,
|
||||
breadcrumbs: Vec<Breadcrumb>,
|
||||
}
|
||||
|
||||
impl BreadcrumbTrail {
|
||||
/// Initialize with a non-empty base path.
|
||||
pub fn from_base_path(base_path: &str) -> Self {
|
||||
Self {
|
||||
base_path: base_path.to_owned(),
|
||||
breadcrumbs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Append an i18n path segment to the base path.
|
||||
pub fn with_i18n_slug(mut self, language_code: &str) -> Self {
|
||||
self.base_path.push('/');
|
||||
self.base_path.push_str(language_code);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a breadcrumb by name and slug. If other breadcrumbs have already
|
||||
/// been added, href will be generated by appending it to the previous href
|
||||
/// as "<previous>/<slug>". Otherwise, it will be appended to the base path
|
||||
/// with i18n slug (if any).
|
||||
pub fn push_slug(mut self, label: &str, slug: &str) -> Self {
|
||||
let href = if let Some(prev_breadcrumb) = self.iter().last() {
|
||||
format!(
|
||||
"{}/{}",
|
||||
prev_breadcrumb.href,
|
||||
percent_encoding::percent_encode(
|
||||
slug.as_bytes(),
|
||||
percent_encoding::NON_ALPHANUMERIC
|
||||
)
|
||||
)
|
||||
} else {
|
||||
format!("{}/{}", self.base_path, slug)
|
||||
};
|
||||
self.breadcrumbs.push(Breadcrumb {
|
||||
label: label.to_owned(),
|
||||
href,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> std::slice::Iter<'_, Breadcrumb> {
|
||||
self.breadcrumbs.iter()
|
||||
}
|
||||
|
||||
/// Get an absolute URI path, starting from the child of the last
|
||||
/// breadcrumb. For example, if the last breadcrumb has an href of
|
||||
/// "/en/teams/team123" and the relative path is "../team456", the result
|
||||
/// will be "/en/teams/team456". If no breadcrumbs exist, the base path
|
||||
/// with i18n slug (if any) will be used.
|
||||
pub fn join(&self, rel_path: &str) -> String {
|
||||
let base = if let Some(breadcrumb) = self.iter().last() {
|
||||
&breadcrumb.href
|
||||
} else {
|
||||
&self.base_path
|
||||
};
|
||||
let mut path_buf: Vec<&str> = base.split('/').collect();
|
||||
for rel_segment in rel_path.split('/') {
|
||||
if rel_segment == "." {
|
||||
continue;
|
||||
} else if rel_segment == ".." {
|
||||
path_buf.pop();
|
||||
} else {
|
||||
path_buf.push(rel_segment);
|
||||
}
|
||||
}
|
||||
path_buf.join("/")
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for BreadcrumbTrail {
|
||||
type Item = Breadcrumb;
|
||||
type IntoIter = std::vec::IntoIter<Breadcrumb>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.breadcrumbs.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Breadcrumb {
|
||||
pub href: String,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NavbarBuilder {
|
||||
base_path: String,
|
||||
items: Vec<NavbarItem>,
|
||||
active_item: Option<String>,
|
||||
params: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl NavbarBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_path: "".to_owned(),
|
||||
items: Vec::new(),
|
||||
active_item: None,
|
||||
params: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_base_path(mut self, base_path: &str) -> Self {
|
||||
self.base_path = base_path.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a navbar item. Subpath is a path relative to the base path, and it
|
||||
/// may contain placeholders for path params, such as "/{lang}/teams".
|
||||
/// The navbar item will only be displayed if all corresponding path params
|
||||
/// are registered using .with_param().
|
||||
pub fn push_item(mut self, id: &str, label: &str, subpath: &str) -> Self {
|
||||
self.items.push(NavbarItem {
|
||||
id: id.to_owned(),
|
||||
href: subpath.to_owned(),
|
||||
label: label.to_owned(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers a path param with the navbar builder.
|
||||
pub fn with_param(mut self, k: &str, v: &str) -> Self {
|
||||
self.params.insert(k.to_owned(), v.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// If a visible navbar item matches the provided ID, it will render as
|
||||
/// active. Calling this method overrides any previously specified value.
|
||||
pub fn with_active_item(mut self, item_id: &str) -> Self {
|
||||
self.active_item = Some(item_id.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Navbar {
|
||||
let mut built_items: Vec<NavbarItem> = Vec::with_capacity(self.items.len());
|
||||
for item in self.items {
|
||||
let path_segments = item.href.split('/');
|
||||
let substituted_segments: Vec<Option<&str>> = path_segments
|
||||
.map(|segment| {
|
||||
if segment.starts_with("{") && segment.ends_with("}") {
|
||||
let param_k = segment[1..segment.len() - 1].trim();
|
||||
self.params.get(param_k).map(|v| v.as_str())
|
||||
} else {
|
||||
Some(segment)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if substituted_segments.iter().all(|segment| segment.is_some()) {
|
||||
built_items.push(NavbarItem {
|
||||
id: item.id,
|
||||
href: format!(
|
||||
"{}{}",
|
||||
self.base_path,
|
||||
substituted_segments
|
||||
.into_iter()
|
||||
.map(|segment| {
|
||||
segment.expect(
|
||||
"should already have checked that all path segments are Some",
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
),
|
||||
label: item.label,
|
||||
});
|
||||
}
|
||||
}
|
||||
Navbar {
|
||||
active_item: self.active_item,
|
||||
items: built_items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NavbarBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
.push_item(NAVBAR_ITEM_TEAMS, "Teams", "/en/teams")
|
||||
.push_item(
|
||||
NAVBAR_ITEM_PROJECTS,
|
||||
"Projects",
|
||||
"/en/teams/{team_id}/projects",
|
||||
)
|
||||
.push_item(
|
||||
NAVBAR_ITEM_CHANNELS,
|
||||
"Channels",
|
||||
"/en/teams/{team_id}/channels",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromRef<S> for NavbarBuilder
|
||||
where
|
||||
S: Into<AppState> + Clone,
|
||||
{
|
||||
fn from_ref(state: &S) -> Self {
|
||||
Into::<AppState>::into(state.clone())
|
||||
.navbar_template
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Navbar {
|
||||
pub items: Vec<NavbarItem>,
|
||||
pub active_item: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NavbarItem {
|
||||
pub href: String,
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
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);
|
||||
self.navbar_active_item = "teams".to_string();
|
||||
self.breadcrumbs.push(Breadcrumb {
|
||||
href: format!("{}/en/teams", self.base_path),
|
||||
label: "Teams".to_string(),
|
||||
});
|
||||
self.breadcrumbs.push(Breadcrumb {
|
||||
href: format!("{}/en/teams/{}", self.base_path, team.id.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!("{}/en/teams/{}/projects", self.base_path, team_id),
|
||||
label: "Projects".to_string(),
|
||||
});
|
||||
self.breadcrumbs.push(Breadcrumb {
|
||||
href: format!(
|
||||
"{}/en/teams/{}/projects/{}",
|
||||
self.base_path,
|
||||
team_id,
|
||||
project.id.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
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ use crate::{
|
|||
channels::Channel,
|
||||
csrf::generate_csrf_token,
|
||||
guards,
|
||||
nav_state::{Breadcrumb, NavState},
|
||||
nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_PROJECTS},
|
||||
projects::Project,
|
||||
schema::channel_selections,
|
||||
settings::Settings,
|
||||
|
@ -40,6 +40,7 @@ pub fn new_router() -> Router<AppState> {
|
|||
|
||||
async fn projects_page(
|
||||
State(Settings { base_path, .. }): State<Settings>,
|
||||
State(navbar_template): State<NavbarBuilder>,
|
||||
DbConn(db_conn): DbConn,
|
||||
Path(team_id): Path<Uuid>,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
|
@ -79,25 +80,26 @@ async fn projects_page(
|
|||
#[template(path = "projects.html")]
|
||||
struct ResponseTemplate {
|
||||
base_path: String,
|
||||
breadcrumbs: BreadcrumbTrail,
|
||||
csrf_token: String,
|
||||
keys: Vec<ApiKey>,
|
||||
nav_state: NavState,
|
||||
navbar: Navbar,
|
||||
projects: Vec<Project>,
|
||||
}
|
||||
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).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 {
|
||||
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
|
||||
.with_i18n_slug("en")
|
||||
.push_slug("Teams", "teams")
|
||||
.push_slug(&team.name, &team.id.simple().to_string())
|
||||
.push_slug("Projects", "projects"),
|
||||
base_path,
|
||||
csrf_token,
|
||||
nav_state,
|
||||
navbar: navbar_template
|
||||
.with_param("team_id", &team.id.simple().to_string())
|
||||
.with_active_item(NAVBAR_ITEM_PROJECTS)
|
||||
.build(),
|
||||
projects,
|
||||
keys: api_keys,
|
||||
}
|
||||
|
@ -108,6 +110,7 @@ async fn projects_page(
|
|||
|
||||
async fn project_page(
|
||||
State(Settings { base_path, .. }): State<Settings>,
|
||||
State(navbar_template): State<NavbarBuilder>,
|
||||
DbConn(db_conn): DbConn,
|
||||
Path((team_id, project_id)): Path<(Uuid, Uuid)>,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
|
@ -153,28 +156,33 @@ async fn project_page(
|
|||
.context("failed to load team channels")?;
|
||||
|
||||
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
||||
let nav_state = NavState::new()
|
||||
.set_base_path(&base_path)
|
||||
.push_team(&team)
|
||||
.push_project(&project)?;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "project.html")]
|
||||
struct ResponseTemplate {
|
||||
base_path: String,
|
||||
breadcrumbs: BreadcrumbTrail,
|
||||
csrf_token: String,
|
||||
enabled_channel_ids: HashSet<Uuid>,
|
||||
nav_state: NavState,
|
||||
navbar: Navbar,
|
||||
project: Project,
|
||||
team_channels: Vec<Channel>,
|
||||
}
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
|
||||
.with_i18n_slug("en")
|
||||
.push_slug("Teams", "teams")
|
||||
.push_slug(&team.name, &team.id.simple().to_string())
|
||||
.push_slug("Projects", "projects")
|
||||
.push_slug(&project.name, &project.id.simple().to_string()),
|
||||
base_path,
|
||||
csrf_token,
|
||||
enabled_channel_ids,
|
||||
project,
|
||||
nav_state,
|
||||
navbar: navbar_template
|
||||
.with_param("team_id", &team.id.simple().to_string())
|
||||
.with_active_item(NAVBAR_ITEM_PROJECTS)
|
||||
.build(),
|
||||
team_channels,
|
||||
}
|
||||
.render()?,
|
||||
|
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
app_state::{AppState, DbConn},
|
||||
csrf::generate_csrf_token,
|
||||
guards,
|
||||
nav_state::{Breadcrumb, NavState},
|
||||
nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_TEAMS},
|
||||
projects::{Project, DEFAULT_PROJECT_NAME},
|
||||
schema::{team_memberships, teams},
|
||||
settings::Settings,
|
||||
|
@ -37,6 +37,7 @@ pub fn new_router() -> Router<AppState> {
|
|||
|
||||
async fn teams_page(
|
||||
State(Settings { base_path, .. }): State<Settings>,
|
||||
State(navbar_template): State<NavbarBuilder>,
|
||||
DbConn(conn): DbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
|
@ -48,24 +49,21 @@ async fn teams_page(
|
|||
.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: "en/teams".to_string(),
|
||||
label: "Teams".to_string(),
|
||||
})
|
||||
.set_navbar_active_item("teams");
|
||||
#[derive(Template)]
|
||||
#[template(path = "teams.html")]
|
||||
struct ResponseTemplate {
|
||||
base_path: String,
|
||||
breadcrumbs: BreadcrumbTrail,
|
||||
navbar: Navbar,
|
||||
teams: Vec<Team>,
|
||||
nav_state: NavState,
|
||||
}
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
|
||||
.with_i18n_slug("en")
|
||||
.push_slug("Teams", "teams"),
|
||||
base_path,
|
||||
nav_state,
|
||||
navbar: navbar_template.with_active_item(NAVBAR_ITEM_TEAMS).build(),
|
||||
teams,
|
||||
}
|
||||
.render()?,
|
||||
|
@ -105,30 +103,28 @@ async fn post_new_api_key(
|
|||
|
||||
async fn new_team_page(
|
||||
State(Settings { base_path, .. }): State<Settings>,
|
||||
State(navbar_template): State<NavbarBuilder>,
|
||||
DbConn(db_conn): DbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
) -> 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: "en/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,
|
||||
breadcrumbs: BreadcrumbTrail,
|
||||
csrf_token: String,
|
||||
nav_state: NavState,
|
||||
navbar: Navbar,
|
||||
}
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
|
||||
.with_i18n_slug("en")
|
||||
.push_slug("New Team", "new-team"),
|
||||
base_path,
|
||||
csrf_token,
|
||||
nav_state,
|
||||
navbar: navbar_template.with_active_item(NAVBAR_ITEM_TEAMS).build(),
|
||||
}
|
||||
.render()?,
|
||||
))
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<nav class="container mt-4" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
{% for breadcrumb in nav_state.breadcrumbs.iter().rev().skip(1).rev() %}
|
||||
{% for breadcrumb in 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() %}
|
||||
{% if let Some(breadcrumb) = breadcrumbs.iter().last() %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ breadcrumb.label }}</li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<section class="mb-4">
|
||||
<form
|
||||
method="post"
|
||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-channel"
|
||||
action="{{ breadcrumbs.join("update-channel") }}"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<label for="channel-name-input" class="form-label">Channel Name</label>
|
||||
|
@ -51,7 +51,7 @@
|
|||
<section class="mb-4">
|
||||
<form
|
||||
method="post"
|
||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-email-recipient"
|
||||
action="{{ breadcrumbs.join("update-email-recipient") }}"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<label for="channel-recipient-input" class="form-label">Recipient Email</label>
|
||||
|
@ -81,7 +81,7 @@
|
|||
<section class="mb-4">
|
||||
<form
|
||||
method="post"
|
||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/verify-email"
|
||||
action="{{ breadcrumbs.join("verify-email") }}"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<label for="channel-recipient-verification-code" class="form-label">
|
||||
|
@ -114,7 +114,7 @@
|
|||
<form
|
||||
id="email-verification-form"
|
||||
method="post"
|
||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}/update-email-recipient"
|
||||
action="{{ breadcrumbs.join("update-email-recipient") }}"
|
||||
>
|
||||
<input type="hidden" name="recipient" value="{{ email_data.recipient }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<li>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/new-channel"
|
||||
action="{{ breadcrumbs.join("../new-channel") }}"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="channel_type" value="email">
|
||||
|
@ -41,7 +41,7 @@
|
|||
Channels are places to send messages, alerts, and so on. Once created, they
|
||||
can be connected to specific projects at the
|
||||
<a
|
||||
href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/projects"
|
||||
href="{{ breadcrumbs.join("../projects") }}"
|
||||
>Projects page</a>.
|
||||
</div>
|
||||
<section class="mb-3">
|
||||
|
@ -50,7 +50,7 @@
|
|||
{% for channel in channels %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}">
|
||||
<a href="{{ breadcrumbs.join(channel.id.simple().to_string().as_str()) }}">
|
||||
{{ channel.name }}
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -14,35 +14,17 @@
|
|||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
{% for item in navbar.items %}
|
||||
<li class="nav-item">
|
||||
<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 }}/en/teams"
|
||||
class="nav-link{% if navbar.active_item == Some(item.id.to_owned()) %} active{% endif %}"
|
||||
{% if navbar.active_item == Some(item.id.to_owned()) %}aria-current="page"{% endif %}
|
||||
href="{{ item.href }}"
|
||||
>
|
||||
Teams
|
||||
{{ item.label }}
|
||||
</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 }}/en/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 }}/en/teams/{{ team_id.simple() }}/channels"
|
||||
>
|
||||
Channels
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 profile-menu">
|
||||
<li class="nav-item dropdown">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<h2>Enabled Channels</h2>
|
||||
<form
|
||||
method="post"
|
||||
action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/projects/{{ project.id.simple() }}/update-enabled-channels"
|
||||
action="{{ breadcrumbs.join("update-enabled-channels") }}"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<table class="table">
|
||||
|
@ -29,7 +29,7 @@
|
|||
<label for="enable-channel-switch-{{ channel.id.simple() }}">
|
||||
<a
|
||||
target="_blank"
|
||||
href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/channels/{{ channel.id.simple() }}"
|
||||
href="{{ breadcrumbs.join(format!("../../channels/{}", channel.id.simple()).as_str()) }}"
|
||||
>
|
||||
{{ channel.name }}
|
||||
</a>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
{% for project in projects %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/projects/{{ project.id.simple() }}">
|
||||
<a href="{{ breadcrumbs.join(project.id.simple().to_string().as_str()) }}">
|
||||
{{ project.name }}
|
||||
</a>
|
||||
</td>
|
||||
|
@ -53,7 +53,7 @@
|
|||
<h1 class="mb-4">API Keys</h1>
|
||||
</section>
|
||||
<section class="mb-3">
|
||||
<form method="post" action="{{ base_path }}/en/teams/{{ nav_state.team_id.unwrap().simple() }}/new-api-key">
|
||||
<form method="post" action="{{ breadcrumbs.join("../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