code: basic create view and HX-Header integration (first htmx)
This commit is contained in:
parent
c439220409
commit
9feb2f4ae7
20
TODOS.md
20
TODOS.md
@ -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
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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>
|
||||
|
2
templates/admin/base_hx.jinja
Normal file
2
templates/admin/base_hx.jinja
Normal file
@ -0,0 +1,2 @@
|
||||
{% block content %}
|
||||
{% endblock content %}
|
@ -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>
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends "admin/base.jinja" %}
|
||||
{% extends base|none("admin/base.jinja") %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user