Add delete, error handling

- Wire up deleting a series.
- Add an error boundary for better error handling from the server
  function. Suspense and bounday expect the child to be an option or a
  result respectively not a view itself. Took a while to figure that
  out.
This commit is contained in:
Thomas Gideon 2024-09-07 16:44:00 -04:00
parent 3d3b08caf1
commit a2c8a15d84
7 changed files with 113 additions and 23 deletions

View file

@ -0,0 +1,3 @@
alter table "public"."series" add column source text not null;
alter table "public"."series" add column imdb text null;
alter table "public"."series" add column wikipedia text null

1
sql/delete_series.sql Normal file
View file

@ -0,0 +1 @@
delete from series where id = $1;

View file

@ -1,15 +1,16 @@
use super::series::{add::AddSeries, SeriesList}; use super::series::{add::AddSeries, DeleteSeries, SeriesList};
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
#[component] #[component]
pub fn HomePage() -> impl IntoView { pub fn HomePage() -> impl IntoView {
let add_series = create_server_action::<AddSeries>(); let add_series = create_server_action::<AddSeries>();
let delete_series = create_server_action::<DeleteSeries>();
provide_context(add_series); provide_context(add_series);
provide_context(delete_series);
view! { view! {
<h1 class="mt-3 mb-5">"What We're Watching"</h1>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<SeriesList /> <SeriesList />

View file

@ -39,6 +39,7 @@ pub fn App() -> impl IntoView {
} }
.into_view() .into_view()
}> }>
<h1 class="mb-5 text-bg-primary p-5">"What We're Watching"</h1>
<main class="container"> <main class="container">
<Routes> <Routes>
<Route path="" view=HomePage> <Route path="" view=HomePage>

View file

@ -7,20 +7,48 @@ pub fn AddSeries() -> impl IntoView {
view! { view! {
<div class="card p-3"> <div class="card p-3">
<div class="card-heading"> <div class="card-heading">
<h2>Add Series</h2> <A href="/"
attr:class="btn btn-close"
attr:style="float: right;"
>
<span style="display: none;">x</span>
</A>
<h2>New Series</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
<ActionForm action=add_series> <ActionForm action=add_series>
<div class="input-group mb-3"> <div class="input-group mb-3">
<label class="input-group-text" for="name">Name</label>
<input type="text" <input type="text"
name="name" name="name"
class="form-control" class="form-control"
/> />
</div> </div>
<div class="input-group mb-3">
<label class="input-group-text" for="source">Source Link</label>
<input type="text"
name="source"
class="form-control"
/>
</div>
<div class="input-group mb-3">
<label class="input-group-text" for="imdb">IMDB Link</label>
<input type="text"
name="imdb"
class="form-control"
/>
</div>
<div class="input-group mb-3">
<label class="input-group-text" for="wikipedia">Wikipedia Link</label>
<input type="text"
name="wikipedia"
class="form-control"
/>
</div>
<div> <div>
<input type="submit" <input type="submit"
class="btn btn-primary" class="btn btn-primary"
value="Add Series" value="Add"
/> />
</div> </div>
</ActionForm> </ActionForm>

View file

@ -11,16 +11,15 @@ pub mod view;
#[component] #[component]
pub fn SeriesList() -> impl IntoView { pub fn SeriesList() -> impl IntoView {
let add_user = expect_context::<Action<AddSeries, Result<(), ServerFnError>>>(); let add_series = expect_context::<Action<AddSeries, Result<(), ServerFnError>>>();
let load = create_resource( let delete_series = expect_context::<Action<DeleteSeries, Result<(), ServerFnError>>>();
move || add_user.version().get(), let all_series = create_resource(
move || (add_series.version().get(), delete_series.version().get()),
|_| async move { get_all().await }, |_| async move { get_all().await },
); );
view! { view! {
<Suspense fallback=|| view!{ <div>"Loading..."</div> }> <Suspense fallback=|| view!{ <div>"Loading..."</div> }>
<ul class="list-group mb-3"> { move || all_series.get().map(format_all_series)}
{load().map(format_series)}
</ul>
</Suspense> </Suspense>
<div> <div>
<A href="add" <A href="add"
@ -32,23 +31,58 @@ pub fn SeriesList() -> impl IntoView {
} }
} }
fn format_series(data: Result<Vec<Series>, ServerFnError>) -> impl IntoView { fn format_all_series(
data.unwrap_or_default() all_series: Result<Vec<Series>, ServerFnError>,
) -> Result<impl IntoView, ServerFnError> {
Ok(view! {
<ErrorBoundary fallback=|errors| {
view! {
<div>
{errors.get()
.into_iter() .into_iter()
.map(|d| { .map(|(_, e)| e.to_string())
.collect::<Vec<_>>()
.join(", ")
}
</div>
}
}
>
<ul class="list-group mb-3">
{
all_series.map(|all_series| {
Some(all_series.into_iter().map(format_series).collect_view())
})
}
</ul>
</ErrorBoundary>
})
}
fn format_series(series: Series) -> impl IntoView {
let delete_series = expect_context::<Action<DeleteSeries, Result<(), ServerFnError>>>();
view! { view! {
<li class="list-group-item"> <li class="list-group-item">
<A href=format!("view/{}", d.id) <ActionForm action=delete_series>
<div class="btn-group" style="float: right;">
<A href=format!("view/{}", series.id)
attr:class="btn btn-sm btn-secondary" attr:class="btn btn-sm btn-secondary"
attr:style="float: right" attr:style="float: right"
> >
View Series View
</A> </A>
{d.name} <input type="submit"
class="btn btn-sm btn-secondary"
value="Delete"
/>
</div>
<input type="hidden"
name="id"
value=series.id.to_string() />
</ActionForm>
{series.name}
</li> </li>
} }
})
.collect_view()
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -76,3 +110,18 @@ pub async fn get_all() -> Result<Vec<Series>, ServerFnError> {
Ok(found) Ok(found)
} }
#[server(DeleteSeries)]
pub async fn delete(id: String) -> Result<(), ServerFnError> {
use crate::db::load_statement;
use sqlx::{Pool, Postgres};
let pool: Pool<Postgres> = expect_context();
let s = load_statement("delete_series.sql");
sqlx::query(&s)
.bind(id.parse::<Uuid>()?)
.execute(&pool)
.await?;
Ok(())
}

View file

@ -1,10 +1,17 @@
use leptos::*; use leptos::*;
use leptos_router::*;
#[component] #[component]
pub fn ViewSeries() -> impl IntoView { pub fn ViewSeries() -> impl IntoView {
view! { view! {
<div class="card p-3"> <div class="card p-3">
<div class="card-heading"> <div class="card-heading">
<A href="/"
attr:class="btn btn-close"
attr:style="float: right;"
>
<span style="display: none;">x</span>
</A>
<h2>View Series</h2> <h2>View Series</h2>
</div> </div>
<div class="card-body"> <div class="card-body">