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_router::*;
#[component]
pub fn HomePage() -> impl IntoView {
let add_series = create_server_action::<AddSeries>();
let delete_series = create_server_action::<DeleteSeries>();
provide_context(add_series);
provide_context(delete_series);
view! {
<h1 class="mt-3 mb-5">"What We're Watching"</h1>
<div class="row">
<div class="col">
<SeriesList />

View file

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

View file

@ -7,20 +7,48 @@ pub fn AddSeries() -> impl IntoView {
view! {
<div class="card p-3">
<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 class="card-body">
<ActionForm action=add_series>
<div class="input-group mb-3">
<label class="input-group-text" for="name">Name</label>
<input type="text"
name="name"
class="form-control"
/>
</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>
<input type="submit"
class="btn btn-primary"
value="Add Series"
value="Add"
/>
</div>
</ActionForm>

View file

@ -11,16 +11,15 @@ pub mod view;
#[component]
pub fn SeriesList() -> impl IntoView {
let add_user = expect_context::<Action<AddSeries, Result<(), ServerFnError>>>();
let load = create_resource(
move || add_user.version().get(),
let add_series = expect_context::<Action<AddSeries, Result<(), ServerFnError>>>();
let delete_series = expect_context::<Action<DeleteSeries, Result<(), ServerFnError>>>();
let all_series = create_resource(
move || (add_series.version().get(), delete_series.version().get()),
|_| async move { get_all().await },
);
view! {
<Suspense fallback=|| view!{ <div>"Loading..."</div> }>
<ul class="list-group mb-3">
{load().map(format_series)}
</ul>
{ move || all_series.get().map(format_all_series)}
</Suspense>
<div>
<A href="add"
@ -32,23 +31,58 @@ pub fn SeriesList() -> impl IntoView {
}
}
fn format_series(data: Result<Vec<Series>, ServerFnError>) -> impl IntoView {
data.unwrap_or_default()
.into_iter()
.map(|d| {
view! {
<li class="list-group-item">
<A href=format!("view/{}", d.id)
fn format_all_series(
all_series: Result<Vec<Series>, ServerFnError>,
) -> Result<impl IntoView, ServerFnError> {
Ok(view! {
<ErrorBoundary fallback=|errors| {
view! {
<div>
{errors.get()
.into_iter()
.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! {
<li class="list-group-item">
<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:style="float: right"
>
View Series
View
</A>
{d.name}
</li>
}
})
.collect_view()
<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>
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -76,3 +110,18 @@ pub async fn get_all() -> Result<Vec<Series>, ServerFnError> {
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_router::*;
#[component]
pub fn ViewSeries() -> impl IntoView {
view! {
<div class="card p-3">
<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>
</div>
<div class="card-body">