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_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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue