code: rudimentary implementation of detail and create
This commit is contained in:
parent
d1c98516a8
commit
c439220409
1
TODOS.md
1
TODOS.md
@ -9,3 +9,4 @@
|
|||||||
- [ ] better 404 handling
|
- [ ] better 404 handling
|
||||||
- [ ] better 500 handling
|
- [ ] better 500 handling
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ pub use config::AdminModelConfig;
|
|||||||
pub use dto::AdminApp;
|
pub use dto::AdminApp;
|
||||||
pub use dto::AdminModel;
|
pub use dto::AdminModel;
|
||||||
pub use repository::{
|
pub use repository::{
|
||||||
AdminRepository, RepoInfo, RepositoryInfo, RepositoryInfoBuilder, RepositoryList,
|
AdminRepository, LookupKey, RepoInfo, RepositoryInfo, RepositoryInfoBuilder, RepositoryItem,
|
||||||
|
RepositoryList,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod auth {
|
mod auth {
|
||||||
@ -49,29 +50,77 @@ mod dto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod repository {
|
pub mod repository {
|
||||||
|
use super::dto::AdminModel;
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::vec::IntoIter;
|
use std::vec::IntoIter;
|
||||||
|
|
||||||
|
pub enum LookupKey {
|
||||||
|
Integer(usize),
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that LookupKey auto converts to integer.
|
||||||
|
impl From<String> for LookupKey {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
if let Ok(int_key) = s.parse::<usize>() {
|
||||||
|
LookupKey::Integer(int_key)
|
||||||
|
} else {
|
||||||
|
LookupKey::String(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for LookupKey {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
if let Ok(int_key) = s.parse::<usize>() {
|
||||||
|
LookupKey::Integer(int_key)
|
||||||
|
} else {
|
||||||
|
LookupKey::String(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<usize> for LookupKey {
|
||||||
|
fn from(i: usize) -> Self {
|
||||||
|
LookupKey::Integer(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for LookupKey {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
LookupKey::Integer(i) => write!(f, "{}", i),
|
||||||
|
LookupKey::String(s) => write!(f, "{}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RepositoryItem {
|
||||||
|
pub fields: Value,
|
||||||
|
pub detail_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub enum RepositoryList {
|
pub enum RepositoryList {
|
||||||
Empty,
|
Empty,
|
||||||
List {
|
List {
|
||||||
values: Vec<Value>,
|
values: Vec<RepositoryItem>,
|
||||||
},
|
},
|
||||||
Page {
|
Page {
|
||||||
values: Vec<Value>,
|
values: Vec<RepositoryItem>,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
total: usize,
|
total: usize,
|
||||||
},
|
},
|
||||||
Stream {
|
Stream {
|
||||||
values: Vec<Value>,
|
values: Vec<RepositoryItem>,
|
||||||
next_index: Option<String>,
|
next_index: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoIterator for RepositoryList {
|
impl IntoIterator for RepositoryList {
|
||||||
type Item = Value;
|
type Item = RepositoryItem;
|
||||||
type IntoIter = IntoIter<Self::Item>;
|
type IntoIter = IntoIter<Self::Item>;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
@ -148,11 +197,11 @@ pub mod repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait AdminRepository: Send + Sync {
|
pub trait AdminRepository: Send + Sync {
|
||||||
fn info(&self) -> RepositoryInfo;
|
fn info(&self, model: &AdminModel) -> RepositoryInfo;
|
||||||
fn list(&self) -> RepositoryList;
|
fn list(&self, model: &AdminModel) -> RepositoryList;
|
||||||
fn get(&self, id: usize) -> Option<Value>;
|
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>;
|
||||||
fn create(&self, data: Value) -> Option<Value>;
|
fn create(&self, model: &AdminModel, data: Value) -> Option<Value>;
|
||||||
fn update(&self, id: usize, data: Value) -> Option<Value>;
|
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<Value>;
|
||||||
fn delete(&self, id: usize) -> Option<Value>;
|
fn delete(&self, model: &AdminModel, id: LookupKey) -> Option<Value>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// implementation of static repository
|
// implementation of static repository
|
||||||
|
|
||||||
use super::domain::{AdminModelConfig, AdminRepository, RepoInfo, RepositoryInfo, RepositoryList};
|
use super::domain::*;
|
||||||
use super::state::AdminRegistry;
|
use super::state::AdminRegistry;
|
||||||
|
use log::warn;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
struct MyStaticRepository {
|
struct MyStaticRepository {
|
||||||
@ -18,24 +19,54 @@ impl MyStaticRepository {
|
|||||||
json!({"id": 4, "name": "Rex", "age": 72}),
|
json!({"id": 4, "name": "Rex", "age": 72}),
|
||||||
json!({"id": 5, "name": "Justin", "age": 46}),
|
json!({"id": 5, "name": "Justin", "age": 46}),
|
||||||
json!({"id": 6, "name": "Reacher", "age": 39, "level": "adept", "powers": 0}),
|
json!({"id": 6, "name": "Reacher", "age": 39, "level": "adept", "powers": 0}),
|
||||||
json!({"id": 7, "name": "Arnold", "age": 64}),
|
json!({"id": 7, "name": "Arnold", "age": 7}),
|
||||||
|
json!({"id": 8, "name": "Eight", "age": 8}),
|
||||||
|
json!({"id": 9, "name": "Nine", "age": 9}),
|
||||||
|
json!({"id": 10, "name": "Ten", "age": 10}),
|
||||||
|
json!({"id": 11, "name": "Eleven", "age": 11}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminRepository for MyStaticRepository {
|
impl AdminRepository for MyStaticRepository {
|
||||||
fn get(&self, id: usize) -> Option<Value> {
|
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem> {
|
||||||
self.content.get(id).cloned()
|
if let LookupKey::Integer(id) = id {
|
||||||
|
let item = self.content.get(id - 1).cloned().unwrap();
|
||||||
|
Some(RepositoryItem {
|
||||||
|
detail_url: Some(format!(
|
||||||
|
"{}/detail/{}",
|
||||||
|
model.admin_url,
|
||||||
|
item.get("id").unwrap()
|
||||||
|
)),
|
||||||
|
fields: item,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
warn!("Got non-integer lookup key: {}", id);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(&self) -> RepositoryList {
|
fn list(&self, model: &AdminModel) -> RepositoryList {
|
||||||
|
// Admin needs to inject info in these.
|
||||||
RepositoryList::List {
|
RepositoryList::List {
|
||||||
values: self.content.clone(),
|
values: self
|
||||||
|
.content
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| RepositoryItem {
|
||||||
|
detail_url: Some(format!(
|
||||||
|
"{}/detail/{}",
|
||||||
|
model.admin_url,
|
||||||
|
item.get("id").unwrap()
|
||||||
|
)),
|
||||||
|
fields: item,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(&self) -> RepositoryInfo {
|
fn info(&self, model: &AdminModel) -> RepositoryInfo {
|
||||||
RepoInfo {
|
RepoInfo {
|
||||||
name: "My Static Repository",
|
name: "My Static Repository",
|
||||||
lookup_key: "id",
|
lookup_key: "id",
|
||||||
@ -44,17 +75,17 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(&self, data: Value) -> Option<Value> {
|
fn create(&self, model: &AdminModel, data: Value) -> Option<Value> {
|
||||||
println!("I would now create: {}", data);
|
println!("I would now create: {}", data);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&self, id: usize, data: Value) -> Option<Value> {
|
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<Value> {
|
||||||
println!("I would now update: {}, {}", id, data);
|
println!("I would now update: {}, {}", id, data);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete(&self, id: usize) -> Option<Value> {
|
fn delete(&self, model: &AdminModel, id: LookupKey) -> Option<Value> {
|
||||||
println!("Would delete: {}", id);
|
println!("Would delete: {}", id);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ 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(
|
.route(
|
||||||
"/app/:app/model/:model/detail/:id",
|
"/app/:app/model/:model/detail/:id",
|
||||||
get(views::item_details),
|
get(views::item_details),
|
||||||
|
@ -56,15 +56,16 @@ impl AdminRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn model_from_internal(&self, internal_model: &internal::AdminModel) -> AdminModel {
|
fn model_from_internal(&self, internal_model: &internal::AdminModel) -> AdminModel {
|
||||||
|
let admin_url = format!(
|
||||||
|
"/{}/app/{}/model/{}",
|
||||||
|
self.base_path, internal_model.app_key, internal_model.model_key
|
||||||
|
);
|
||||||
AdminModel {
|
AdminModel {
|
||||||
key: internal_model.model_key.clone(),
|
key: internal_model.model_key.clone(),
|
||||||
name: internal_model.name.clone(),
|
name: internal_model.name.clone(),
|
||||||
admin_url: format!(
|
|
||||||
"/{}/app/{}/model/{}",
|
|
||||||
self.base_path, internal_model.app_key, internal_model.model_key
|
|
||||||
),
|
|
||||||
view_only: false,
|
view_only: false,
|
||||||
add_url: None,
|
add_url: Some(format!("{}/add", admin_url)),
|
||||||
|
admin_url: admin_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,9 +94,9 @@ impl AdminRegistry {
|
|||||||
if self.models.contains_key(&local_config_name) {
|
if self.models.contains_key(&local_config_name) {
|
||||||
return Err(format!("Model {} already exists", local_config_name));
|
return Err(format!("Model {} already exists", local_config_name));
|
||||||
}
|
}
|
||||||
let model_key = local_config.model_key.clone();
|
let full_model_key = local_config_name.clone();
|
||||||
self.models.insert(local_config_name, local_config);
|
self.models.insert(local_config_name, local_config);
|
||||||
Ok(model_key)
|
Ok(full_model_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_model<R: AdminRepository + 'static>(
|
pub fn register_model<R: AdminRepository + 'static>(
|
||||||
@ -108,8 +109,13 @@ impl AdminRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_repository(&self, model_key: &str) -> Result<&Box<dyn AdminRepository>, String> {
|
pub fn get_repository(
|
||||||
if let Some(repo) = self.repositories.get(model_key) {
|
&self,
|
||||||
|
app_key: &str,
|
||||||
|
model_key: &str,
|
||||||
|
) -> Result<&Box<dyn AdminRepository>, String> {
|
||||||
|
let full_model_key = format!("{}.{}", app_key, model_key);
|
||||||
|
if let Some(repo) = self.repositories.get(&full_model_key) {
|
||||||
return Ok(repo);
|
return Ok(repo);
|
||||||
} else {
|
} else {
|
||||||
return Err("Couldn't find repository".to_owned());
|
return Err("Couldn't find repository".to_owned());
|
||||||
@ -143,4 +149,14 @@ mod internal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<(&str, &str)> for AdminModel {
|
||||||
|
fn from(value: (&str, &str)) -> Self {
|
||||||
|
AdminModel {
|
||||||
|
app_key: value.0.to_owned(),
|
||||||
|
model_key: slug::slugify(value.1.to_owned()),
|
||||||
|
name: value.1.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,15 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::{extract::State, response::IntoResponse, Form};
|
use axum::{extract::State, response::IntoResponse};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::admin::domain::{AdminApp, AdminModel};
|
use crate::admin::domain::{AdminApp, AdminModel};
|
||||||
use crate::admin::state;
|
use crate::admin::state;
|
||||||
use crate::service::templates;
|
use crate::service::templates;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use super::domain::{RepositoryInfo, RepositoryList};
|
use super::domain::{LookupKey, RepositoryInfo, RepositoryItem, RepositoryList};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Question {
|
|
||||||
question: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct ExampleData {
|
|
||||||
title: String,
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct AdminRequest {
|
pub struct AdminRequest {
|
||||||
@ -43,9 +31,10 @@ pub struct AdminContext {
|
|||||||
|
|
||||||
pub request: AdminRequest,
|
pub request: AdminRequest,
|
||||||
pub available_apps: Vec<AdminApp>,
|
pub available_apps: Vec<AdminApp>,
|
||||||
|
pub item_model: Option<AdminModel>,
|
||||||
pub item_info: Option<RepositoryInfo>,
|
pub item_info: Option<RepositoryInfo>,
|
||||||
pub item_list: RepositoryList,
|
pub item_list: RepositoryList,
|
||||||
pub item: Option<Value>,
|
pub item: Option<RepositoryItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AdminContext {
|
impl Default for AdminContext {
|
||||||
@ -65,6 +54,7 @@ impl Default for AdminContext {
|
|||||||
request: AdminRequest {
|
request: AdminRequest {
|
||||||
path: "".to_owned(),
|
path: "".to_owned(),
|
||||||
},
|
},
|
||||||
|
item_model: None,
|
||||||
item_info: None,
|
item_info: None,
|
||||||
item_list: RepositoryList::Empty,
|
item_list: RepositoryList::Empty,
|
||||||
item: None,
|
item: None,
|
||||||
@ -86,7 +76,7 @@ pub async fn index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Index Action is POST to the index site. We can anchor some general business code here.
|
// Index Action is POST to the index site. We can anchor some general business code here.
|
||||||
pub async fn index_action(Form(example_data): Form<ExampleData>) -> impl IntoResponse {
|
pub async fn index_action() -> impl IntoResponse {
|
||||||
"There is your answer!".to_owned()
|
"There is your answer!".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,13 +94,18 @@ pub async fn list_item_collection(
|
|||||||
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(&model_key) {
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
// we should consider using Vec<Value> instead in get_list.
|
let admin_model = registry
|
||||||
|
.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
|
||||||
|
// 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 {
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
content: model_key.to_owned(),
|
content: model_key.to_owned(),
|
||||||
item_info: Some(repo.info()),
|
item_info: Some(repo.info(&admin_model)),
|
||||||
item_list: repo.list(),
|
item_list: repo.list(&admin_model),
|
||||||
|
item_model: Some(admin_model),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -124,7 +119,6 @@ pub async fn list_item_collection(
|
|||||||
|
|
||||||
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input.
|
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input.
|
||||||
pub async fn item_collection_action(
|
pub async fn item_collection_action(
|
||||||
Form(question): Form<Question>,
|
|
||||||
Path((app_key, model_key)): Path<(String, String)>,
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
"There is your answer!".to_owned()
|
"There is your answer!".to_owned()
|
||||||
@ -133,14 +127,62 @@ 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 item_details(
|
||||||
templates: State<templates::Templates>,
|
templates: State<templates::Templates>,
|
||||||
|
registry: State<Arc<state::AdminRegistry>>,
|
||||||
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
templates.render_html("admin/items/item_detail.jinja", ())
|
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?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist
|
||||||
|
// 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();
|
||||||
|
AdminContext {
|
||||||
|
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),
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_detail.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_item(
|
||||||
|
templates: State<templates::Templates>,
|
||||||
|
registry: State<Arc<state::AdminRegistry>>,
|
||||||
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> 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?");
|
||||||
|
AdminContext {
|
||||||
|
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),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_create.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(
|
||||||
Form(question): Form<Question>,
|
|
||||||
Path((app_key, model_key, model_id)): Path<(String, String, String)>,
|
Path((app_key, model_key, model_id)): Path<(String, String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
"There is your answer!".to_owned()
|
"There is your answer!".to_owned()
|
||||||
|
@ -20,11 +20,13 @@
|
|||||||
margin-left: var(--sidebar-margin);
|
margin-left: var(--sidebar-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@media screen and (min-width: 1920px) {
|
@media screen and (min-width: 1920px) {
|
||||||
.pushover {
|
.pushover {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#main_sidemenu {
|
#main_sidemenu {
|
||||||
background-color: rgb(27, 28, 29);
|
background-color: rgb(27, 28, 29);
|
||||||
|
@ -110,12 +110,12 @@
|
|||||||
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %} aria-current="page" {%
|
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %} aria-current="page" {%
|
||||||
endif %}>{{ model.name }}</a>
|
endif %}>{{ model.name }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ model.name }}
|
<span>{{ model.name }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<i class="plus link icon"></i>
|
|
||||||
{% if model.add_url %}
|
{% if model.add_url %}<i class="plus link icon">
|
||||||
<a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a>
|
<a href="{{ model.add_url }}" class="addlink">{{ translate( 'Add') }}</a></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
17
templates/admin/items/item_create.jinja
Normal file
17
templates/admin/items/item_create.jinja
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "admin/base.jinja" %}
|
||||||
|
|
||||||
|
{% macro input(name, value="", type="text") -%}
|
||||||
|
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
Create {{item_model.name}} in {{item_info.name}}
|
||||||
|
|
||||||
|
<form>
|
||||||
|
{% set fields = item_info.display_list %}
|
||||||
|
{% for field in fields %}
|
||||||
|
<p><label>{{field}}</label>{{ input(field) }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" name="submit" value="Create">
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
@ -1 +1,11 @@
|
|||||||
Item Detail.
|
{% extends "admin/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if item %}
|
||||||
|
{{item.fields}}
|
||||||
|
{% else %}
|
||||||
|
No Item found.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock content %}
|
@ -16,7 +16,7 @@
|
|||||||
{% set primary_key = item_info.lookup_key %}
|
{% set primary_key = item_info.lookup_key %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="ui short scrolling container">
|
<div class="ui basic container">
|
||||||
<table class="ui very compact celled table head stuck unstackable">
|
<table class="ui very compact celled table head stuck unstackable">
|
||||||
<caption>{{ item_info.name|none(content) }}</caption>
|
<caption>{{ item_info.name|none(content) }}</caption>
|
||||||
<thead>
|
<thead>
|
||||||
@ -39,12 +39,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
{% for key in item_keys %}
|
{% for key in item_keys %}
|
||||||
{% if key==primary_key %}
|
{% if key==primary_key %}
|
||||||
<td class="selectable warning">
|
<td class="selectable warning">{% if item.detail_url %}<a href="{{item.detail_url}}">{{
|
||||||
<a href="#">{{ item[key] }}</a>
|
item.fields[key] }}</a>{%
|
||||||
</td>
|
else %}{{item.fields[key] }}{% endif %}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{{ item[key] }}</td>
|
<td>{{ item.fields[key] }}</td>{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
Loading…
Reference in New Issue
Block a user