html
This commit is contained in:
12
rust/.sqlx/query-bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d.json
generated
Normal file
12
rust/.sqlx/query-bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE trips_items\n SET pick = ?\n WHERE trip_id = ?\n AND item_id = ?\n AND user_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d"
|
||||
}
|
||||
@@ -406,7 +406,7 @@ impl TripItem {
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips_items
|
||||
SET " => "pick" => "= ?
|
||||
SET pick = ?
|
||||
WHERE trip_id = ?
|
||||
AND item_id = ?
|
||||
AND user_id = ?",
|
||||
|
||||
@@ -5,6 +5,8 @@ use axum::{
|
||||
Form,
|
||||
};
|
||||
|
||||
use crate::view::Component;
|
||||
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -98,12 +100,26 @@ pub struct TripTypeUpdate {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn root(Extension(current_user): Extension<models::user::User>) -> impl IntoResponse {
|
||||
pub async fn root(
|
||||
Extension(current_user): Extension<models::user::User>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
if htmx::is_htmx(&headers) {
|
||||
view::root::Body::init(
|
||||
view::Parent::Root,
|
||||
view::root::BodyArgs {
|
||||
body: &view::home::Home::build(),
|
||||
active_page: None,
|
||||
},
|
||||
)
|
||||
.build(&Context::build(current_user))
|
||||
} else {
|
||||
view::Root::build(
|
||||
&Context::build(current_user),
|
||||
&view::home::Home::build(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -166,13 +182,28 @@ pub async fn inventory_inactive(
|
||||
Extension(current_user): Extension<models::user::User>,
|
||||
State(mut state): State<AppState>,
|
||||
Query(inventory_query): Query<InventoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let ctx = Context::build(current_user);
|
||||
state.client_state.edit_item = inventory_query.edit_item;
|
||||
state.client_state.active_category_id = None;
|
||||
|
||||
let inventory = models::inventory::Inventory::load(&ctx, &state.database_pool).await?;
|
||||
|
||||
if htmx::is_htmx(&headers) {
|
||||
Ok(view::root::Body::init(
|
||||
view::Parent::Root,
|
||||
view::root::BodyArgs {
|
||||
body: &view::inventory::Inventory::build(
|
||||
None,
|
||||
&inventory.categories,
|
||||
state.client_state.edit_item,
|
||||
),
|
||||
|
||||
active_page: Some(&TopLevelPage::Inventory),
|
||||
},
|
||||
)
|
||||
.build(&ctx))
|
||||
} else {
|
||||
Ok(view::Root::build(
|
||||
&ctx,
|
||||
&view::inventory::Inventory::build(
|
||||
@@ -182,6 +213,7 @@ pub async fn inventory_inactive(
|
||||
),
|
||||
Some(&TopLevelPage::Inventory),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -345,15 +377,27 @@ pub async fn trip_create(
|
||||
pub async fn trips(
|
||||
Extension(current_user): Extension<models::user::User>,
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let ctx = Context::build(current_user);
|
||||
let trips = models::trips::Trip::all(&ctx, &state.database_pool).await?;
|
||||
|
||||
if htmx::is_htmx(&headers) {
|
||||
Ok(view::root::Body::init(
|
||||
view::Parent::Root,
|
||||
view::root::BodyArgs {
|
||||
body: &view::trip::TripManager::build(trips),
|
||||
active_page: Some(&TopLevelPage::Trips),
|
||||
},
|
||||
)
|
||||
.build(&ctx))
|
||||
} else {
|
||||
Ok(view::Root::build(
|
||||
&ctx,
|
||||
&view::trip::TripManager::build(trips),
|
||||
Some(&TopLevelPage::Trips),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
||||
@@ -261,31 +261,31 @@ macro_rules! execute {
|
||||
}
|
||||
};
|
||||
|
||||
( $class:expr, $pool:expr, $( $query:expr )=>+, $( $args:tt )*) => {
|
||||
{
|
||||
use tracing::Instrument as _;
|
||||
async {
|
||||
// $crate::sqlite::sqlx_query($class, $( $query )+ , &[]);
|
||||
// println!("haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay: {}", $crate::strip_plus!($(+ $query )+));
|
||||
let result: Result<sqlx::sqlite::SqliteQueryResult, Error> = sqlx::query!(
|
||||
// "x" + "y",
|
||||
$crate::strip_plus!($(+ $query )+),
|
||||
// "UPDATE trips_items
|
||||
// SET " + "pick" +
|
||||
// "= ?
|
||||
// WHERE trip_id = ?
|
||||
// AND item_id = ?
|
||||
// AND user_id = ?",
|
||||
$( $args )*
|
||||
)
|
||||
.execute($pool)
|
||||
.await
|
||||
.map_err(|e| e.into());
|
||||
// ( $class:expr, $pool:expr, $( $query:expr )=>+, $( $args:tt )*) => {
|
||||
// {
|
||||
// use tracing::Instrument as _;
|
||||
// async {
|
||||
// // $crate::sqlite::sqlx_query($class, $( $query )+ , &[]);
|
||||
// // println!("haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay: {}", $crate::strip_plus!($(+ $query )+));
|
||||
// let result: Result<sqlx::sqlite::SqliteQueryResult, Error> = sqlx::query!(
|
||||
// // "x" + "y",
|
||||
// $crate::strip_plus!($(+ $query )+),
|
||||
// // "UPDATE trips_items
|
||||
// // SET " + "pick" +
|
||||
// // "= ?
|
||||
// // WHERE trip_id = ?
|
||||
// // AND item_id = ?
|
||||
// // AND user_id = ?",
|
||||
// $( $args )*
|
||||
// )
|
||||
// .execute($pool)
|
||||
// .await
|
||||
// .map_err(|e| e.into());
|
||||
|
||||
result
|
||||
}.instrument(tracing::info_span!("packager::sql::query", "query"))
|
||||
}
|
||||
};
|
||||
// result
|
||||
// }.instrument(tracing::info_span!("packager::sql::query", "query"))
|
||||
// }
|
||||
// };
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
||||
21
rust/src/view/error.rs
Normal file
21
rust/src/view/error.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use maud::{html, Markup, DOCTYPE};
|
||||
|
||||
pub struct ErrorPage;
|
||||
|
||||
impl ErrorPage {
|
||||
#[tracing::instrument]
|
||||
pub fn build(message: &str) -> Markup {
|
||||
html!(
|
||||
(DOCTYPE)
|
||||
html {
|
||||
head {
|
||||
title { "Packager" }
|
||||
}
|
||||
body {
|
||||
h1 { "Error" }
|
||||
p { (message) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,135 +1,130 @@
|
||||
use super::Context;
|
||||
use std::fmt;
|
||||
|
||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||
use base64::Engine as _;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::Context;
|
||||
use maud::Markup;
|
||||
|
||||
pub mod error;
|
||||
pub mod home;
|
||||
pub mod inventory;
|
||||
pub mod root;
|
||||
pub mod trip;
|
||||
|
||||
pub struct Root;
|
||||
pub use error::ErrorPage;
|
||||
pub use root::Root;
|
||||
|
||||
use crate::TopLevelPage;
|
||||
#[derive(Debug)]
|
||||
pub enum HtmxAction {
|
||||
Get(String),
|
||||
}
|
||||
|
||||
impl Root {
|
||||
impl fmt::Display for HtmxAction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Get(path) => write!(f, "{}", path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FallbackAction {
|
||||
Get(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for FallbackAction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Get(path) => write!(f, "{}", path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ComponentId(String);
|
||||
|
||||
impl ComponentId {
|
||||
#[tracing::instrument]
|
||||
pub fn build(context: &Context, body: &Markup, active_page: Option<&TopLevelPage>) -> Markup {
|
||||
let menu_item = |item: TopLevelPage, active_page: Option<&TopLevelPage>| {
|
||||
let active = active_page.map_or(false, |page| *page == item);
|
||||
html!(
|
||||
a
|
||||
href=(item.path())
|
||||
#{"header-link-" (item.id())}
|
||||
."px-5"
|
||||
."flex"
|
||||
."h-full"
|
||||
."text-lg"
|
||||
."hover:bg-gray-300"
|
||||
|
||||
// invisible top border to fix alignment
|
||||
."border-t-gray-200"[active]
|
||||
."hover:border-t-gray-300"[active]
|
||||
|
||||
."border-b-gray-500"[active]
|
||||
."border-y-4"[active]
|
||||
."font-bold"[active]
|
||||
{ span ."m-auto" ."font-semibold" { (item.name()) }}
|
||||
)
|
||||
// fn new() -> Self {
|
||||
// NOTE: this could also use a static AtomicUsize incrementing integer, which might be faster
|
||||
// Self(random::<u32>())
|
||||
// }
|
||||
#[tracing::instrument]
|
||||
fn html_id(&self) -> String {
|
||||
let id = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.0.as_bytes());
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
html!(
|
||||
(DOCTYPE)
|
||||
html {
|
||||
head {
|
||||
title { "Packager" }
|
||||
script src="https://unpkg.com/htmx.org@1.9.4" {}
|
||||
script src="https://unpkg.com/alpinejs@3.12.3" defer {}
|
||||
script src="https://cdn.tailwindcss.com/3.3.3" {}
|
||||
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" {}
|
||||
link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {}
|
||||
script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) }
|
||||
meta name="htmx-config" content=r#"{"useTemplateFragments":true}"# {}
|
||||
}
|
||||
body
|
||||
{
|
||||
header
|
||||
#header
|
||||
."h-16"
|
||||
."bg-gray-200"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-nowrap"
|
||||
."justify-between"
|
||||
."items-stretch"
|
||||
{
|
||||
a
|
||||
#home
|
||||
href="/"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."gap-3"
|
||||
."px-5"
|
||||
."hover:bg-gray-300"
|
||||
{
|
||||
img ."h-12" src="/assets/luggage.svg" {}
|
||||
span
|
||||
."text-xl"
|
||||
."font-semibold"
|
||||
{ "Packager" }
|
||||
}
|
||||
nav
|
||||
."grow"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-center"
|
||||
."gap-x-10"
|
||||
."items-stretch"
|
||||
{
|
||||
(menu_item(TopLevelPage::Inventory, active_page))
|
||||
(menu_item(TopLevelPage::Trips, active_page))
|
||||
}
|
||||
a
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."gap-3"
|
||||
."px-5"
|
||||
."bg-gray-200"
|
||||
."hover:bg-gray-300"
|
||||
href=(format!("/user/{}", context.user.id))
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-account"
|
||||
."text-3xl"
|
||||
{}
|
||||
p { (context.user.fullname)}
|
||||
}
|
||||
}
|
||||
(body)
|
||||
}
|
||||
}
|
||||
)
|
||||
// 9 bytes is enough to be unique
|
||||
// If this is divisible by 3, it means that we can base64-encode it without
|
||||
// any "=" padding
|
||||
//
|
||||
// cannot panic, as the output for sha256 will always be bit
|
||||
let id = &id[..9];
|
||||
|
||||
// URL_SAFE because we cannot have slashes in the output
|
||||
let id = base64::engine::general_purpose::URL_SAFE.encode(id);
|
||||
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ErrorPage;
|
||||
|
||||
impl ErrorPage {
|
||||
#[tracing::instrument]
|
||||
pub fn build(message: &str) -> Markup {
|
||||
html!(
|
||||
(DOCTYPE)
|
||||
html {
|
||||
head {
|
||||
title { "Packager" }
|
||||
}
|
||||
body {
|
||||
h1 { "Error" }
|
||||
p { (message) }
|
||||
}
|
||||
}
|
||||
)
|
||||
impl fmt::Display for ComponentId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.html_id())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HtmxTarget {
|
||||
Myself,
|
||||
Component(ComponentId),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HtmxComponent {
|
||||
id: ComponentId,
|
||||
action: HtmxAction,
|
||||
fallback_action: FallbackAction,
|
||||
target: HtmxTarget,
|
||||
}
|
||||
|
||||
impl HtmxComponent {
|
||||
fn target(&self) -> &ComponentId {
|
||||
match self.target {
|
||||
HtmxTarget::Myself => &self.id,
|
||||
HtmxTarget::Component(ref id) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Parent {
|
||||
Root,
|
||||
Component(ComponentId),
|
||||
}
|
||||
|
||||
impl From<Parent> for ComponentId {
|
||||
fn from(value: Parent) -> Self {
|
||||
match value {
|
||||
Parent::Root => ComponentId("/".into()),
|
||||
Parent::Component(c) => c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComponentId> for Parent {
|
||||
fn from(value: ComponentId) -> Self {
|
||||
Self::Component(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Component {
|
||||
type Args;
|
||||
|
||||
fn init(parent: Parent, args: Self::Args) -> Self;
|
||||
fn build(self, context: &Context) -> Markup;
|
||||
}
|
||||
|
||||
216
rust/src/view/root.rs
Normal file
216
rust/src/view/root.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use crate::{Context, TopLevelPage};
|
||||
|
||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||
|
||||
use super::{
|
||||
Component, ComponentId, FallbackAction, HtmxAction, HtmxComponent, HtmxTarget, Parent,
|
||||
};
|
||||
|
||||
pub struct Header;
|
||||
|
||||
impl Header {
|
||||
#[tracing::instrument]
|
||||
pub fn build() -> Markup {
|
||||
html!(
|
||||
head {
|
||||
title { "Packager" }
|
||||
script src="https://unpkg.com/htmx.org@1.9.4" {}
|
||||
script src="https://unpkg.com/alpinejs@3.12.3" defer {}
|
||||
script src="https://cdn.tailwindcss.com/3.3.3" {}
|
||||
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" {}
|
||||
link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {}
|
||||
script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) }
|
||||
meta name="htmx-config" content=r#"{"useTemplateFragments":true}"# {}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HeaderLink<'a> {
|
||||
htmx: HtmxComponent,
|
||||
args: HeaderLinkArgs<'a>,
|
||||
}
|
||||
|
||||
pub struct HeaderLinkArgs<'a> {
|
||||
pub item: TopLevelPage,
|
||||
pub active_page: Option<&'a TopLevelPage>,
|
||||
}
|
||||
|
||||
impl<'a> Component for HeaderLink<'a> {
|
||||
type Args = HeaderLinkArgs<'a>;
|
||||
|
||||
#[tracing::instrument(skip(args))]
|
||||
fn init(parent: Parent, args: Self::Args) -> Self {
|
||||
Self {
|
||||
htmx: HtmxComponent {
|
||||
id: ComponentId(format!("/header/component/{}", args.item.id())),
|
||||
action: HtmxAction::Get(args.item.path().to_string()),
|
||||
fallback_action: FallbackAction::Get(args.item.path().to_string()),
|
||||
target: HtmxTarget::Component(parent.into()),
|
||||
},
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn build(self, context: &Context) -> Markup {
|
||||
let active = self
|
||||
.args
|
||||
.active_page
|
||||
.map_or(false, |page| *page == self.args.item);
|
||||
html!(
|
||||
a
|
||||
href=(self.args.item.path())
|
||||
hx-get=(self.args.item.path())
|
||||
hx-target={ "#" (self.htmx.target().html_id()) }
|
||||
hx-swap="outerHtml"
|
||||
#{"header-link-" (self.args.item.id())}
|
||||
."px-5"
|
||||
."flex"
|
||||
."h-full"
|
||||
."text-lg"
|
||||
."hover:bg-gray-300"
|
||||
|
||||
// invisible top border to fix alignment
|
||||
."border-t-gray-200"[active]
|
||||
."hover:border-t-gray-300"[active]
|
||||
|
||||
."border-b-gray-500"[active]
|
||||
."border-y-4"[active]
|
||||
."font-bold"[active]
|
||||
{ span ."m-auto" ."font-semibold" { (self.args.item.name()) }}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Body<'a> {
|
||||
htmx: HtmxComponent,
|
||||
args: BodyArgs<'a>,
|
||||
}
|
||||
|
||||
pub struct BodyArgs<'a> {
|
||||
pub body: &'a Markup,
|
||||
pub active_page: Option<&'a TopLevelPage>,
|
||||
}
|
||||
|
||||
impl<'a> Component for Body<'a> {
|
||||
type Args = BodyArgs<'a>;
|
||||
|
||||
#[tracing::instrument(skip(args))]
|
||||
fn init(parent: Parent, args: Self::Args) -> Self {
|
||||
Self {
|
||||
htmx: HtmxComponent {
|
||||
id: ComponentId("/body/".into()),
|
||||
action: HtmxAction::Get("/".into()),
|
||||
fallback_action: FallbackAction::Get("/".into()),
|
||||
target: HtmxTarget::Myself,
|
||||
},
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn build(self, context: &Context) -> Markup {
|
||||
html!(
|
||||
body #(self.htmx.id.html_id())
|
||||
{
|
||||
header
|
||||
#header
|
||||
."h-16"
|
||||
."bg-gray-200"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-nowrap"
|
||||
."justify-between"
|
||||
."items-stretch"
|
||||
{
|
||||
a
|
||||
#home
|
||||
href=(self.htmx.fallback_action)
|
||||
hx-get=(self.htmx.action)
|
||||
hx-target={ "#" (self.htmx.target()) }
|
||||
hx-swap="outerHTML"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."gap-3"
|
||||
."px-5"
|
||||
."hover:bg-gray-300"
|
||||
{
|
||||
img ."h-12" src="/assets/luggage.svg" {}
|
||||
span
|
||||
."text-xl"
|
||||
."font-semibold"
|
||||
{ "Packager" }
|
||||
}
|
||||
nav
|
||||
."grow"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-center"
|
||||
."gap-x-10"
|
||||
."items-stretch"
|
||||
{
|
||||
(
|
||||
// todo make clone() unnecessary
|
||||
// make ComponentId take &str instead of owned string
|
||||
HeaderLink::init(
|
||||
self.htmx.id.clone().into(),
|
||||
HeaderLinkArgs {
|
||||
item: TopLevelPage::Inventory,
|
||||
active_page: self.args.active_page
|
||||
}
|
||||
).build(&context)
|
||||
)
|
||||
(
|
||||
HeaderLink::init(
|
||||
self.htmx.id.clone().into(),
|
||||
HeaderLinkArgs {
|
||||
item: TopLevelPage::Trips,
|
||||
active_page: self.args.active_page
|
||||
}
|
||||
).build(&context)
|
||||
)
|
||||
}
|
||||
a
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."gap-3"
|
||||
."px-5"
|
||||
."bg-gray-200"
|
||||
."hover:bg-gray-300"
|
||||
href=(format!("/user/{}", context.user.id))
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-account"
|
||||
."text-3xl"
|
||||
{}
|
||||
p { (context.user.fullname)}
|
||||
}
|
||||
}
|
||||
(self.args.body)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Root;
|
||||
|
||||
impl Root {
|
||||
#[tracing::instrument]
|
||||
pub fn build(context: &Context, body: &Markup, active_page: Option<&TopLevelPage>) -> Markup {
|
||||
html!(
|
||||
(DOCTYPE)
|
||||
html {
|
||||
(Header::build())
|
||||
(Body::init(Parent::Root, BodyArgs {
|
||||
body,
|
||||
active_page
|
||||
}).build(&context))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user