refactor(techdemo): scope card handles to partials

Move board card and lane actions onto their reusable partial templates, register the scoped generated handles directly, and remove hidden shell-level handle anchors and manual handle fields from the rendered view data.

req: component/003

req: view/001

req: examples/001
This commit is contained in:
slhx agent
2026-06-01 21:58:40 +02:00
parent c69eb9932c
commit 4319dea171
7 changed files with 22 additions and 33 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ mod tests {
fn techdemo_exports_form_and_interaction_handles() { fn techdemo_exports_form_and_interaction_handles() {
assert_ne!( assert_ne!(
ui::control_center::handles::launch_work.id(), ui::control_center::handles::launch_work.id(),
ui::control_center::handles::advance_work.id() ui::issue_card::handles::advance_work.id()
); );
assert_eq!( assert_eq!(
ui::control_center::forms::launch_work.field("title").resource, ui::control_center::forms::launch_work.field("title").resource,
+8 -15
View File
@@ -4,7 +4,7 @@ use axum::routing::get;
use axum::Router; use axum::Router;
use futures_util::{stream, StreamExt}; use futures_util::{stream, StreamExt};
use hemplate::Hemplate; use hemplate::Hemplate;
use slhx::{CssClass, CssClasses, Handle, IntoEffect, RenderSlotExt, SafeHtml}; use slhx::{CssClass, CssClasses, IntoEffect, RenderSlotExt, SafeHtml};
use slhx_axum::{ use slhx_axum::{
runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm,
PageRequest, PageRequest,
@@ -102,7 +102,6 @@ struct Shared {
struct IssueLane { struct IssueLane {
class: CssClass, class: CssClass,
lane_id: &'static str, lane_id: &'static str,
move_handle: Handle<()>,
title: &'static str, title: &'static str,
description: &'static str, description: &'static str,
cards: Vec<IssueCard>, cards: Vec<IssueCard>,
@@ -116,9 +115,6 @@ struct IssueCard {
title: String, title: String,
stage: &'static str, stage: &'static str,
impact_style: String, impact_style: String,
spotlight_handle: Handle<()>,
advance_handle: Handle<()>,
delete_handle: Handle<()>,
} }
#[derive(Hemplate)] #[derive(Hemplate)]
@@ -304,7 +300,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Launch accepted · 4 targets updated") demo_effects(&demo, "Launch accepted · 4 targets updated")
} }
}) })
.register_handle(ui::control_center::handles::advance_work, { .register_handle(ui::issue_card::handles::advance_work, {
let shared = shared.clone(); let shared = shared.clone();
move |form| { move |form| {
// req: list/003 req: examples/001 // req: list/003 req: examples/001
@@ -322,7 +318,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Pipeline advanced") demo_effects(&demo, "Pipeline advanced")
} }
}) })
.register_handle(ui::control_center::handles::move_to_lane, { .register_handle(ui::issue_lane::handles::move_to_lane, {
let shared = shared.clone(); let shared = shared.clone();
move |form| { move |form| {
// req: list/003 req: examples/001 // req: list/003 req: examples/001
@@ -344,7 +340,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Drag-and-drop move persisted") demo_effects(&demo, "Drag-and-drop move persisted")
} }
}) })
.register_handle(ui::control_center::handles::delete_work, { .register_handle(ui::issue_card::handles::delete_work, {
let shared = shared.clone(); let shared = shared.clone();
move |form| { move |form| {
// req: list/003 req: examples/001 // req: list/003 req: examples/001
@@ -363,7 +359,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
demo_effects(&demo, "Card removed") demo_effects(&demo, "Card removed")
} }
}) })
.register_handle(ui::control_center::handles::spotlight_work, { .register_handle(ui::issue_card::handles::spotlight_work, {
let shared = shared.clone(); let shared = shared.clone();
move |form| { move |form| {
// req: examples/001 // req: examples/001
@@ -416,7 +412,7 @@ fn update_work(demo: &mut DemoState, id: Option<&str>, update: impl FnOnce(&mut
fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect { fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect {
( (
ui::control_center::slots::hero_metrics.render(&hero_view(demo)), ui::control_center::slots::hero_metrics.render(&hero_view(demo)),
ui::control_center::slots::board.render(&board_view(demo)), ui::control_center::slots::board.html(render_board(demo)),
ui::control_center::slots::activity.render(&activity_view(demo)), ui::control_center::slots::activity.render(&activity_view(demo)),
ui::control_center::slots::inspector.render(&inspector_view(demo)), ui::control_center::slots::inspector.render(&inspector_view(demo)),
ui::control_center::slots::notice.text(notice), ui::control_center::slots::notice.text(notice),
@@ -491,7 +487,6 @@ fn board_view(demo: &DemoState) -> BoardLanes {
.map(|(idx, (lane_id, title, description))| IssueLane { .map(|(idx, (lane_id, title, description))| IssueLane {
class: ui::control_center::classes::lane, class: ui::control_center::classes::lane,
lane_id, lane_id,
move_handle: ui::control_center::handles::move_to_lane,
title, title,
description, description,
cards: demo cards: demo
@@ -508,7 +503,8 @@ fn board_view(demo: &DemoState) -> BoardLanes {
fn render_board(demo: &DemoState) -> SafeHtml { fn render_board(demo: &DemoState) -> SafeHtml {
// req: html_safety/002 req: view/001 // req: html_safety/002 req: view/001
slhx::render_html(&board_view(demo)) let html = render_template(&board_view(demo));
SafeHtml::trusted(ui::issue_lane::lower_html(ui::issue_card::lower_html(html)))
} }
fn issue_card(item: &WorkItem, selected: bool) -> IssueCard { fn issue_card(item: &WorkItem, selected: bool) -> IssueCard {
@@ -525,9 +521,6 @@ fn issue_card(item: &WorkItem, selected: bool) -> IssueCard {
title: item.title.clone(), title: item.title.clone(),
stage: item.stage.label(), stage: item.stage.label(),
impact_style: format!("width:{}%", item.impact as usize * 11), impact_style: format!("width:{}%", item.impact as usize * 11),
spotlight_handle: ui::control_center::handles::spotlight_work,
advance_handle: ui::control_center::handles::advance_work,
delete_handle: ui::control_center::handles::delete_work,
} }
} }
@@ -65,8 +65,4 @@
</article> </article>
</section> </section>
<button type="button" data-slhx-handle="advance_work" hidden="hidden">Advance</button>
<button type="button" data-slhx-handle="delete_work" hidden="hidden">Delete</button>
<button type="button" data-slhx-handle="spotlight_work" hidden="hidden">Spotlight</button>
<button type="button" data-slhx-handle="move_to_lane" hidden="hidden">Move to lane</button>
</main> </main>
@@ -2,8 +2,8 @@
<header><strong>{+ self.title +}</strong><span class="pill">{+ self.stage +}</span></header> <header><strong>{+ self.title +}</strong><span class="pill">{+ self.stage +}</span></header>
<div class="impact"><i +style="self.impact_style"></i></div> <div class="impact"><i +style="self.impact_style"></i></div>
<div class="card-actions"> <div class="card-actions">
<button type="button" +data-hid="self.spotlight_handle" +data-work-id="self.id">Inspect</button> <button type="button" data-slhx-handle="spotlight_work" +data-work-id="self.id">Inspect</button>
<button type="button" +data-hid="self.advance_handle" +data-work-id="self.id">Advance</button> <button type="button" data-slhx-handle="advance_work" +data-work-id="self.id">Advance</button>
<button type="button" +data-hid="self.delete_handle" +data-work-id="self.id">Delete</button> <button type="button" data-slhx-handle="delete_work" +data-work-id="self.id">Delete</button>
</div> </div>
</article> </article>
@@ -1,4 +1,4 @@
<section +class="self.class" +data-lane="self.lane_id" +data-hid="self.move_handle" data-slhx-on="drop"> <section +class="self.class" +data-lane="self.lane_id" data-slhx-handle="move_to_lane" data-slhx-on="drop">
<h3>{+ self.title +}</h3> <h3>{+ self.title +}</h3>
<p>{+ self.description +}</p> <p>{+ self.description +}</p>
<template h-for="card in &self.cards"> <template h-for="card in &self.cards">
+2 -2
View File
@@ -72,7 +72,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice), "Drag-and-drop move persisted").await?; wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice), "Drag-and-drop move persisted").await?;
driver driver
.find(By::Css(&card_button_selector(ui::control_center::handles::spotlight_work, 2))) .find(By::Css(&card_button_selector(ui::issue_card::handles::spotlight_work, 2)))
.await? .await?
.click() .click()
.await?; .await?;
@@ -84,7 +84,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
.await?; .await?;
driver driver
.find(By::Css(&card_button_selector(ui::control_center::handles::advance_work, 3))) .find(By::Css(&card_button_selector(ui::issue_card::handles::advance_work, 3)))
.await? .await?
.click() .click()
.await?; .await?;
+7 -7
View File
@@ -116,32 +116,32 @@ fn product_is_e2e_working_over_http() {
let move_to_lane = post( let move_to_lane = post(
"/", "/",
&format!("__h={}&work_id=4&lane=runtime", ui::control_center::handles::move_to_lane), &format!("__h={}&work_id=4&lane=runtime", ui::issue_lane::handles::move_to_lane),
); );
assert_effect_response(&move_to_lane); assert_effect_response(&move_to_lane);
let move_to_lane_batch = move_to_lane.batch(); let move_to_lane_batch = move_to_lane.batch();
assert_card(&move_to_lane_batch, "Design hero moment", "Runtime", "Active", "width:99%"); assert_card(&move_to_lane_batch, "Design hero moment", "Runtime", "Active", "width:99%");
assert_payload_contains(&move_to_lane_batch, "Drag-and-drop move persisted"); assert_payload_contains(&move_to_lane_batch, "Drag-and-drop move persisted");
let inspect = post("/", "__h=3112123592&work_id=4"); let inspect = post("/", &format!("__h={}&work_id=4", ui::issue_card::handles::spotlight_work));
assert_effect_response(&inspect); assert_effect_response(&inspect);
let inspect_batch = inspect.batch(); let inspect_batch = inspect.batch();
assert_payload_contains(&inspect_batch, "Design hero moment · lane=Runtime"); assert_payload_contains(&inspect_batch, "Design hero moment · lane=Runtime");
assert_payload_contains(&inspect_batch, "Inspector focused"); assert_payload_contains(&inspect_batch, "Inspector focused");
let advance = post("/", "__h=4085043678&work_id=4"); let advance = post("/", &format!("__h={}&work_id=4", ui::issue_card::handles::advance_work));
assert_effect_response(&advance); assert_effect_response(&advance);
let advance_batch = advance.batch(); let advance_batch = advance.batch();
assert_payload_contains(&advance_batch, "Pipeline advanced"); assert_payload_contains(&advance_batch, "Pipeline advanced");
assert_payload_contains(&advance_batch, "<span class=\"pill\">Active</span>"); assert_payload_contains(&advance_batch, "<span class=\"pill\">Active</span>");
let advance_default = post("/", "__h=4085043678&work_id=5"); let advance_default = post("/", &format!("__h={}&work_id=5", ui::issue_card::handles::advance_work));
assert_effect_response(&advance_default); assert_effect_response(&advance_default);
let advance_default_batch = advance_default.batch(); let advance_default_batch = advance_default.batch();
assert_payload_contains(&advance_default_batch, "Default impact"); assert_payload_contains(&advance_default_batch, "Default impact");
assert_card(&advance_default_batch, "Default impact", "Compiler", "Active", "width:55%"); assert_card(&advance_default_batch, "Default impact", "Compiler", "Active", "width:55%");
let ship_default = post("/", "__h=4085043678&work_id=5"); let ship_default = post("/", &format!("__h={}&work_id=5", ui::issue_card::handles::advance_work));
assert_effect_response(&ship_default); assert_effect_response(&ship_default);
let ship_default_batch = ship_default.batch(); let ship_default_batch = ship_default.batch();
assert_card(&ship_default_batch, "Default impact", "Product", "Shipped", "width:55%"); assert_card(&ship_default_batch, "Default impact", "Product", "Shipped", "width:55%");
@@ -154,12 +154,12 @@ fn product_is_e2e_working_over_http() {
assert_payload_contains(&push_batch, "Simulated push event produced the same EffectBatch shape"); assert_payload_contains(&push_batch, "Simulated push event produced the same EffectBatch shape");
assert_emit(&push_batch, "slhx:island-orbit", "activity rows"); assert_emit(&push_batch, "slhx:island-orbit", "activity rows");
let delete_missing = post("/", "__h=165211725&work_id=999"); let delete_missing = post("/", &format!("__h={}&work_id=999", ui::issue_card::handles::delete_work));
assert_effect_response(&delete_missing); assert_effect_response(&delete_missing);
let delete_missing_batch = delete_missing.batch(); let delete_missing_batch = delete_missing.batch();
assert_payload_not_contains(&delete_missing_batch, "Deleted card #999"); assert_payload_not_contains(&delete_missing_batch, "Deleted card #999");
let delete = post("/", "__h=165211725&work_id=4"); let delete = post("/", &format!("__h={}&work_id=4", ui::issue_card::handles::delete_work));
assert_effect_response(&delete); assert_effect_response(&delete);
let delete_batch = delete.batch(); let delete_batch = delete.batch();
assert_payload_contains(&delete_batch, "Card removed"); assert_payload_contains(&delete_batch, "Card removed");