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:
parent
3d3b08caf1
commit
a2c8a15d84
7 changed files with 113 additions and 23 deletions
3
migrations/20240907190149_add-links.sql
Normal file
3
migrations/20240907190149_add-links.sql
Normal 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
1
sql/delete_series.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
delete from series where id = $1;
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue