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
|
- [ ] 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 info(&self, model: &AdminModel) -> RepositoryInfo;
|
||||||
fn list(&self, model: &AdminModel) -> RepositoryList;
|
fn list(&self, model: &AdminModel) -> RepositoryList;
|
||||||
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>;
|
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>;
|
||||||
fn create(&self, model: &AdminModel, data: Value) -> Option<Value>;
|
fn create(&self, model: &AdminModel, data: Value) -> Option<RepositoryItem>;
|
||||||
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<Value>;
|
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem>;
|
||||||
fn delete(&self, model: &AdminModel, id: LookupKey) -> Option<Value>;
|
fn delete(&self, model: &AdminModel, id: LookupKey) -> Option<Value>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,12 +75,12 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
.into()
|
.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);
|
println!("I would now create: {}", data);
|
||||||
None
|
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);
|
println!("I would now update: {}, {}", id, data);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, routing::post, Router};
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@ -12,7 +12,10 @@ pub fn routes() -> Router<AppState> {
|
|||||||
.route("/", get(views::index).post(views::index_action))
|
.route("/", get(views::index).post(views::index_action))
|
||||||
.route("/app/:app", get(views::list_app))
|
.route("/app/:app", get(views::list_app))
|
||||||
.route("/app/:app/model/:model", get(views::list_item_collection))
|
.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(
|
.route(
|
||||||
"/app/:app/model/:model/detail/:id",
|
"/app/:app/model/:model/detail/:id",
|
||||||
get(views::item_details),
|
get(views::item_details),
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::Form;
|
||||||
use axum::{extract::State, response::IntoResponse};
|
use axum::{extract::State, response::IntoResponse};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::admin::domain::{AdminApp, AdminModel};
|
use crate::admin::domain::{AdminApp, AdminModel};
|
||||||
use crate::admin::state;
|
use crate::admin::state;
|
||||||
@ -18,6 +21,7 @@ pub struct AdminRequest {
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct AdminContext {
|
pub struct AdminContext {
|
||||||
|
pub base: Option<String>,
|
||||||
pub language_code: Option<String>,
|
pub language_code: Option<String>,
|
||||||
pub language_bidi: Option<bool>,
|
pub language_bidi: Option<bool>,
|
||||||
pub user: Option<String>, // Todo: user type
|
pub user: Option<String>, // Todo: user type
|
||||||
@ -40,6 +44,7 @@ pub struct AdminContext {
|
|||||||
impl Default for AdminContext {
|
impl Default for AdminContext {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
AdminContext {
|
AdminContext {
|
||||||
|
base: None,
|
||||||
language_code: Some("en-us".to_string()), // Default language code
|
language_code: Some("en-us".to_string()), // Default language code
|
||||||
language_bidi: Some(false), // Default language bidi
|
language_bidi: Some(false), // Default language bidi
|
||||||
user: None, //UserType::default(), // Assuming UserType has a Default impl
|
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(
|
pub async fn index(
|
||||||
templates: State<templates::Templates>,
|
templates: State<templates::Templates>,
|
||||||
registry: State<Arc<state::AdminRegistry>>,
|
registry: State<Arc<state::AdminRegistry>>,
|
||||||
|
headers: HeaderMap,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
templates.render_html(
|
templates.render_html(
|
||||||
"admin/index.html",
|
"admin/index.html",
|
||||||
AdminContext {
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@ -91,9 +108,11 @@ pub async fn list_app(
|
|||||||
pub async fn list_item_collection(
|
pub async fn list_item_collection(
|
||||||
templates: State<templates::Templates>,
|
templates: State<templates::Templates>,
|
||||||
registry: State<Arc<state::AdminRegistry>>,
|
registry: State<Arc<state::AdminRegistry>>,
|
||||||
|
headers: HeaderMap,
|
||||||
Path((app_key, model_key)): Path<(String, String)>,
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("list_item_collection {} for model {}", app_key, model_key);
|
info!("list_item_collection {} for model {}", app_key, model_key);
|
||||||
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
let admin_model = registry
|
let admin_model = registry
|
||||||
.get_model(&app_key, &model_key)
|
.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.
|
// 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.
|
// Another solution would be a clear "AdminRepositoryContext", that contains information about the current model.
|
||||||
AdminContext {
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
content: model_key.to_owned(),
|
|
||||||
item_info: Some(repo.info(&admin_model)),
|
item_info: Some(repo.info(&admin_model)),
|
||||||
item_list: repo.list(&admin_model),
|
item_list: repo.list(&admin_model),
|
||||||
item_model: Some(admin_model),
|
item_model: Some(admin_model),
|
||||||
@ -110,6 +129,7 @@ pub async fn list_item_collection(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AdminContext {
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -128,6 +148,7 @@ pub async fn item_collection_action(
|
|||||||
pub async fn item_details(
|
pub async fn item_details(
|
||||||
templates: State<templates::Templates>,
|
templates: State<templates::Templates>,
|
||||||
registry: State<Arc<state::AdminRegistry>>,
|
registry: State<Arc<state::AdminRegistry>>,
|
||||||
|
headers: HeaderMap,
|
||||||
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
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.
|
// Another solution would be a clear "AdminRepositoryContext", that contains information about the current model.
|
||||||
let key: LookupKey = id.into();
|
let key: LookupKey = id.into();
|
||||||
AdminContext {
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
content: model_key.to_owned(),
|
|
||||||
item_info: Some(repo.info(&admin_model)),
|
item_info: Some(repo.info(&admin_model)),
|
||||||
item_list: repo.list(&admin_model),
|
item_list: repo.list(&admin_model),
|
||||||
item: repo.get(&admin_model, key),
|
item: repo.get(&admin_model, key),
|
||||||
@ -148,6 +169,7 @@ pub async fn item_details(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AdminContext {
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -155,9 +177,10 @@ pub async fn item_details(
|
|||||||
templates.render_html("admin/items/item_detail.jinja", context)
|
templates.render_html("admin/items/item_detail.jinja", context)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_item(
|
pub async fn new_item(
|
||||||
templates: State<templates::Templates>,
|
templates: State<templates::Templates>,
|
||||||
registry: State<Arc<state::AdminRegistry>>,
|
registry: State<Arc<state::AdminRegistry>>,
|
||||||
|
headers: HeaderMap,
|
||||||
Path((app_key, model_key)): Path<(String, String)>,
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
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)
|
.get_model(&app_key, &model_key)
|
||||||
.expect("Admin Model not found?");
|
.expect("Admin Model not found?");
|
||||||
AdminContext {
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
content: model_key.to_owned(),
|
|
||||||
item_info: Some(repo.info(&admin_model)),
|
item_info: Some(repo.info(&admin_model)),
|
||||||
item_list: repo.list(&admin_model),
|
item_list: repo.list(&admin_model),
|
||||||
item_model: Some(admin_model),
|
item_model: Some(admin_model),
|
||||||
@ -174,6 +197,42 @@ pub async fn create_item(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AdminContext {
|
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(),
|
available_apps: registry.get_apps(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -107,13 +107,12 @@
|
|||||||
<div
|
<div
|
||||||
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
{% if model.admin_url %}
|
{% if model.admin_url %}
|
||||||
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %} aria-current="page" {%
|
<a href="{{ model.admin_url }}" hx-get="{{ model.admin_url }}" hx-target="#main" hx-push-url="true" {% if
|
||||||
endif %}>{{ model.name }}</a>
|
model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>{{ model.name }}</span>
|
<span>{{ model.name }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if model.add_url %}<i class="plus link icon">
|
{% if model.add_url %}<i class="plus link icon">
|
||||||
<a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a></i>
|
<a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -130,7 +129,7 @@
|
|||||||
{% endblock sidebar %}
|
{% endblock sidebar %}
|
||||||
|
|
||||||
<div class="pusher pushover" id="main_content">
|
<div class="pusher pushover" id="main_content">
|
||||||
<main>
|
<main id="main">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</main>
|
</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") -%}
|
{% macro input(name, value="", type="text") -%}
|
||||||
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
|
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
|
||||||
@ -7,7 +7,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
Create {{item_model.name}} in {{item_info.name}}
|
Create {{item_model.name}} in {{item_info.name}}
|
||||||
|
|
||||||
<form>
|
<form action="{{item_model.add_url}}" method="POST">
|
||||||
{% set fields = item_info.display_list %}
|
{% set fields = item_info.display_list %}
|
||||||
{% for field in fields %}
|
{% for field in fields %}
|
||||||
<p><label>{{field}}</label>{{ input(field) }}</p>
|
<p><label>{{field}}</label>{{ input(field) }}</p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends "admin/base.jinja" %}
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends "admin/base.jinja" %}
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -39,7 +39,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
{% for key in item_keys %}
|
{% for key in item_keys %}
|
||||||
{% if key==primary_key %}
|
{% 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>{%
|
item.fields[key] }}</a>{%
|
||||||
else %}{{item.fields[key] }}{% endif %}</td>
|
else %}{{item.fields[key] }}{% endif %}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
Loading…
Reference in New Issue
Block a user