Upgraded Leptos to 0.8. Fixes #1

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 07:51:29 +02:00
co-authored by Claude Opus 5.5
parent 01d48ac0e4
commit ec73351405
55 changed files with 1371 additions and 1097 deletions
+2 -1
View File
@@ -11,7 +11,8 @@ paths:
## Server functions
- One file per domain in `src/backend/`. Server-only helpers and their imports go in the `cfg_if! { if #[cfg(feature = "ssr")] { ... }}` block at the top; `#[server]` functions follow below it. Extra server-only imports inside a server fn body are written as local `use` statements.
- Use plain `#[server]` for new functions (older ones spell out `#[server(Name, "/api", "Url", "name")]`; both end up under `/api`).
- Use plain `#[server]` for new functions (a few older ones use `#[server(name = Name, prefix = "/api", endpoint = "name")]`; both end up under `/api`). The positional form with `"Url"` no longer compiles. Server functions are registered by `leptos_routes` in `src/main.rs`; there is no separate `/api` route.
- The current request is `use_context::<leptos_actix::Request>()` (it derefs to `HttpRequest`); extractors go through `leptos_actix::extract().await?`.
- Return `Result<ApiResponse<T>, ServerFnError>`. Expected/business failures are `Ok(ApiResponse::Error("English message".to_string()))` — the client passes that string through `trl()`, so add it to the catalogue. `Err(ServerFnError)` is for real failures (DB down etc.).
- A function used with `<DataForm>` / `<QuestionDialog>` must return `ApiResponse<()>` and take the form entity as a single struct argument; its name is the prefix of the form field names (`company: Company` ↔ `name="company[name]"`).
- For `<DataForm>`, implement `ForValidation` for the generated struct (PascalCase of the fn name) right below the function:
+13 -3
View File
@@ -12,15 +12,25 @@ paths:
## Components
- Leptos 0.6 API only. Components are `#[component]` functions returning `impl IntoView`, one page or dialog per file in `src/pages/`; reusable building blocks go to `src/components/`. Register new files in the respective `mod.rs` (`pub mod` only if used from `app.rs`).
- Leptos 0.8 API only (`use leptos::prelude::*`). Components are `#[component]` functions returning `impl IntoView`, one page or dialog per file in `src/pages/`; reusable building blocks go to `src/components/`. Register new files in the respective `mod.rs` (`pub mod` only if used from `app.rs`).
- This code runs on the server (SSR) and in the browser (hydration). Browser-only APIs (`window()`, `web_sys`) must not run during SSR — keep them in event handlers/effects or behind `#[cfg(not(feature = "ssr"))]`. Markup must render the same on both sides or hydration breaks.
- A new admin page needs a `<Route path="admin/..."/>` in `src/app.rs` wrapped in `<AdminPortal>`, and a menu entry in `src/components/admin_portal.rs`. Paths starting with `/admin` are login-protected by the middleware; data access is still guarded by `perm_check!` in the server fn.
## Leptos 0.8 pitfalls
- Views are statically typed: `if`/`else` and `match` arms that return different markup must each end with `.into_any()`.
- Context is not available in event handlers (`on:click` etc.). Call `use_context` in the component body and keep the result — `DialogOpener` and `MenuOpener` do this in `new()`, so create them in a component body, not inside a handler.
- Markup must be valid HTML, or hydration panics in the browser and the whole page stops reacting: no block elements (`<div>`, `<table>`, another `<p>`) inside `<p>`, and `<tr>` only inside `<tbody>`/`<thead>`. The compiler doesn't catch this — load the page with a full reload and check the browser console.
- HTML attributes are typed. Deprecated ones (`align`, `width` on `<table>`) don't compile; use `style`.
- A value used twice in one `view!` (e.g. in an attribute and as a child) must be cloned for the first use.
- Client-side form validation hooks into `<ActionForm>` with `on:submit:capture` and `ev.prevent_default()`; a plain `on:submit` runs too late. `DataForm` already does this.
- `action.value()` is not an `RwSignal`; pass it on as `Signal::derive(move || action.value().get())`.
## Data loading and forms
- Load data with `create_blocking_resource` / `create_resource` calling the server fn, rendered inside `<Transition fallback=...>` with `{trl("Loading...")}`. Use a dialog's `opener.visible()` as the resource source so lists reload after the dialog closes (see `src/pages/users.rs`).
- Load data with `Resource::new_blocking` / `Resource::new` calling the server fn, rendered inside `<Transition fallback=...>` with `{trl("Loading...")}`. Read the resource in a closure — `{move || res.get().map(...)}` — a plain `{res.get()...}` block is evaluated once and never updates. Use a dialog's `opener.visible()` as the resource source so lists reload after the dialog closes (see `src/pages/users.rs`).
- Handle all three outcomes of a server call: `Err(e)`, `Ok(ApiResponse::Error(msg))`, `Ok(ApiResponse::Data(d))`.
- Edit/create dialogs: `DialogOpener::new()` in the parent, `create_server_action::<ServerFnStruct>()` in the dialog, wrapped in `<DataForm opener=.. action=.. title="..">`. It supplies the modal, Save/Close buttons, client validation and server error display. Confirmations use `<QuestionDialog>`.
- Edit/create dialogs: `DialogOpener::new()` in the parent, `ServerAction::<ServerFnStruct>::new()` in the dialog, wrapped in `<DataForm opener=.. action=.. title="..">`. It supplies the modal, Save/Close buttons, client validation and server error display. Confirmations use `<QuestionDialog>`.
- Form field names follow the server fn argument: `name="company[name]"`. Pass the id as a hidden input. Fill inputs with `prop:value={move || signal.get().field}`.
- `DataForm`/`ModalDialog` take `title` as an untranslated English key (`&'static str`); they translate it themselves.