code: list+create improvements, expanded notes, added logging
This commit is contained in:
parent
fdbc37db06
commit
1c6317808a
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -1847,6 +1847,8 @@ dependencies = [
|
|||||||
"sqlformat",
|
"sqlformat",
|
||||||
"strinto",
|
"strinto",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3477,6 +3479,23 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.4.0",
|
||||||
|
"bytes",
|
||||||
|
"http 1.0.0",
|
||||||
|
"http-body 1.0.0",
|
||||||
|
"http-body-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -3491,11 +3510,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.37"
|
version = "0.1.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
@ -3515,9 +3533,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.31"
|
version = "0.1.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
@ -52,3 +52,5 @@ serde_json = "1.0.108"
|
|||||||
slug = "0.1.5"
|
slug = "0.1.5"
|
||||||
derive_builder = "0.13.0"
|
derive_builder = "0.13.0"
|
||||||
async-trait = "0.1.77"
|
async-trait = "0.1.77"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tower-http = { version = "0.5.1", features = ["trace"] }
|
||||||
|
33
NOTES.md
33
NOTES.md
@ -156,6 +156,7 @@ As I started by quickly using a django admin template with CSS to jump start the
|
|||||||
### UiKit
|
### UiKit
|
||||||
|
|
||||||
in general really nice, but I got annoyed by uk- prefixes pretty fast.
|
in general really nice, but I got annoyed by uk- prefixes pretty fast.
|
||||||
|
Psychologically, "ui grid" seems nicer than "uk-grid", weird.
|
||||||
It would however seem to align with hx- prefixes of htmx?
|
It would however seem to align with hx- prefixes of htmx?
|
||||||
|
|
||||||
### Tailwind/DaisyUI
|
### Tailwind/DaisyUI
|
||||||
@ -175,11 +176,13 @@ As I started by quickly using a django admin template with CSS to jump start the
|
|||||||
|
|
||||||
I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use.
|
I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use.
|
||||||
|
|
||||||
|
Why fomantic won was the lack of needing a btn class.
|
||||||
|
|
||||||
### Builder Pattern etc.
|
### Builder Pattern etc.
|
||||||
|
|
||||||
// consider builder: https://docs.rs/derive_builder/latest/derive_builder/
|
consider builder: https://docs.rs/derive_builder/latest/derive_builder/
|
||||||
// downside of builders as macro: no intellisense!
|
downside of builders as macro: no intellisense!
|
||||||
// each repository has to implement a repo info.
|
|
||||||
//#[derive(Builder)]
|
//#[derive(Builder)]
|
||||||
//#[builder(setter(into))]
|
//#[builder(setter(into))]
|
||||||
|
|
||||||
@ -236,3 +239,27 @@ let dynamic_field = Field::widget(Cow::Owned(dynamic_label));
|
|||||||
- the async dynamic dispatch.
|
- the async dynamic dispatch.
|
||||||
- the building pattern debacle here
|
- the building pattern debacle here
|
||||||
|
|
||||||
|
|
||||||
|
### Async Trait Adventure
|
||||||
|
|
||||||
|
What was not noted here was the few days gone by researching async Trait dilemmas.
|
||||||
|
Given I use dynamic dispatch, the only solution is async_trait crate for now
|
||||||
|
However I am aware that this is an ongoing topic in the rust world.
|
||||||
|
|
||||||
|
For the admin this works well, and in general, it makes sense in the axum world I guess.
|
||||||
|
|
||||||
|
Other solutions if this stops being the case:
|
||||||
|
- Channels like in Go with crossbeam: https://gsquire.github.io/static/post/a-rusty-go-at-channels/
|
||||||
|
- Watch Rust Development here: https://rust-lang.github.io/async-fundamentals-initiative/index.html
|
||||||
|
|
||||||
|
### For the occasional HTMX Reload on Error bug
|
||||||
|
|
||||||
|
The header being in the history, returns a partial that cannot insert itself into the browser's error page.
|
||||||
|
|
||||||
|
Maybe a hint for a cache control problem actually?
|
||||||
|
https://github.com/bigskysoftware/htmx/issues/854
|
||||||
|
|
||||||
|
Make axum log requests!
|
||||||
|
|
||||||
|
There is another crate, that uses some MIME detection middleware for cache-control, called axum-cc, which could inspire middlewares here.
|
||||||
|
|
||||||
|
15
TODOS.md
15
TODOS.md
@ -39,3 +39,18 @@ auth:
|
|||||||
|
|
||||||
In the widget section, we need to add more abilities to adjust attributes of html elements directly.
|
In the widget section, we need to add more abilities to adjust attributes of html elements directly.
|
||||||
Probably options should be transmitted via JSON data.
|
Probably options should be transmitted via JSON data.
|
||||||
|
|
||||||
|
## Form Foomatics
|
||||||
|
|
||||||
|
- multi-item form fields (as in multiple inputs for one field, or multiple fields inside one field)
|
||||||
|
```html
|
||||||
|
<div class="field">
|
||||||
|
<label>Field Summary</label>
|
||||||
|
<div class="two fields">
|
||||||
|
<div class="field">
|
||||||
|
<input>...</input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
-
|
||||||
|
@ -60,9 +60,18 @@ pub mod repository {
|
|||||||
pub type RepositoryContext = AdminModel;
|
pub type RepositoryContext = AdminModel;
|
||||||
|
|
||||||
impl RepositoryContext {
|
impl RepositoryContext {
|
||||||
|
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/detail/{}", self.admin_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/change/{}", self.admin_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
||||||
RepositoryItem {
|
RepositoryItem {
|
||||||
detail_url: Some(format!("{}/detail/{}", self.admin_url, key)),
|
detail_url: self.get_default_detail_url(key),
|
||||||
|
change_url: self.get_default_change_url(key),
|
||||||
fields: fields,
|
fields: fields,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,6 +226,7 @@ pub mod repository {
|
|||||||
pub struct RepositoryItem {
|
pub struct RepositoryItem {
|
||||||
pub fields: Value,
|
pub fields: Value,
|
||||||
pub detail_url: Option<String>,
|
pub detail_url: Option<String>,
|
||||||
|
pub change_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum RepositoryList {
|
pub enum RepositoryList {
|
||||||
|
@ -15,8 +15,9 @@ pub fn routes() -> Router<AppState> {
|
|||||||
"/app/:app/model/:model/add",
|
"/app/:app/model/:model/add",
|
||||||
get(views::new_item).post(views::create_item),
|
get(views::new_item).post(views::create_item),
|
||||||
)
|
)
|
||||||
|
.route("/app/:app/model/:model/change/:id", get(views::change_item))
|
||||||
.route(
|
.route(
|
||||||
"/app/:app/model/:model/detail/:id",
|
"/app/:app/model/:model/detail/:id",
|
||||||
get(views::item_details),
|
get(views::view_item_details),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -116,9 +116,7 @@ pub async fn list_item_collection(
|
|||||||
let repo = repo.lock().await;
|
let repo = repo.lock().await;
|
||||||
let admin_model = registry
|
let admin_model = registry
|
||||||
.get_model(&app_key, &model_key)
|
.get_model(&app_key, &model_key)
|
||||||
.expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist
|
.expect("Admin Model not found?");
|
||||||
// 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 {
|
AdminContext {
|
||||||
base: base_template(&headers),
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
@ -145,7 +143,7 @@ pub async fn item_collection_action(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Item Details shows one single dataset.
|
// Item Details shows one single dataset.
|
||||||
pub async fn item_details(
|
pub async fn view_item_details(
|
||||||
templates: State<templates::Templates>,
|
templates: State<templates::Templates>,
|
||||||
registry: State<Arc<state::AdminRegistry>>,
|
registry: State<Arc<state::AdminRegistry>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
@ -155,9 +153,7 @@ pub async fn item_details(
|
|||||||
let repo = repo.lock().await;
|
let repo = repo.lock().await;
|
||||||
let admin_model = registry
|
let admin_model = registry
|
||||||
.get_model(&app_key, &model_key)
|
.get_model(&app_key, &model_key)
|
||||||
.expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist
|
.expect("Admin Model not found?");
|
||||||
// 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.
|
|
||||||
let key: LookupKey = id.into();
|
let key: LookupKey = id.into();
|
||||||
AdminContext {
|
AdminContext {
|
||||||
base: base_template(&headers),
|
base: base_template(&headers),
|
||||||
@ -243,6 +239,37 @@ pub async fn create_item(
|
|||||||
templates.render_html("admin/items/item_create.jinja", context)
|
templates.render_html("admin/items/item_create.jinja", context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn change_item(
|
||||||
|
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) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let admin_model = registry
|
||||||
|
.get_model(&app_key, &model_key)
|
||||||
|
.expect("Admin Model not found?");
|
||||||
|
let key: LookupKey = id.into();
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
|
item_list: repo.list(&admin_model).await,
|
||||||
|
item: repo.get(&admin_model, key).await,
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_change.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
// Item Action allows running an action on one single dataset.
|
// Item Action allows running an action on one single dataset.
|
||||||
pub async fn item_action(
|
pub async fn item_action(
|
||||||
Path((app_key, model_key, model_id)): Path<(String, String, String)>,
|
Path((app_key, model_key, model_id)): Path<(String, String, String)>,
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use crate::admin::domain::*;
|
use crate::admin::domain::*;
|
||||||
use crate::admin::state::AdminRegistry;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::{debug, warn};
|
use serde_json::Value;
|
||||||
use serde_json::{json, Value};
|
|
||||||
|
|
||||||
struct Repository {}
|
struct Repository {}
|
||||||
|
|
||||||
|
@ -44,8 +44,8 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
fields: &["name", "level", "age", "powers"],
|
fields: &["name", "level", "age", "powers"],
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
.set_widget("age", Widget::default().as_password().labeled("hi there."))
|
//.set_widget("age", Widget::default().as_password().labeled("hi there."))
|
||||||
.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
|
//.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
|
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
|
||||||
@ -117,10 +117,7 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
*item = data.clone();
|
*item = data.clone();
|
||||||
|
|
||||||
if let Some(item_id) = item.get("id") {
|
if let Some(item_id) = item.get("id") {
|
||||||
return Some(RepositoryItem {
|
return Some(model.build_item(&*item_id.to_string(), data));
|
||||||
detail_url: Some(format!("{}/detail/{}", model.admin_url, item_id)),
|
|
||||||
fields: data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
52
src/main.rs
52
src/main.rs
@ -7,6 +7,12 @@ mod state;
|
|||||||
use crate::service::{handlers, templates};
|
use crate::service::{handlers, templates};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use admin_examples::static_repository;
|
use admin_examples::static_repository;
|
||||||
|
use axum::{
|
||||||
|
body::Bytes,
|
||||||
|
extract::MatchedPath,
|
||||||
|
http::{HeaderMap, Request},
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
||||||
};
|
};
|
||||||
@ -15,7 +21,10 @@ use log::info;
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
|
||||||
|
use tracing::{info_span, Span};
|
||||||
|
|
||||||
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
|
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
|
||||||
templates.render_html("index.html", ())
|
templates.render_html("index.html", ())
|
||||||
@ -51,7 +60,48 @@ async fn main() {
|
|||||||
.nest("/howto", howto::routes())
|
.nest("/howto", howto::routes())
|
||||||
.route_service("/static/*file", handlers::static_handler.into_service())
|
.route_service("/static/*file", handlers::static_handler.into_service())
|
||||||
.fallback(handlers::not_found_handler)
|
.fallback(handlers::not_found_handler)
|
||||||
.with_state(state);
|
.with_state(state)
|
||||||
|
.layer(
|
||||||
|
TraceLayer::new_for_http()
|
||||||
|
.make_span_with(|request: &Request<_>| {
|
||||||
|
// Log the matched route's path (with placeholders not filled in).
|
||||||
|
// Use request.uri() or OriginalUri if you want the real path.
|
||||||
|
let matched_path = request
|
||||||
|
.extensions()
|
||||||
|
.get::<MatchedPath>()
|
||||||
|
.map(MatchedPath::as_str);
|
||||||
|
info_span!(
|
||||||
|
"http_request",
|
||||||
|
method = ?request.method(),
|
||||||
|
uri = ?request.uri(),
|
||||||
|
matched_path,
|
||||||
|
some_other_field = tracing::field::Empty,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_request(|_request: &Request<_>, _span: &Span| {
|
||||||
|
// You can use `_span.record("some_other_field", value)` in one of these
|
||||||
|
// closures to attach a value to the initially empty field in the info_span
|
||||||
|
// created above.
|
||||||
|
info!("Request: {:?}", _request);
|
||||||
|
})
|
||||||
|
.on_response(|_response: &Response, _latency: Duration, _span: &Span| {
|
||||||
|
// ...
|
||||||
|
info!("Response: {:?}", _response);
|
||||||
|
})
|
||||||
|
.on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
.on_eos(
|
||||||
|
|_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_failure(
|
||||||
|
|_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Run Server
|
// Run Server
|
||||||
let app_host: std::net::IpAddr = env::var("APP_HOST")
|
let app_host: std::net::IpAddr = env::var("APP_HOST")
|
||||||
|
@ -113,8 +113,9 @@
|
|||||||
<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" hx-get="{{ model.add_url }}" hx-target="#main"
|
||||||
<a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a></i>
|
hx-push-url="true">
|
||||||
|
{% if captions %}<a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a>{% endif %}</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
10
templates/admin/items/item_change.jinja
Normal file
10
templates/admin/items/item_change.jinja
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="ui container">
|
||||||
|
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
|
||||||
|
{% set fields = item_info.fields %}
|
||||||
|
{% set form = { 'action': item.change_url } %}
|
||||||
|
{% include "/admin/items/item_change_form.jinja" %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
16
templates/admin/items/item_change_form.jinja
Normal file
16
templates/admin/items/item_change_form.jinja
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<form action="{{form.action}}" method="{{form.method|default('POST')}}" class="ui form">
|
||||||
|
<!--noformat-->
|
||||||
|
{% from "/admin/items/items.jinja" import field_widget %}
|
||||||
|
{% for field_name, field_defs in fields %}
|
||||||
|
{% if item %}
|
||||||
|
{% set field_value = item.fields[field_name]|none("") %}
|
||||||
|
{% else %}
|
||||||
|
{% set field_value = "" %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="field">
|
||||||
|
{{ field_widget(field_name, field_defs, field_value) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<!--noformat-->
|
||||||
|
<button class="ui button" type="submit">Create</button>
|
||||||
|
</form>
|
@ -1,22 +1,17 @@
|
|||||||
{% extends base|none("admin/base.jinja") %}
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from "/admin/items/items.jinja" import field_widget %}
|
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<h1>Create {{item_model.name}} in {{item_info.name}}</h1>
|
<h1>Create {{item_model.name}} in {{item_info.name}}</h1>
|
||||||
|
|
||||||
<form action="{{item_model.add_url}}" method="POST" class="ui form">
|
{% block navigation_bar %}
|
||||||
<!--noformat-->
|
<div class="ui container">
|
||||||
{% set fields = item_info.fields %}
|
<button hx-get="{{ item_model.admin_url }}" hx-target="#main" hx-push-url="true">List Items</button>
|
||||||
{% for field_name, field_defs in fields %}
|
</div>
|
||||||
{% if item %}
|
{% endblock %}
|
||||||
{% set field_value = item.fields[field_name]|none("") %}
|
|
||||||
{% else %}
|
{% set fields = item_info.fields %}
|
||||||
{% set field_value = "" %}
|
{% set form = { 'action': item_model.add_url } %}
|
||||||
{% endif %}
|
{% include "/admin/items/item_change_form.jinja" %}
|
||||||
{{ field_widget(field_name, field_defs, field_value) }}
|
|
||||||
{% endfor %}
|
|
||||||
<button class="ui button" type="submit">Create</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -1,57 +1,84 @@
|
|||||||
{% extends base|none("admin/base.jinja") %}
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="ui container">
|
||||||
|
<h1>List of {{ item_info.name }}</h1>
|
||||||
|
|
||||||
{% block search_bar %}
|
{% block navigation_bar %}
|
||||||
<div class="ui icon input">
|
<div class="ui container">
|
||||||
<input type="text" placeholder="Search...">
|
<button hx-get="{{ item_model.add_url }}" hx-target="#main" hx-push-url="true">Create New</button>
|
||||||
<i class="circular search link icon"></i>
|
</div>
|
||||||
</div>
|
{% endblock %}
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% if item_list %}
|
{% block search_bar %}
|
||||||
{% set item_keys = item_info.display_list or item_list|table_keys %}
|
<div class="ui icon input">
|
||||||
|
<input type="text" placeholder="Search...">
|
||||||
|
<i class="circular search link icon"></i>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% if item_info.lookup_key in item_keys %}
|
{% if item_list %}
|
||||||
{% set primary_key = item_info.lookup_key %}
|
{% set item_keys = item_info.display_list or item_list|table_keys %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="ui basic container">
|
{% if item_info.lookup_key in item_keys %}
|
||||||
<table class="ui very compact celled table head stuck unstackable">
|
{% set primary_key = item_info.lookup_key %}
|
||||||
<caption>{{ item_info.name|none(content) }}</caption>
|
{% endif %}
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{% for key in item_keys %}
|
|
||||||
{% if key==primary_key %}
|
|
||||||
<th class="blue"><i class="key icon"></i>
|
|
||||||
{{ key|capitalize }}
|
|
||||||
</th>
|
|
||||||
{% else %}
|
|
||||||
<th>{{ key|capitalize }}</th>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endfor %}
|
<div class="ui basic container">
|
||||||
</tr>
|
<table class="ui very compact celled table head stuck unstackable">
|
||||||
</thead>
|
<caption>{{ item_info.name }}</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="collapsing grey"></th>
|
||||||
|
{% for key in item_keys %}
|
||||||
|
{% if key==primary_key %}
|
||||||
|
<th class="blue"><i class="key icon"></i>
|
||||||
|
{{ key|capitalize }}
|
||||||
|
</th>
|
||||||
|
{% else %}
|
||||||
|
<th>{{ key|capitalize }}</th>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<tbody>
|
{% endfor %}
|
||||||
{% for item in item_list %}
|
|
||||||
<tr>
|
</tr>
|
||||||
{% for key in item_keys %}
|
</thead>
|
||||||
{% if key==primary_key %}
|
|
||||||
<td class="selectable warning">{% if item.detail_url %}<a href="{{item.detail_url}}"
|
<tbody>
|
||||||
hx-get="{{item.detail_url}}" hx-target="#main" hx-push-url="true">{{
|
{% for item in item_list %}
|
||||||
|
<tr>
|
||||||
|
<td class="collapsing">
|
||||||
|
<div class="ui buttons">
|
||||||
|
<button class="ui compact icon button left attached" {% if item.detail_url %} hx-get="{{item.detail_url}}"
|
||||||
|
hx-target="#main" hx-push-url="true" {% endif %}>
|
||||||
|
<i class="eye icon"></i>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
<button class="ui compact icon button right attached" {% if item.change_url %}
|
||||||
|
hx-get="{{item.change_url}}" hx-target="#main" hx-push-url="true" {% endif %}>
|
||||||
|
<i class="edit icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{% for key in item_keys %}
|
||||||
|
{% if key==primary_key %}
|
||||||
|
<td class="selectable warning">{% if item.change_url %}<a href="{{item.change_url}}"
|
||||||
|
hx-get="{{item.change_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 %}
|
||||||
<td>{{ item.fields[key] }}</td>{% endif %}
|
<td>{{ item.fields[key] }}</td>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tbody>
|
||||||
{% endfor %}
|
<tfoot>
|
||||||
</tbody>
|
</tfoot>
|
||||||
<tfoot>
|
</table>
|
||||||
</tfoot>
|
</div>
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
No Items found.
|
No Items found.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<div class="inline field">
|
<div class="ui input {% if 'disabled' in field.options and field.options.disabled %}disabled{% endif %} inline field">
|
||||||
<label>{{field.label or field.name|capitalize}}</label>
|
<label>{{field.label or field.name|capitalize}}</label>
|
||||||
<input type="{{ field.type|none('text') }}" name="{{ field.name }}" value="{{ field.value }}" placeholder="{{field.placeholder}}">
|
<input type="{{ field.type|none('text') }}" name="{{ field.name }}" value="{{ field.value }}"
|
||||||
|
placeholder="{{field.placeholder}}">
|
||||||
</div>
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
<div class="inline field">
|
<div class="ui input {% if 'disabled' in field.options and field.options.disabled %}disabled{% endif %} inline field">
|
||||||
<label>{{ field.label or field.name|capitalize }}</label>
|
<label>{{ field.label or field.name|capitalize }}</label>
|
||||||
<!--noformat-->
|
<!--noformat-->
|
||||||
<textarea name="{{ field.name }}" {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
|
<textarea name="{{ field.name }}" {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
|
||||||
|
8
templates/admin/widgets/select_labeled.jinja
Normal file
8
templates/admin/widgets/select_labeled.jinja
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<div class="ui labeled input">
|
||||||
|
<div class="ui label">{{field.label or field.name|capitalize}}</div>
|
||||||
|
<select class="ui selection dropdown">
|
||||||
|
{% for value, label in field.value %}
|
||||||
|
<option value="{{value}}">{{label}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user