refactor(kanban): render board cards with hemplate
Move Kanban board/column/card rendering from Rust raw HTML assembly into hemplate partial views. Keep handler effects returning generated slot payloads and cover rendered structure with scraper. req: html_safety/002 req: view/001 req: test/005
This commit is contained in:
+78
-40
@@ -33,6 +33,31 @@ struct Card {
|
||||
column: usize,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct BoardColumns {
|
||||
columns: Vec<BoardColumn>,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct BoardColumn {
|
||||
title: &'static str,
|
||||
cards: Vec<BoardCard>,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct BoardCard {
|
||||
id: u64,
|
||||
title: String,
|
||||
left_handle: u32,
|
||||
right_handle: u32,
|
||||
delete_handle: u32,
|
||||
left_disabled: bool,
|
||||
right_disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
struct Board {
|
||||
options: SafeHtml,
|
||||
@@ -174,7 +199,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
||||
|
||||
fn board_effects(board: &BoardState, notice: &'static str) -> impl IntoEffect {
|
||||
(
|
||||
ui::board::slots::board.html(SafeHtml::trusted(render_board(board))),
|
||||
ui::board::slots::board.html(render_board(board)),
|
||||
ui::board::slots::notice.text(notice),
|
||||
ui::board::forms::create_card.clear("title"),
|
||||
)
|
||||
@@ -203,7 +228,7 @@ fn page_html(board: &BoardState) -> String {
|
||||
// req: html_safety/002 req: view/001
|
||||
ui::board::lower_html(render_template(&Board {
|
||||
options: render_options(),
|
||||
board: SafeHtml::trusted(render_board(board)),
|
||||
board: render_board(board),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -244,37 +269,35 @@ fn render_options() -> SafeHtml {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_board(board: &BoardState) -> String {
|
||||
let mut out = String::from("<div class=\"columns\">");
|
||||
for (idx, (_, title)) in COLUMNS.iter().enumerate() {
|
||||
out.push_str(&format!(r#"<section class="column"><h2>{}</h2>"#, escape_html(title)));
|
||||
for card in board.cards.iter().filter(|card| card.column == idx) {
|
||||
out.push_str(&render_card(card));
|
||||
}
|
||||
out.push_str("</section>");
|
||||
}
|
||||
out.push_str("</div>");
|
||||
out
|
||||
fn render_board(board: &BoardState) -> SafeHtml {
|
||||
// req: html_safety/002 req: view/001
|
||||
render_html(&BoardColumns {
|
||||
columns: COLUMNS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, (_, title))| BoardColumn {
|
||||
title,
|
||||
cards: board
|
||||
.cards
|
||||
.iter()
|
||||
.filter(|card| card.column == idx)
|
||||
.map(render_card)
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_card(card: &Card) -> String {
|
||||
format!(
|
||||
r#"<article class="card" data-key="{id}">
|
||||
<strong>{title}</strong>
|
||||
<menu>
|
||||
<button type="button" data-hid="{left}" data-card-id="{id}" {left_disabled}>←</button>
|
||||
<button type="button" data-hid="{right}" data-card-id="{id}" {right_disabled}>→</button>
|
||||
<button type="button" data-hid="{delete}" data-card-id="{id}">Delete</button>
|
||||
</menu>
|
||||
</article>"#,
|
||||
id = card.id,
|
||||
title = escape_html(&card.title),
|
||||
left = ui::board::handles::move_left.id().id,
|
||||
right = ui::board::handles::move_right.id().id,
|
||||
delete = ui::board::handles::delete_card.id().id,
|
||||
left_disabled = if card.column == 0 { "disabled" } else { "" },
|
||||
right_disabled = if card.column + 1 == COLUMNS.len() { "disabled" } else { "" },
|
||||
)
|
||||
fn render_card(card: &Card) -> BoardCard {
|
||||
BoardCard {
|
||||
id: card.id,
|
||||
title: card.title.clone(),
|
||||
left_handle: ui::board::handles::move_left.id().id,
|
||||
right_handle: ui::board::handles::move_right.id().id,
|
||||
delete_handle: ui::board::handles::delete_card.id().id,
|
||||
left_disabled: card.column == 0,
|
||||
right_disabled: card.column + 1 == COLUMNS.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_presence(count: u64) -> SafeHtml {
|
||||
@@ -290,15 +313,6 @@ fn render_template(template: &impl Hemplate) -> String {
|
||||
template.render().expect("kanban hemplate view renders")
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -322,6 +336,30 @@ mod tests {
|
||||
assert_eq!(document.select(&selector("[data-hid]")).count(), 4);
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
#[test]
|
||||
fn board_payload_is_rendered_by_a_hemplate_view() {
|
||||
let board = BoardState {
|
||||
next_id: 2,
|
||||
cards: vec![Card {
|
||||
id: 1,
|
||||
title: "<b>Compile checked</b>".to_owned(),
|
||||
column: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
let html = render_board(&board);
|
||||
let document = Html::parse_fragment(html.as_str());
|
||||
assert_eq!(document.select(&selector(".columns > section.column")).count(), 3);
|
||||
let card = document
|
||||
.select(&selector("article.card[data-key=\"1\"]"))
|
||||
.next()
|
||||
.expect("card renders");
|
||||
assert!(card.text().collect::<String>().contains("<b>Compile checked</b>"));
|
||||
assert!(card.select(&selector("b")).next().is_none());
|
||||
assert_eq!(card.select(&selector("button[disabled]")).count(), 1);
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
#[test]
|
||||
fn presence_payload_is_rendered_by_a_hemplate_view() {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<article class="card" +data-key="self.id">
|
||||
<strong>{+ self.title +}</strong>
|
||||
<menu>
|
||||
<button h-if="self.left_disabled" type="button" +data-hid="self.left_handle" +data-card-id="self.id" disabled="disabled">←</button>
|
||||
<button h-else type="button" +data-hid="self.left_handle" +data-card-id="self.id">←</button>
|
||||
<button h-if="self.right_disabled" type="button" +data-hid="self.right_handle" +data-card-id="self.id" disabled="disabled">→</button>
|
||||
<button h-else type="button" +data-hid="self.right_handle" +data-card-id="self.id">→</button>
|
||||
<button type="button" +data-hid="self.delete_handle" +data-card-id="self.id">Delete</button>
|
||||
</menu>
|
||||
</article>
|
||||
@@ -0,0 +1,6 @@
|
||||
<section class="column">
|
||||
<h2>{+ self.title +}</h2>
|
||||
<template h-for="card in &self.cards">
|
||||
{+ card +}
|
||||
</template>
|
||||
</section>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="columns">
|
||||
<template h-for="column in &self.columns">
|
||||
{+ column +}
|
||||
</template>
|
||||
</div>
|
||||
Reference in New Issue
Block a user