code: widget implementation, almost there
This commit is contained in:
parent
e8ddfb25fa
commit
6e0a71b7de
59
Cargo.lock
generated
59
Cargo.lock
generated
@ -306,7 +306,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -317,13 +317,13 @@ checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.73"
|
version = "0.1.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
|
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -574,7 +574,7 @@ dependencies = [
|
|||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
"syn_derive",
|
"syn_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -693,7 +693,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1572,7 +1572,7 @@ checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1825,6 +1825,7 @@ name = "miniweb"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"axum 0.7.4",
|
"axum 0.7.4",
|
||||||
"barrel",
|
"barrel",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
@ -2032,7 +2033,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2083,7 +2084,7 @@ dependencies = [
|
|||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2153,7 +2154,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2284,9 +2285,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.67"
|
version = "1.0.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@ -2325,9 +2326,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.33"
|
version = "1.0.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@ -2510,7 +2511,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2620,7 +2621,7 @@ dependencies = [
|
|||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2678,7 +2679,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"sea-bae",
|
"sea-bae",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2742,7 +2743,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2821,7 +2822,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3234,7 +3235,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3268,9 +3269,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.37"
|
version = "2.0.48"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
|
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3286,7 +3287,7 @@ dependencies = [
|
|||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3331,7 +3332,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3415,7 +3416,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3509,7 +3510,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3698,7 +3699,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3732,7 +3733,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
@ -4032,7 +4033,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.37",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -51,3 +51,4 @@ env_logger = "0.11"
|
|||||||
serde_json = "1.0.108"
|
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"
|
||||||
|
3
Justfile
3
Justfile
@ -4,6 +4,9 @@ default:
|
|||||||
@echo "# Miniweb Project"
|
@echo "# Miniweb Project"
|
||||||
@just --list -u
|
@just --list -u
|
||||||
|
|
||||||
|
build:
|
||||||
|
@cargo build
|
||||||
|
|
||||||
# Run the project (just run), or commands with --bin (just run <command>)
|
# Run the project (just run), or commands with --bin (just run <command>)
|
||||||
run args='miniweb':
|
run args='miniweb':
|
||||||
@cargo run --bin {{args}}
|
@cargo run --bin {{args}}
|
||||||
|
61
NOTES.md
61
NOTES.md
@ -175,3 +175,64 @@ 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.
|
||||||
|
|
||||||
|
### Builder Pattern etc.
|
||||||
|
|
||||||
|
// consider builder: https://docs.rs/derive_builder/latest/derive_builder/
|
||||||
|
// downside of builders as macro: no intellisense!
|
||||||
|
// each repository has to implement a repo info.
|
||||||
|
//#[derive(Builder)]
|
||||||
|
//#[builder(setter(into))]
|
||||||
|
|
||||||
|
#### static initializer
|
||||||
|
|
||||||
|
downside of a pub &'static str initializer struct is the rule to avoid any nested types that have any kind of building pattern, because you cannot properly move the data easily, especially if you want to move into enums, like an Option.
|
||||||
|
|
||||||
|
otherwise, self mutating building patterns can stay readonly either with the Copy trait and using self:
|
||||||
|
|
||||||
|
With "Copy":
|
||||||
|
fn self_mutating(mut self, value) -> Self {
|
||||||
|
self.value = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
Without Copy:
|
||||||
|
fn self_mutating_manual(self, value) -> Self {
|
||||||
|
Struct {
|
||||||
|
value: value,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
However also works, but seems dirty:
|
||||||
|
fn self_mutating_instance_mut(&mut self, value) -> Self {
|
||||||
|
self.value = value;
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#### Cow
|
||||||
|
|
||||||
|
Other remarks: also useful is the usage of Cow, if you want to create internal objects that actuall reuse static data or take dynamic data.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
pub struct Field {
|
||||||
|
pub widget: Cow<'static, str>,
|
||||||
|
pub label: Option<Cow<'static, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let static_field = Field::widget(Cow::Borrowed("/admin/widgets/input_text.jinja"));
|
||||||
|
let dynamic_label = String::from("Dynamic Label");
|
||||||
|
let dynamic_field = Field::widget(Cow::Owned(dynamic_label));
|
||||||
|
|
||||||
|
### Write article about "did you read the fineprint"
|
||||||
|
|
||||||
|
- The bleeding of lifetimes.
|
||||||
|
- 'static
|
||||||
|
- Send
|
||||||
|
- "object safe"
|
||||||
|
- the async dynamic dispatch.
|
||||||
|
- the building pattern debacle here
|
||||||
|
|
6
TODOS.md
6
TODOS.md
@ -33,3 +33,9 @@ auth:
|
|||||||
- auth middleware.
|
- auth middleware.
|
||||||
- message the user.
|
- message the user.
|
||||||
-> messaging system
|
-> messaging system
|
||||||
|
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -2,8 +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, LookupKey, RepoInfo, RepositoryInfo, RepositoryInfoBuilder, RepositoryItem,
|
AdminRepository, LookupKey, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem,
|
||||||
RepositoryList,
|
RepositoryList, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod auth {
|
mod auth {
|
||||||
@ -50,11 +50,126 @@ mod dto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod repository {
|
pub mod repository {
|
||||||
use super::dto::AdminModel;
|
use super::dto::{AdminApp, AdminModel};
|
||||||
|
use async_trait::async_trait;
|
||||||
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::{collections::HashMap, vec::IntoIter};
|
||||||
|
|
||||||
|
pub type RepositoryContext = AdminModel;
|
||||||
|
|
||||||
|
impl RepositoryContext {
|
||||||
|
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
||||||
|
RepositoryItem {
|
||||||
|
detail_url: Some(format!("{}/detail/{}", self.admin_url, key)),
|
||||||
|
fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a static configuration object.
|
||||||
|
/// It might be changed in the future to have a dynamic counterpart.
|
||||||
|
///
|
||||||
|
/// ## Example:
|
||||||
|
/// Creating a simple required and readonly text input field with a label:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let my_field = Field::widget("/admin/widgets/input_text.jinja")
|
||||||
|
/// .labelled("Username")
|
||||||
|
/// .required()
|
||||||
|
/// .readonly();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy)]
|
||||||
|
pub struct Widget {
|
||||||
|
pub widget: &'static str,
|
||||||
|
pub label: Option<&'static str>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub field_type: &'static str,
|
||||||
|
pub required: bool,
|
||||||
|
pub readonly: bool,
|
||||||
|
pub options: &'static [(&'static str, &'static str)],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget {
|
||||||
|
pub fn widget(widget: &'static str) -> Self {
|
||||||
|
Widget {
|
||||||
|
widget: widget,
|
||||||
|
label: None,
|
||||||
|
field_type: "text",
|
||||||
|
required: false,
|
||||||
|
readonly: false,
|
||||||
|
options: &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self::widget("/admin/widgets/input_text.jinja")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textarea() -> Self {
|
||||||
|
Self::widget("/admin/widgets/input_textarea.jinja")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required(mut self) -> Self {
|
||||||
|
self.required = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readonly(mut self) -> Self {
|
||||||
|
self.readonly = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn labeled(mut self, label: &'static str) -> Self {
|
||||||
|
self.label = Some(label);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_password(mut self) -> Self {
|
||||||
|
self.field_type = "password";
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_hidden(mut self) -> Self {
|
||||||
|
self.field_type = "hidden";
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self {
|
||||||
|
self.options = options;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Field {
|
||||||
|
widget: String,
|
||||||
|
label: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
field_type: String,
|
||||||
|
readonly: bool,
|
||||||
|
required: bool,
|
||||||
|
options: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Widget> for Field {
|
||||||
|
fn from(value: Widget) -> Self {
|
||||||
|
Field {
|
||||||
|
widget: value.widget.to_string(),
|
||||||
|
label: value.label.map(|s| s.to_string()),
|
||||||
|
field_type: value.field_type.to_string(),
|
||||||
|
readonly: value.readonly,
|
||||||
|
required: value.required,
|
||||||
|
options: value
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.map(|(key, val)| (key.to_string(), serde_json::json!(val)))
|
||||||
|
.collect::<serde_json::Map<String, Value>>()
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum LookupKey {
|
pub enum LookupKey {
|
||||||
@ -156,16 +271,18 @@ pub mod repository {
|
|||||||
pub fields: &'static [&'static str],
|
pub fields: &'static [&'static str],
|
||||||
}
|
}
|
||||||
|
|
||||||
// consider builder: https://docs.rs/derive_builder/latest/derive_builder/
|
impl RepoInfo {
|
||||||
// downside of builders as macro: no intellisense!
|
pub fn build(self) -> RepositoryInfo {
|
||||||
// each repository has to implement a repo info.
|
self.into()
|
||||||
#[derive(Serialize, Builder)]
|
}
|
||||||
#[builder(setter(into))]
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
pub struct RepositoryInfo {
|
pub struct RepositoryInfo {
|
||||||
name: String,
|
name: String,
|
||||||
lookup_key: String,
|
lookup_key: String,
|
||||||
display_list: Vec<String>,
|
display_list: Vec<String>,
|
||||||
fields: Vec<String>,
|
fields: Vec<(String, Field)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositoryInfo {
|
impl RepositoryInfo {
|
||||||
@ -178,12 +295,32 @@ pub mod repository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* // self mutating builder pattern?
|
// self mutating builder pattern
|
||||||
pub fn display_list(mut self, display_list: &[&str]) -> RepositoryInfo {
|
pub fn display_list(mut self, display_list: &[&str]) -> Self {
|
||||||
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
// set field for a given key.
|
||||||
|
pub fn set_widget(mut self, name: &str, widget: Widget) -> Self {
|
||||||
|
// Find the index of the existing entry with the same name, if it exists
|
||||||
|
let pos = self
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.position(|(existing_name, _)| existing_name == name);
|
||||||
|
|
||||||
|
match pos {
|
||||||
|
Some(index) => {
|
||||||
|
// If found, replace the `Field` part of the tuple
|
||||||
|
self.fields[index].1 = Field::from(widget);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// If not found, add a new entry
|
||||||
|
self.fields.push((name.to_owned(), Field::from(widget)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RepoInfo> for RepositoryInfo {
|
impl From<RepoInfo> for RepositoryInfo {
|
||||||
@ -196,22 +333,31 @@ pub mod repository {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|&s| s.to_string())
|
.map(|&s| s.to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
fields: repo_info.fields.iter().map(|&s| s.to_string()).collect(),
|
fields: repo_info
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|x| (x.to_string(), Field::from(Widget::default())))
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait AdminRepository: Send + Sync {
|
pub trait AdminRepository: Send + Sync {
|
||||||
fn info(&self, model: &AdminModel) -> RepositoryInfo;
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||||
fn list(&self, model: &AdminModel) -> RepositoryList;
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||||
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>;
|
async fn get(&self, context: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem>;
|
||||||
fn create(&mut self, model: &AdminModel, data: Value) -> Option<RepositoryItem>;
|
async fn create(
|
||||||
fn update(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
model: &AdminModel,
|
context: &RepositoryContext,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
id: LookupKey,
|
id: LookupKey,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> Option<RepositoryItem>;
|
) -> Option<RepositoryItem>;
|
||||||
fn delete(&mut self, model: &AdminModel, id: LookupKey) -> Option<Value>;
|
async fn delete(&mut self, context: &RepositoryContext, id: LookupKey) -> Option<Value>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,6 @@ impl Default for AdminContext {
|
|||||||
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
||||||
let hx_request = headers.get("HX-Request").is_some();
|
let hx_request = headers.get("HX-Request").is_some();
|
||||||
if hx_request {
|
if hx_request {
|
||||||
println!("HX.");
|
|
||||||
Some("admin/base_hx.jinja".to_string())
|
Some("admin/base_hx.jinja".to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -123,8 +122,8 @@ pub async fn list_item_collection(
|
|||||||
AdminContext {
|
AdminContext {
|
||||||
base: base_template(&headers),
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
item_info: Some(repo.info(&admin_model)),
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
item_list: repo.list(&admin_model),
|
item_list: repo.list(&admin_model).await,
|
||||||
item_model: Some(admin_model),
|
item_model: Some(admin_model),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -163,9 +162,9 @@ pub async fn item_details(
|
|||||||
AdminContext {
|
AdminContext {
|
||||||
base: base_template(&headers),
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
item_info: Some(repo.info(&admin_model)),
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
item_list: repo.list(&admin_model),
|
item_list: repo.list(&admin_model).await,
|
||||||
item: repo.get(&admin_model, key),
|
item: repo.get(&admin_model, key).await,
|
||||||
item_model: Some(admin_model),
|
item_model: Some(admin_model),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -193,8 +192,8 @@ pub async fn new_item(
|
|||||||
AdminContext {
|
AdminContext {
|
||||||
base: base_template(&headers),
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
item_info: Some(repo.info(&admin_model)),
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
item_list: repo.list(&admin_model),
|
item_list: repo.list(&admin_model).await,
|
||||||
item_model: Some(admin_model),
|
item_model: Some(admin_model),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -222,14 +221,14 @@ pub async fn create_item(
|
|||||||
.expect("Admin Model not found?");
|
.expect("Admin Model not found?");
|
||||||
|
|
||||||
// create our item.
|
// create our item.
|
||||||
let result = repo.create(&admin_model, form);
|
let result = repo.create(&admin_model, form).await;
|
||||||
|
|
||||||
// TODO: refactor run over these views, way too much repetition.
|
// TODO: refactor run over these views, way too much repetition.
|
||||||
AdminContext {
|
AdminContext {
|
||||||
base: base_template(&headers),
|
base: base_template(&headers),
|
||||||
available_apps: registry.get_apps(),
|
available_apps: registry.get_apps(),
|
||||||
item_info: Some(repo.info(&admin_model)),
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
item_list: repo.list(&admin_model),
|
item_list: repo.list(&admin_model).await,
|
||||||
item_model: Some(admin_model),
|
item_model: Some(admin_model),
|
||||||
item: result,
|
item: result,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
51
src/admin_examples/empty_repository.rs
Normal file
51
src/admin_examples/empty_repository.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use crate::admin::domain::*;
|
||||||
|
use crate::admin::state::AdminRegistry;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use log::{debug, warn};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
struct Repository {}
|
||||||
|
|
||||||
|
impl Repository {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AdminRepository for Repository {
|
||||||
|
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
|
||||||
|
RepoInfo {
|
||||||
|
name: "My Empty Repository",
|
||||||
|
lookup_key: "id",
|
||||||
|
display_list: &["id"],
|
||||||
|
fields: &[],
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||||
|
RepositoryList::Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
mut data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
id: LookupKey,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option<Value> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
|
pub mod empty_repository;
|
||||||
pub mod static_repository;
|
pub mod static_repository;
|
||||||
|
pub mod user_repository;
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
// implementation of static repository
|
|
||||||
|
|
||||||
use crate::admin::domain::*;
|
use crate::admin::domain::*;
|
||||||
use crate::admin::state::AdminRegistry;
|
use crate::admin::state::AdminRegistry;
|
||||||
|
use async_trait::async_trait;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
/// This is a showcase implementation with a static repository
|
||||||
|
/// It uses a Vec<Value> to store it's data.
|
||||||
struct MyStaticRepository {
|
struct MyStaticRepository {
|
||||||
/// In Memory Storage for Example.
|
/// In Memory Storage for Example.
|
||||||
content: Vec<Value>,
|
content: Vec<Value>,
|
||||||
@ -33,53 +34,47 @@ impl MyStaticRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl AdminRepository for MyStaticRepository {
|
impl AdminRepository for MyStaticRepository {
|
||||||
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem> {
|
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
|
||||||
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, model: &AdminModel) -> RepositoryList {
|
|
||||||
RepositoryList::List {
|
|
||||||
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, _model: &AdminModel) -> RepositoryInfo {
|
|
||||||
RepoInfo {
|
RepoInfo {
|
||||||
name: "My Static Repository",
|
name: "My Static Repository",
|
||||||
lookup_key: "id",
|
lookup_key: "id",
|
||||||
display_list: &["id", "name", "level", "age"],
|
display_list: &["id", "name", "level", "age"],
|
||||||
fields: &["name", "level", "age", "powers"],
|
fields: &["name", "level", "age", "powers"],
|
||||||
}
|
}
|
||||||
.into()
|
.build()
|
||||||
|
.set_widget("age", Widget::default().as_password().labeled("hi there."))
|
||||||
|
.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(&mut self, model: &AdminModel, mut data: Value) -> Option<RepositoryItem> {
|
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
|
||||||
|
if let LookupKey::Integer(id) = id {
|
||||||
|
let item = self.content.get(id - 1).cloned().unwrap();
|
||||||
|
let id = item.get("id").unwrap();
|
||||||
|
Some(model.build_item(&*id.to_string(), item))
|
||||||
|
} else {
|
||||||
|
warn!("Got non-integer lookup key: {}", id);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||||
|
RepositoryList::List {
|
||||||
|
values: self
|
||||||
|
.content
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| model.build_item(&*item.get("id").unwrap().to_string(), item))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
mut data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
debug!("Asked to create: {}", data);
|
debug!("Asked to create: {}", data);
|
||||||
|
|
||||||
let new_id = self.next_id;
|
let new_id = self.next_id;
|
||||||
@ -93,13 +88,15 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
self.content.push(data.clone());
|
self.content.push(data.clone());
|
||||||
|
|
||||||
// Return the newly created item
|
// Return the newly created item
|
||||||
Some(RepositoryItem {
|
Some(model.build_item(&*new_id.to_string(), data))
|
||||||
detail_url: Some(format!("{}/detail/{}", model.admin_url, new_id)),
|
|
||||||
fields: data,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem> {
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
id: LookupKey,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
debug!("I would now update: {}, {}", id, data);
|
debug!("I would now update: {}, {}", id, data);
|
||||||
|
|
||||||
// First, find the index of the item to update
|
// First, find the index of the item to update
|
||||||
@ -130,7 +127,7 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete(&mut self, _model: &AdminModel, id: LookupKey) -> Option<Value> {
|
async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option<Value> {
|
||||||
debug!("Would delete: {}", id);
|
debug!("Would delete: {}", id);
|
||||||
|
|
||||||
let item_index = self.content.iter().position(|item| {
|
let item_index = self.content.iter().position(|item| {
|
||||||
|
52
src/admin_examples/user_repository.rs
Normal file
52
src/admin_examples/user_repository.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use crate::admin::domain::AdminRepository;
|
||||||
|
use crate::admin::domain::*;
|
||||||
|
use crate::admin::state::AdminRegistry;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use log::{debug, warn};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
struct UserRepository {}
|
||||||
|
|
||||||
|
impl UserRepository {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AdminRepository for UserRepository {
|
||||||
|
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
|
||||||
|
RepoInfo {
|
||||||
|
name: "User",
|
||||||
|
lookup_key: "id",
|
||||||
|
display_list: &["id"],
|
||||||
|
fields: &[],
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||||
|
RepositoryList::Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
mut data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
id: LookupKey,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option<Value> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
@ -70,6 +70,12 @@
|
|||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
<a href="{{ url( 'admin:index') }}">{{ translate('Home') }}</a>
|
<a href="{{ url( 'admin:index') }}">{{ translate('Home') }}</a>
|
||||||
{% if title %} › {{ title }}{% endif %}
|
{% if title %} › {{ title }}{% endif %}
|
||||||
|
<div>
|
||||||
|
{% for x in y %}
|
||||||
|
{% for a in b %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -2,4 +2,9 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include "admin/dashboard.jinja" %}
|
{% include "admin/dashboard.jinja" %}
|
||||||
|
<div>
|
||||||
|
Some text
|
||||||
|
another text
|
||||||
|
Third text
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,26 +1,20 @@
|
|||||||
{% extends base|none("admin/base.jinja") %}
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
{% macro input(name, value="", type="text") -%}
|
|
||||||
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" placeholder="">
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{% 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">
|
<form action="{{item_model.add_url}}" method="POST" class="ui form">
|
||||||
|
<!--noformat-->
|
||||||
{% set fields = item_info.fields %}
|
{% set fields = item_info.fields %}
|
||||||
{% for field in fields %}
|
{% for field_name, field_defs in fields %}
|
||||||
{% if item %}
|
{% if item %}
|
||||||
{% set field_value = item.fields[field]|none("") %}
|
{% set field_value = item.fields[field_name]|none("") %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set field_value = "" %}
|
{% set field_value = "" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ field_widget(field_name, field_defs, field_value) }}
|
||||||
<div class="inline field">
|
|
||||||
<label>{{field|capitalize}}</label>
|
|
||||||
{{ input(field, field_value) }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button class="ui button" type="submit">Create</button>
|
<button class="ui button" type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if item %}
|
{% if item %}
|
||||||
{{item.fields}}
|
{{item.fields}}
|
||||||
{% else %}
|
{% else %}
|
||||||
No Item found.
|
No Item found.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
15
templates/admin/items/items.jinja
Normal file
15
templates/admin/items/items.jinja
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!--noformat-->
|
||||||
|
{% macro field_widget(name, definitions, value) -%}
|
||||||
|
{% set field = {
|
||||||
|
'type': definitions.type,
|
||||||
|
'name': name,
|
||||||
|
'value': value,
|
||||||
|
'placeholder': definitions.placeholder,
|
||||||
|
'label': definitions.label,
|
||||||
|
'readonly': definitions.readonly,
|
||||||
|
'required': definitions.required,
|
||||||
|
'options': definitions.options,
|
||||||
|
}
|
||||||
|
%}
|
||||||
|
{% include definitions.widget %}
|
||||||
|
{%- endmacro %}
|
4
templates/admin/widgets/input_text.jinja
Normal file
4
templates/admin/widgets/input_text.jinja
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="inline field">
|
||||||
|
<label>{{field.label or field.name|capitalize}}</label>
|
||||||
|
<input type="{{ field.type|none('text') }}" name="{{ field.name }}" value="{{ field.value }}" placeholder="{{field.placeholder}}">
|
||||||
|
</div>
|
12
templates/admin/widgets/input_textarea.jinja
Normal file
12
templates/admin/widgets/input_textarea.jinja
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<div class="inline field">
|
||||||
|
<label>{{ field.label or field.name|capitalize }}</label>
|
||||||
|
<!--noformat-->
|
||||||
|
<textarea name="{{ field.name }}" {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
|
||||||
|
{% for field_name in ['rows', 'cols', 'maxlength', 'minlength', 'wrap', 'autofocus'] %}
|
||||||
|
{% if field_name in field.options %}{{field_name}}="{{ field.options[field_name] }}" {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if field.required %}required{% endif %}
|
||||||
|
{% if field.readonly %}readonly{% endif %}
|
||||||
|
{% if "disabled" in field.options and field.options.disabled %}disabled{% endif %}
|
||||||
|
>{{ field.value }}</textarea>
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user