code: basic create view and HX-Header integration (first htmx)

This commit is contained in:
Gabor Körber 2024-01-28 00:53:49 +01:00
parent c439220409
commit 9feb2f4ae7
10 changed files with 103 additions and 19 deletions

View File

@ -10,3 +10,23 @@
- [ ] better 500 handling
edit functionality:
- edit-list (which fields can we edit)
-> unlike in django, we have no access to the model completely, however, we also do not have to care.
- widgets (how fields are rendered in the edit form)
-> how would i create widgets? programmatically? in templates?
-> maybe even both approaches available?
-> need a list of MVP widgets, and a way to extend them.
- validation.
-> client side as well?
-> existing solutions?
-> transparent way to display error messages at the appropriate field (probably adding results into responses)
-> required
-> invalid
-> invalid(max-length)
auth:
- user model.
- auth middleware.
- message the user.
-> messaging system

View File

@ -200,8 +200,8 @@ pub mod repository {
fn info(&self, model: &AdminModel) -> RepositoryInfo;
fn list(&self, model: &AdminModel) -> RepositoryList;
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>;
fn create(&self, model: &AdminModel, data: Value) -> Option<Value>;
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<Value>;
fn create(&self, model: &AdminModel, data: Value) -> Option<RepositoryItem>;
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem>;
fn delete(&self, model: &AdminModel, id: LookupKey) -> Option<Value>;
}
}

View File

@ -75,12 +75,12 @@ impl AdminRepository for MyStaticRepository {
.into()
}
fn create(&self, model: &AdminModel, data: Value) -> Option<Value> {
fn create(&self, model: &AdminModel, data: Value) -> Option<RepositoryItem> {
println!("I would now create: {}", data);
None
}
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<Value> {
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem> {
println!("I would now update: {}, {}", id, data);
None
}

View File

@ -1,4 +1,4 @@
use axum::{routing::get, Router};
use axum::{routing::get, routing::post, Router};
use crate::state::AppState;
@ -12,7 +12,10 @@ pub fn routes() -> Router<AppState> {
.route("/", get(views::index).post(views::index_action))
.route("/app/:app", get(views::list_app))
.route("/app/:app/model/:model", get(views::list_item_collection))
.route("/app/:app/model/:model/add", get(views::create_item))
.route(
"/app/:app/model/:model/add",
get(views::new_item).post(views::create_item),
)
.route(
"/app/:app/model/:model/detail/:id",
get(views::item_details),

View File

@ -1,8 +1,11 @@
use std::sync::Arc;
use axum::extract::Path;
use axum::http::HeaderMap;
use axum::Form;
use axum::{extract::State, response::IntoResponse};
use log::info;
use serde_json::Value;
use crate::admin::domain::{AdminApp, AdminModel};
use crate::admin::state;
@ -18,6 +21,7 @@ pub struct AdminRequest {
#[derive(Serialize)]
pub struct AdminContext {
pub base: Option<String>,
pub language_code: Option<String>,
pub language_bidi: Option<bool>,
pub user: Option<String>, // Todo: user type
@ -40,6 +44,7 @@ pub struct AdminContext {
impl Default for AdminContext {
fn default() -> Self {
AdminContext {
base: None,
language_code: Some("en-us".to_string()), // Default language code
language_bidi: Some(false), // Default language bidi
user: None, //UserType::default(), // Assuming UserType has a Default impl
@ -62,13 +67,25 @@ impl Default for AdminContext {
}
}
pub fn base_template(headers: &HeaderMap) -> Option<String> {
let hx_request = headers.get("HX-Request").is_some();
if hx_request {
println!("HX.");
Some("admin/base_hx.jinja".to_string())
} else {
None
}
}
pub async fn index(
templates: State<templates::Templates>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap,
) -> impl IntoResponse {
templates.render_html(
"admin/index.html",
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
..Default::default()
},
@ -91,9 +108,11 @@ pub async fn list_app(
pub async fn list_item_collection(
templates: State<templates::Templates>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap,
Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse {
info!("list_item_collection {} for model {}", app_key, model_key);
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let admin_model = registry
.get_model(&app_key, &model_key)
@ -101,8 +120,8 @@ pub async fn list_item_collection(
// Note: AdminModel contains Registry Data, while Repository only contains user data; however, both could be retrieved more reliably from each other.
// Another solution would be a clear "AdminRepositoryContext", that contains information about the current model.
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
content: model_key.to_owned(),
item_info: Some(repo.info(&admin_model)),
item_list: repo.list(&admin_model),
item_model: Some(admin_model),
@ -110,6 +129,7 @@ pub async fn list_item_collection(
}
} else {
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
..Default::default()
}
@ -128,6 +148,7 @@ pub async fn item_collection_action(
pub async fn item_details(
templates: State<templates::Templates>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap,
Path((app_key, model_key, id)): Path<(String, String, String)>,
) -> impl IntoResponse {
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
@ -138,8 +159,8 @@ pub async fn item_details(
// Another solution would be a clear "AdminRepositoryContext", that contains information about the current model.
let key: LookupKey = id.into();
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
content: model_key.to_owned(),
item_info: Some(repo.info(&admin_model)),
item_list: repo.list(&admin_model),
item: repo.get(&admin_model, key),
@ -148,6 +169,7 @@ pub async fn item_details(
}
} else {
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
..Default::default()
}
@ -155,9 +177,10 @@ pub async fn item_details(
templates.render_html("admin/items/item_detail.jinja", context)
}
pub async fn create_item(
pub async fn new_item(
templates: State<templates::Templates>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap,
Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse {
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
@ -165,8 +188,8 @@ pub async fn create_item(
.get_model(&app_key, &model_key)
.expect("Admin Model not found?");
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
content: model_key.to_owned(),
item_info: Some(repo.info(&admin_model)),
item_list: repo.list(&admin_model),
item_model: Some(admin_model),
@ -174,6 +197,42 @@ pub async fn create_item(
}
} else {
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
..Default::default()
}
};
templates.render_html("admin/items/item_create.jinja", context)
}
pub async fn create_item(
templates: State<templates::Templates>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap,
Path((app_key, model_key)): Path<(String, String)>,
Form(form): Form<Value>,
) -> impl IntoResponse {
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let admin_model = registry
.get_model(&app_key, &model_key)
.expect("Admin Model not found?");
// create our item.
let result = repo.create(&admin_model, form);
// TODO: refactor run over these views, way too much repetition.
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
item_info: Some(repo.info(&admin_model)),
item_list: repo.list(&admin_model),
item_model: Some(admin_model),
item: result,
..Default::default()
}
} else {
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
..Default::default()
}

View File

@ -107,13 +107,12 @@
<div
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
{% if model.admin_url %}
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %} aria-current="page" {%
endif %}>{{ model.name }}</a>
<a href="{{ model.admin_url }}" hx-get="{{ model.admin_url }}" hx-target="#main" hx-push-url="true" {% if
model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a>
{% else %}
<span>{{ model.name }}</span>
{% endif %}
{% if model.add_url %}<i class="plus link icon">
<a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a></i>
{% endif %}
@ -130,7 +129,7 @@
{% endblock sidebar %}
<div class="pusher pushover" id="main_content">
<main>
<main id="main">
{% block content %}
{% endblock content %}
</main>

View File

@ -0,0 +1,2 @@
{% block content %}
{% endblock content %}

View File

@ -1,4 +1,4 @@
{% extends "admin/base.jinja" %}
{% extends base|none("admin/base.jinja") %}
{% macro input(name, value="", type="text") -%}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
@ -7,7 +7,7 @@
{% block content %}
Create {{item_model.name}} in {{item_info.name}}
<form>
<form action="{{item_model.add_url}}" method="POST">
{% set fields = item_info.display_list %}
{% for field in fields %}
<p><label>{{field}}</label>{{ input(field) }}</p>

View File

@ -1,4 +1,4 @@
{% extends "admin/base.jinja" %}
{% extends base|none("admin/base.jinja") %}
{% block content %}

View File

@ -1,4 +1,4 @@
{% extends "admin/base.jinja" %}
{% extends base|none("admin/base.jinja") %}
{% block content %}
@ -39,7 +39,8 @@
<tr>
{% for key in item_keys %}
{% if key==primary_key %}
<td class="selectable warning">{% if item.detail_url %}<a href="{{item.detail_url}}">{{
<td class="selectable warning">{% if item.detail_url %}<a href="{{item.detail_url}}"
hx-get="{{item.detail_url}}" hx-target="#main" hx-push-url="true">{{
item.fields[key] }}</a>{%
else %}{{item.fields[key] }}{% endif %}</td>
{% else %}