refactor: using word depot in rear instead of admin
This commit is contained in:
parent
a26e17a064
commit
966291dbd9
78
rear/src/depot/context.rs
Normal file
78
rear/src/depot/context.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::repository::{RepositoryInfo, RepositoryItem, RepositoryList};
|
||||||
|
|
||||||
|
// representation of a Model in the template.
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct DepotModel {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub model_url: String,
|
||||||
|
pub view_only: bool,
|
||||||
|
|
||||||
|
pub add_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// representation of a Section in the template.
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct DepotSection {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub section_url: String,
|
||||||
|
pub models: Vec<DepotModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct DepotRequest {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DepotContext {
|
||||||
|
pub base: Option<String>,
|
||||||
|
pub language_code: Option<String>,
|
||||||
|
pub language_bidi: Option<bool>,
|
||||||
|
pub user: Option<String>, // Todo: user type
|
||||||
|
pub depot_url: String,
|
||||||
|
pub site_url: Option<String>,
|
||||||
|
pub docsroot: Option<String>,
|
||||||
|
pub messages: Vec<String>, // Todo: message type
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub subtitle: Option<String>,
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
pub request: DepotRequest,
|
||||||
|
pub sections: Vec<DepotSection>,
|
||||||
|
pub item_model: Option<DepotModel>,
|
||||||
|
pub item_info: Option<RepositoryInfo>,
|
||||||
|
pub item_list: RepositoryList,
|
||||||
|
pub item: Option<RepositoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DepotContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
DepotContext {
|
||||||
|
base: None, // TODO: what is this used for?
|
||||||
|
language_code: Some("en-us".to_string()), // Default language code
|
||||||
|
language_bidi: Some(false), // Default language bidi
|
||||||
|
user: None, //UserType::default(), // Assuming UserType has a Default impl
|
||||||
|
depot_url: "/depot".to_owned(),
|
||||||
|
site_url: None,
|
||||||
|
docsroot: None,
|
||||||
|
messages: Vec::new(), // Empty vector for messages
|
||||||
|
title: None,
|
||||||
|
subtitle: None,
|
||||||
|
content: String::new(), // Empty string for content
|
||||||
|
sections: Vec::new(),
|
||||||
|
request: DepotRequest {
|
||||||
|
path: "".to_owned(),
|
||||||
|
},
|
||||||
|
item_model: None,
|
||||||
|
item_info: None,
|
||||||
|
item_list: RepositoryList::Empty,
|
||||||
|
item: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
rear/src/depot/mod.rs
Normal file
31
rear/src/depot/mod.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use axum::{routing::get, Router};
|
||||||
|
|
||||||
|
mod context;
|
||||||
|
pub mod prelude;
|
||||||
|
mod registry;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod state;
|
||||||
|
pub mod views;
|
||||||
|
pub mod widgets;
|
||||||
|
|
||||||
|
pub fn routes<S: state::DepotState + Clone + Send + Sync + 'static>() -> Router<S> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(views::index::<S>).post(views::index_action::<S>))
|
||||||
|
.route("/app/:app", get(views::list_app::<S>))
|
||||||
|
.route(
|
||||||
|
"/app/:app/model/:model",
|
||||||
|
get(views::list_item_collection::<S>),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/app/:app/model/:model/add",
|
||||||
|
get(views::new_item::<S>).post(views::create_item::<S>),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/app/:app/model/:model/change/:id",
|
||||||
|
get(views::change_item::<S>).patch(views::update_item::<S>),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/app/:app/model/:model/detail/:id",
|
||||||
|
get(views::view_item_details::<S>),
|
||||||
|
)
|
||||||
|
}
|
7
rear/src/depot/prelude.rs
Normal file
7
rear/src/depot/prelude.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub use super::context::{DepotContext, DepotModel};
|
||||||
|
pub use super::registry::DepotRegistry;
|
||||||
|
pub use super::repository::{
|
||||||
|
DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem,
|
||||||
|
RepositoryList,
|
||||||
|
};
|
||||||
|
pub use super::widgets::Widget;
|
164
rear/src/depot/registry.rs
Normal file
164
rear/src/depot/registry.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
context::{DepotModel, DepotSection},
|
||||||
|
repository::{DepotModelConfig, DepotRepository, DepotRepositoryWrapper, DynDepotRepository},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DepotRegistry {
|
||||||
|
base_path: String,
|
||||||
|
sections: HashMap<String, internal::DepotSectionInfo>,
|
||||||
|
models: HashMap<String, internal::DepotModelInfo>,
|
||||||
|
repositories: HashMap<String, Arc<Mutex<dyn DynDepotRepository>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DepotRegistry {
|
||||||
|
pub fn new(base_path: &str) -> Self {
|
||||||
|
DepotRegistry {
|
||||||
|
base_path: base_path.to_owned(),
|
||||||
|
sections: HashMap::new(),
|
||||||
|
models: HashMap::new(),
|
||||||
|
repositories: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sections(&self) -> Vec<DepotSection> {
|
||||||
|
self.sections
|
||||||
|
.iter()
|
||||||
|
.map(|(key, section_info)| self.get_section(key, section_info))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_section(&self, key: &str, section_info: &internal::DepotSectionInfo) -> DepotSection {
|
||||||
|
let my_models = self.get_models(key);
|
||||||
|
DepotSection {
|
||||||
|
key: key.to_owned(),
|
||||||
|
name: section_info.name.to_owned(),
|
||||||
|
section_url: format!("/{}/section/{}", self.base_path, key.to_owned()),
|
||||||
|
models: my_models,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_section(&mut self, name: &str) -> String {
|
||||||
|
let key = self.get_key(name);
|
||||||
|
self.sections.insert(
|
||||||
|
key.to_owned(),
|
||||||
|
internal::DepotSectionInfo {
|
||||||
|
key: key.to_owned(),
|
||||||
|
name: name.to_owned(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key(&self, name: &str) -> String {
|
||||||
|
slug::slugify(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel {
|
||||||
|
let model_url = format!(
|
||||||
|
"/{}/section/{}/model/{}",
|
||||||
|
self.base_path, model_info.section_key, model_info.model_key
|
||||||
|
);
|
||||||
|
DepotModel {
|
||||||
|
key: model_info.model_key.clone(),
|
||||||
|
name: model_info.name.clone(),
|
||||||
|
view_only: false,
|
||||||
|
add_url: Some(format!("{}/add", model_url)),
|
||||||
|
model_url: model_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_models(&self, section_key: &str) -> Vec<DepotModel> {
|
||||||
|
self.models
|
||||||
|
.iter()
|
||||||
|
.filter(|(key, _)| key.starts_with(&format!("{}.", section_key)))
|
||||||
|
.map(|(_, model)| self.model_from_model_info(model))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_model(&self, section_key: &str, model_key: &str) -> Option<DepotModel> {
|
||||||
|
let full_model_key = format!("{}.{}", section_key, model_key);
|
||||||
|
let internal_model = self.models.get(&full_model_key)?;
|
||||||
|
Some(self.model_from_model_info(internal_model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_model_config(&mut self, model: DepotModelConfig) -> Result<String, String> {
|
||||||
|
let local_config = internal::DepotModelInfo::from(model);
|
||||||
|
if local_config.model_key.is_empty() {
|
||||||
|
return Err("No model name".to_owned());
|
||||||
|
}
|
||||||
|
let local_config_name = format!("{}.{}", local_config.section_key, local_config.model_key);
|
||||||
|
if self.models.contains_key(&local_config_name) {
|
||||||
|
return Err(format!("Model {} already exists", local_config_name));
|
||||||
|
}
|
||||||
|
let full_model_key = local_config_name.clone();
|
||||||
|
self.models.insert(local_config_name, local_config);
|
||||||
|
Ok(full_model_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_model<R: DepotRepository + 'static>(
|
||||||
|
&mut self,
|
||||||
|
model: DepotModelConfig,
|
||||||
|
repository: R,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let model_key = self.register_model_config(model)?;
|
||||||
|
let repository = DepotRepositoryWrapper::new(repository);
|
||||||
|
self.repositories
|
||||||
|
.insert(model_key, Arc::new(Mutex::new(repository)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_repository(
|
||||||
|
&self,
|
||||||
|
section_key: &str,
|
||||||
|
model_key: &str,
|
||||||
|
) -> Result<Arc<Mutex<dyn DynDepotRepository>>, String> {
|
||||||
|
let full_model_key = format!("{}.{}", section_key, model_key);
|
||||||
|
if let Some(repo) = self.repositories.get(&full_model_key) {
|
||||||
|
// Clone the Arc to return a reference to the repository
|
||||||
|
return Ok(Arc::clone(repo));
|
||||||
|
} else {
|
||||||
|
return Err("Couldn't find repository".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod internal {
|
||||||
|
// how the registry saves data internally.
|
||||||
|
use super::super::repository::DepotModelConfig;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct DepotSectionInfo {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct DepotModelInfo {
|
||||||
|
pub section_key: String,
|
||||||
|
pub model_key: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DepotModelConfig> for DepotModelInfo {
|
||||||
|
fn from(value: DepotModelConfig) -> Self {
|
||||||
|
DepotModelInfo {
|
||||||
|
section_key: value.section_key,
|
||||||
|
model_key: slug::slugify(value.name.clone()),
|
||||||
|
name: value.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&str, &str)> for DepotModelInfo {
|
||||||
|
fn from(value: (&str, &str)) -> Self {
|
||||||
|
DepotModelInfo {
|
||||||
|
section_key: value.0.to_owned(),
|
||||||
|
model_key: slug::slugify(value.1.to_owned()),
|
||||||
|
name: value.1.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
361
rear/src/depot/repository.rs
Normal file
361
rear/src/depot/repository.rs
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::any::Any;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::vec::IntoIter;
|
||||||
|
|
||||||
|
use super::context::DepotModel;
|
||||||
|
use super::widgets::Widget;
|
||||||
|
|
||||||
|
// user uses this configuration object to register another model.
|
||||||
|
pub struct DepotModelConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub section_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&str, &str)> for DepotModelConfig {
|
||||||
|
fn from(value: (&str, &str)) -> Self {
|
||||||
|
DepotModelConfig {
|
||||||
|
section_key: value.0.to_owned(),
|
||||||
|
name: value.1.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// May become it's own structure.
|
||||||
|
pub type RepositoryContext = DepotModel;
|
||||||
|
|
||||||
|
impl RepositoryContext {
|
||||||
|
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/detail/{}", self.model_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/change/{}", self.model_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
||||||
|
RepositoryItem {
|
||||||
|
detail_url: self.get_default_detail_url(key),
|
||||||
|
change_url: self.get_default_change_url(key),
|
||||||
|
fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub 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(Serialize)]
|
||||||
|
pub struct RepositoryItem {
|
||||||
|
pub fields: Value,
|
||||||
|
pub detail_url: Option<String>,
|
||||||
|
pub change_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RepositoryList {
|
||||||
|
Empty,
|
||||||
|
List {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
},
|
||||||
|
Page {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
offset: usize,
|
||||||
|
total: usize,
|
||||||
|
},
|
||||||
|
Stream {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
next_index: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for RepositoryList {
|
||||||
|
type Item = RepositoryItem;
|
||||||
|
type IntoIter = IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
match self {
|
||||||
|
RepositoryList::Empty => vec![].into_iter(),
|
||||||
|
RepositoryList::List { values } => values.into_iter(),
|
||||||
|
RepositoryList::Page { values, .. } => values.into_iter(),
|
||||||
|
RepositoryList::Stream { values, .. } => values.into_iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for RepositoryList {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
RepositoryList::Empty => serializer.serialize_unit(),
|
||||||
|
RepositoryList::List { values }
|
||||||
|
| RepositoryList::Page { values, .. }
|
||||||
|
| RepositoryList::Stream { values, .. } => values.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static initializer for RepositoryInfo.
|
||||||
|
pub struct RepoInfo {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub lookup_key: &'static str,
|
||||||
|
pub display_list: &'static [&'static str],
|
||||||
|
pub fields: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoInfo {
|
||||||
|
pub fn build(self) -> RepositoryInfo {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RepositoryInfo {
|
||||||
|
name: String,
|
||||||
|
lookup_key: String,
|
||||||
|
display_list: Vec<String>,
|
||||||
|
fields: Vec<(String, Field)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepositoryInfo {
|
||||||
|
pub fn new(name: &str, lookup_key: &str) -> Self {
|
||||||
|
RepositoryInfo {
|
||||||
|
name: name.to_owned(),
|
||||||
|
lookup_key: lookup_key.to_owned(),
|
||||||
|
display_list: vec![],
|
||||||
|
fields: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// self mutating builder pattern
|
||||||
|
pub fn display_list(mut self, display_list: &[&str]) -> Self {
|
||||||
|
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_widget<T: Into<Field>>(mut self, name: &str, item: T) -> Self {
|
||||||
|
let field = item.into(); // Convert the input into a Field
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
self.fields[index].1 = field;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.fields.push((name.to_owned(), field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RepoInfo> for RepositoryInfo {
|
||||||
|
fn from(repo_info: RepoInfo) -> Self {
|
||||||
|
RepositoryInfo {
|
||||||
|
name: repo_info.name.to_string(),
|
||||||
|
lookup_key: repo_info.lookup_key.to_string(),
|
||||||
|
display_list: repo_info
|
||||||
|
.display_list
|
||||||
|
.iter()
|
||||||
|
.map(|&s| s.to_string())
|
||||||
|
.collect(),
|
||||||
|
fields: repo_info
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|x| (x.to_string(), Field::from(Widget::default())))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PrimaryKeyType: Any + Debug + Send + Sync {
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryKeyType for i64 {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryKeyType for String {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait DepotRepository: Send + Sync {
|
||||||
|
type Key: PrimaryKeyType;
|
||||||
|
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Self::Key>;
|
||||||
|
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||||
|
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem>;
|
||||||
|
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem>;
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub(crate) trait DynDepotRepository: Send + Sync {
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem>;
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn delete(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DepotRepositoryWrapper<T: DepotRepository> {
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DepotRepository> DepotRepositoryWrapper<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_from_string(&self, s: String) -> Option<<T as DepotRepository>::Key> {
|
||||||
|
self.inner.key_from_string(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: DepotRepository> DynDepotRepository for DepotRepositoryWrapper<T> {
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>> {
|
||||||
|
if let Some(key) = self.inner.key_from_string(s) {
|
||||||
|
Some(Box::new(key))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo {
|
||||||
|
self.inner.info(context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList {
|
||||||
|
self.inner.list(context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.get(context, key).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem> {
|
||||||
|
self.inner.create(context, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.update(context, key, data).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.replace(context, key, data).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<Value> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.delete(context, key).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
rear/src/depot/state.rs
Normal file
11
rear/src/depot/state.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use crate::service::templates::Templates;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::registry::DepotRegistry;
|
||||||
|
|
||||||
|
pub trait DepotState {
|
||||||
|
fn get_templates(&self) -> &Templates;
|
||||||
|
fn get_registry(&self) -> SharedDepotRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedDepotRegistry = Arc<DepotRegistry>;
|
294
rear/src/depot/views.rs
Normal file
294
rear/src/depot/views.rs
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
use axum::extract::Path;
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::Form;
|
||||||
|
use axum::{extract::State, response::IntoResponse};
|
||||||
|
use log::info;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::context::DepotContext;
|
||||||
|
use super::state::DepotState;
|
||||||
|
|
||||||
|
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
||||||
|
let hx_request = headers.get("HX-Request").is_some();
|
||||||
|
if hx_request {
|
||||||
|
Some("depot/base_hx.jinja".to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
templates.render_html(
|
||||||
|
"depot/index.html",
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index Action is POST to the index site. We can anchor some general business code here.
|
||||||
|
pub async fn index_action<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_app<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path(depot_key): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
templates.render_html("depot/depot.jinja", ())
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Items renders the entire list item page.
|
||||||
|
pub async fn list_item_collection<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
info!("list_item_collection {} for model {}", depot_key, model_key);
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects.
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_list.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Details shows one single dataset.
|
||||||
|
pub async fn view_item_details<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item: repo.get(&depot_model, key.as_ref()).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_detail.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_item<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?");
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_create.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_item<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key)): Path<(String, String)>,
|
||||||
|
Form(form): Form<Value>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let mut repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("Depot Model not found?");
|
||||||
|
|
||||||
|
// create our item.
|
||||||
|
let result = repo.create(&depot_model, form).await;
|
||||||
|
|
||||||
|
// TODO: refactor run over these views, way too much repetition.
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
item: result,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_create.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change is the GET version.
|
||||||
|
pub async fn change_item<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item: repo.get(&depot_model, key.as_ref()).await,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("depot/items/item_change.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_item<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
Form(form): Form<Value>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = depot.get_templates();
|
||||||
|
let registry = depot.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
||||||
|
let mut repo = repo.lock().await;
|
||||||
|
let depot_model = registry
|
||||||
|
.get_model(&depot_key, &model_key)
|
||||||
|
.expect("depot Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
let result = repo.update(&depot_model, key.as_ref(), form).await;
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
item_info: Some(repo.info(&depot_model).await),
|
||||||
|
item_list: repo.list(&depot_model).await,
|
||||||
|
item: result,
|
||||||
|
item_model: Some(depot_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DepotContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
sections: registry.get_sections(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = templates.render_html("depot/items/item_change.jinja", context);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Action allows running an action on one single dataset.
|
||||||
|
pub async fn item_action<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path((depot_key, model_key, model_id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn debug_view<S: DepotState + Clone + Send + Sync + 'static>(
|
||||||
|
depot: State<S>,
|
||||||
|
Path(data): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
println!("debug: {}", data);
|
||||||
|
"Debug!".to_owned()
|
||||||
|
}
|
79
rear/src/depot/widgets.rs
Normal file
79
rear/src/depot/widgets.rs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// 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 checkbox() -> Self {
|
||||||
|
Self::widget("/admin/widgets/checkbox_toggle.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
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,2 @@
|
|||||||
pub mod admin;
|
pub mod depot;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
@ -3,7 +3,23 @@ use axum::response::Html;
|
|||||||
use minijinja::{path_loader, Environment, Value};
|
use minijinja::{path_loader, Environment, Value};
|
||||||
use minijinja_autoreload::AutoReloader;
|
use minijinja_autoreload::AutoReloader;
|
||||||
use pulldown_cmark::Event;
|
use pulldown_cmark::Event;
|
||||||
use std::sync::Arc;
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
pub fn composite_loader<P: AsRef<Path>>(
|
||||||
|
dirs: Vec<P>,
|
||||||
|
) -> impl Fn(&str) -> Result<Option<String>, minijinja::Error> + Send + Sync + 'static {
|
||||||
|
let loaders: Vec<_> = dirs.into_iter().map(|dir| path_loader(dir)).collect();
|
||||||
|
move |name| {
|
||||||
|
for loader in &loaders {
|
||||||
|
match loader(name) {
|
||||||
|
Ok(Some(template)) => return Ok(Some(template)),
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Templates {
|
pub struct Templates {
|
||||||
@ -15,7 +31,8 @@ impl Templates {
|
|||||||
let reloader = AutoReloader::new(move |notifier| {
|
let reloader = AutoReloader::new(move |notifier| {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
let template_path = "templates";
|
let template_path = "templates";
|
||||||
environment.set_loader(path_loader(&template_path));
|
let loader = composite_loader(vec![template_path]);
|
||||||
|
environment.set_loader(loader);
|
||||||
environment.add_filter("none", none);
|
environment.add_filter("none", none);
|
||||||
environment.add_filter("markdown", markdown);
|
environment.add_filter("markdown", markdown);
|
||||||
environment.add_filter("yesno", filter_yesno);
|
environment.add_filter("yesno", filter_yesno);
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use rear::admin::domain::*;
|
use rear::depot::prelude::*;
|
||||||
use rear::admin::state::AdminRegistry;
|
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::models::UserRepository;
|
use crate::models::UserRepository;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for UserRepository {
|
impl DepotRepository for UserRepository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@ -202,11 +201,11 @@ impl AdminRepository for UserRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut AdminRegistry, db: DatabaseConnection) -> UserRepository {
|
pub fn register(registry: &mut DepotRegistry, db: DatabaseConnection) -> UserRepository {
|
||||||
let app_key = registry.register_app("Auth");
|
let section_key = registry.register_section("Auth");
|
||||||
let repo = UserRepository::new(db);
|
let repo = UserRepository::new(db);
|
||||||
let model_config = AdminModelConfig {
|
let model_config = DepotModelConfig {
|
||||||
app_key: app_key,
|
section_key: section_key,
|
||||||
name: "User".to_owned(),
|
name: "User".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo.clone());
|
let model_result = registry.register_model(model_config, repo.clone());
|
||||||
|
@ -23,7 +23,7 @@ pub struct NextUrl {
|
|||||||
next: Option<String>,
|
next: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes<S: rear::admin::state::AdminState + Clone + Send + Sync + 'static>() -> Router<S>
|
pub fn routes<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>() -> Router<S>
|
||||||
where {
|
where {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/login", post(self::post::login))
|
.route("/login", post(self::post::login))
|
||||||
@ -44,7 +44,7 @@ mod post {
|
|||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
messages.error("Invalid credentials");
|
messages.error("Invalid credentials");
|
||||||
|
|
||||||
let mut login_url = "/admin/login".to_string();
|
let mut login_url = "/depot/login".to_string();
|
||||||
if let Some(next) = creds.next {
|
if let Some(next) = creds.next {
|
||||||
login_url = format!("{}?next={}", login_url, next);
|
login_url = format!("{}?next={}", login_url, next);
|
||||||
};
|
};
|
||||||
@ -70,7 +70,7 @@ mod post {
|
|||||||
|
|
||||||
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
||||||
match auth_session.logout().await {
|
match auth_session.logout().await {
|
||||||
Ok(_) => Redirect::to("/admin/login").into_response(),
|
Ok(_) => Redirect::to("/depot/login").into_response(),
|
||||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ mod get {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
|
|
||||||
pub async fn login<S: rear::admin::state::AdminState + Clone + Send + Sync + 'static>(
|
pub async fn login<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>(
|
||||||
messages: Messages,
|
messages: Messages,
|
||||||
admin: State<S>,
|
admin: State<S>,
|
||||||
Query(NextUrl { next }): Query<NextUrl>,
|
Query(NextUrl { next }): Query<NextUrl>,
|
||||||
@ -90,7 +90,7 @@ mod get {
|
|||||||
messages: messages.into_iter().collect(),
|
messages: messages.into_iter().collect(),
|
||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
templates.render_html("admin/login.html", context)
|
templates.render_html("depot/login.html", context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::admin::domain::*;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use rear::depot::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
struct Repository {}
|
struct Repository {}
|
||||||
@ -7,7 +7,7 @@ struct Repository {}
|
|||||||
impl Repository {}
|
impl Repository {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for Repository {
|
impl DepotRepository for Repository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use crate::admin::domain::*;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rear::admin::state::AdminRegistry;
|
use rear::depot::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@ -18,7 +17,7 @@ impl FileRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for FileRepository {
|
impl DepotRepository for FileRepository {
|
||||||
type Key = String;
|
type Key = String;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@ -101,11 +100,11 @@ impl AdminRepository for FileRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut AdminRegistry, path: &str) {
|
pub fn register(registry: &mut DepotRegistry, path: &str) {
|
||||||
let app_key = registry.register_app("Files");
|
let section_key = registry.register_section("Files");
|
||||||
let repo = FileRepository::new(path);
|
let repo = FileRepository::new(path);
|
||||||
let model_config = AdminModelConfig {
|
let model_config = DepotModelConfig {
|
||||||
app_key: app_key,
|
section_key: section_key,
|
||||||
name: "Files".to_owned(),
|
name: "Files".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo);
|
let model_result = registry.register_model(model_config, repo);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use crate::admin::domain::*;
|
use crate::depot::prelude::*;
|
||||||
use crate::admin::state::AdminRegistry;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::{debug, warn};
|
use log::debug;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
/// This is a showcase implementation with a static repository
|
/// This is a showcase implementation with a static repository
|
||||||
@ -35,7 +34,7 @@ impl MyStaticRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for MyStaticRepository {
|
impl DepotRepository for MyStaticRepository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@ -156,11 +155,11 @@ impl AdminRepository for MyStaticRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut AdminRegistry) {
|
pub fn register(registry: &mut DepotRegistry) {
|
||||||
let app_key = registry.register_app("Example App");
|
let section_key = registry.register_section("Example App");
|
||||||
let repo = MyStaticRepository::new();
|
let repo = MyStaticRepository::new();
|
||||||
let model_config = AdminModelConfig {
|
let model_config = DepotModelConfig {
|
||||||
app_key: app_key,
|
section_key: section_key,
|
||||||
name: "ExampleModel".to_owned(),
|
name: "ExampleModel".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo);
|
let model_result = registry.register_model(model_config, repo);
|
||||||
|
@ -23,7 +23,7 @@ use axum_login::AuthManagerLayerBuilder;
|
|||||||
use axum_messages::MessagesManagerLayer;
|
use axum_messages::MessagesManagerLayer;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rear::admin;
|
use rear::depot;
|
||||||
use rear::service::{handlers, templates};
|
use rear::service::{handlers, templates};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@ -53,7 +53,7 @@ async fn main() {
|
|||||||
|
|
||||||
// Prepare Application State Members
|
// Prepare Application State Members
|
||||||
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
||||||
let mut admin = admin::state::AdminRegistry::new("admin");
|
let mut admin = depot::prelude::DepotRegistry::new("admin");
|
||||||
|
|
||||||
// Register Admin Apps
|
// Register Admin Apps
|
||||||
static_repository::register(&mut admin);
|
static_repository::register(&mut admin);
|
||||||
@ -79,7 +79,7 @@ async fn main() {
|
|||||||
//.merge(admin_router)
|
//.merge(admin_router)
|
||||||
.nest(
|
.nest(
|
||||||
"/admin",
|
"/admin",
|
||||||
admin::routes()
|
depot::routes()
|
||||||
.route_layer(login_required!(
|
.route_layer(login_required!(
|
||||||
rear_auth::models::UserRepository,
|
rear_auth::models::UserRepository,
|
||||||
login_url = "/admin/login"
|
login_url = "/admin/login"
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
|
|
||||||
use rear::admin::state::{AdminState, SharedAdminRegistry};
|
use rear::depot::state::{DepotState, SharedDepotRegistry};
|
||||||
use rear::service::templates;
|
use rear::service::templates;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub templates: templates::Templates,
|
pub templates: templates::Templates,
|
||||||
pub admin: SharedAdminRegistry,
|
pub admin: SharedDepotRegistry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRef<AppState> for templates::Templates {
|
impl FromRef<AppState> for templates::Templates {
|
||||||
@ -15,12 +15,12 @@ impl FromRef<AppState> for templates::Templates {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminState for AppState {
|
impl DepotState for AppState {
|
||||||
fn get_templates(&self) -> &templates::Templates {
|
fn get_templates(&self) -> &templates::Templates {
|
||||||
&self.templates
|
&self.templates
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_registry(&self) -> SharedAdminRegistry {
|
fn get_registry(&self) -> SharedDepotRegistry {
|
||||||
self.admin.clone()
|
self.admin.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@ -1,10 +0,0 @@
|
|||||||
{% extends "admin/base.jinja" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% include "admin/dashboard.jinja" %}
|
|
||||||
<div>
|
|
||||||
Some text
|
|
||||||
another text
|
|
||||||
Third text
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% include "fomantic.html" %}
|
{% include "fomantic.html" %}
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/admin/admin.css">
|
<link rel="stylesheet" type="text/css" href="/static/rear/depot.css">
|
||||||
|
|
||||||
{% block extrastyle %}{% endblock %}
|
{% block extrastyle %}{% endblock %}
|
||||||
{% block extrahead %}{% endblock %}
|
{% block extrahead %}{% endblock %}
|
||||||
@ -42,14 +42,14 @@
|
|||||||
{% endblock usertools %}
|
{% endblock usertools %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a class="ui blue image label">
|
<a class="ui blue image label">
|
||||||
<img src="/static/admin/teddy_bear.png">
|
<img src="/static/rear/teddy_bear.png">
|
||||||
Person
|
Person
|
||||||
<div class="detail">Admin</div>
|
<div class="detail">Admiral</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="icon item">
|
<div class="icon item">
|
||||||
<form id="logout-form" method="post" action="{{ url('admin:logout') }}">
|
<form id="logout-form" method="post" action="{{ url('depot:logout') }}">
|
||||||
<i class="power link icon" style="float: none;"></i>
|
<i class="power link icon" style="float: none;"></i>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -62,7 +62,7 @@
|
|||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
|
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
|
||||||
<div class="ui vertical inverted fluid menu">
|
<div class="ui vertical inverted fluid menu">
|
||||||
<div class="item"><a href="{{admin_url}}">
|
<div class="item"><a href="{{depot_url}}">
|
||||||
<i class="big d20 dice icon" style="float: none;"></i>
|
<i class="big d20 dice icon" style="float: none;"></i>
|
||||||
<b>Administration</b>
|
<b>Administration</b>
|
||||||
</a>
|
</a>
|
||||||
@ -97,17 +97,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if available_apps %}
|
{% if sections %}
|
||||||
{% for app in available_apps %}
|
{% for section in sections %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="cog link icon" href="{{ app.admin_url }}"></i>
|
<i class="cog link icon" href="{{ section.section_url }}"></i>
|
||||||
<div class="header">{{ app.name }} </div>
|
<div class="header">{{ section.name }} </div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{% for model in app.models %}
|
{% for model in section.models %}
|
||||||
<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.model_url %}
|
||||||
<a href="{{ model.admin_url }}" hx-get="{{ model.admin_url }}" hx-target="#main" hx-push-url="true" {% if
|
<a href="{{ model.model_url }}" hx-get="{{ model.model_url }}" hx-target="#main" hx-push-url="true" {% if
|
||||||
model.admin_url in request.path|urlencode %} aria-current="page" {% 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>
|
@ -1,15 +1,16 @@
|
|||||||
{% if app_list %}
|
{% if sections %}
|
||||||
{% for app in app_list %}
|
{% for section in sections %}
|
||||||
<div
|
<div
|
||||||
class="app-{{ app.key }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %} ui short scrolling container">
|
class="section-{{ section.key }} module{% if section.section_url in request.path|urlencode %} current-section{% endif %} ui short scrolling container">
|
||||||
<table class="ui very compact celled table head stuck unstackable">
|
<table class="ui very compact celled table head stuck unstackable">
|
||||||
<caption>
|
<caption>
|
||||||
<a href="{{ app.app_url }}" class="section" title="Models in the {{ name }} application">{{ app.name }}</a>
|
<a href="{{ section.section_url }}" class="section"
|
||||||
|
title="Models in the {{ name }} Section">{{ section.name }}</a>
|
||||||
</caption>
|
</caption>
|
||||||
{% for model in app.models %}
|
{% for model in section.models %}
|
||||||
<tr class="model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
<tr class="model-{{ model.key }}{% if model.model_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
{% if model.admin_url %}
|
{% if model.model_url %}
|
||||||
<th scope="row"><a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}
|
<th scope="row"><a href="{{ model.model_url }}" {% if model.model_url in request.path|urlencode %}
|
||||||
aria-current="page" {% endif %}>{{ model.name }}</a></th>
|
aria-current="page" {% endif %}>{{ model.name }}</a></th>
|
||||||
{% else %}
|
{% else %}
|
||||||
<th scope="row">{{ model.name }}</th>
|
<th scope="row">{{ model.name }}</th>
|
||||||
@ -21,11 +22,11 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if model.admin_url and show_changelinks %}
|
{% if model.model_url and show_changelinks %}
|
||||||
{% if model.view_only %}
|
{% if model.view_only %}
|
||||||
<td><a href="{{ model.admin_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
|
<td><a href="{{ model.model_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><a href="{{ model.admin_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
|
<td><a href="{{ model.model_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif show_changelinks %}
|
{% elif show_changelinks %}
|
||||||
<td></td>
|
<td></td>
|
10
templates/depot/index.html
Normal file
10
templates/depot/index.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "depot/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "depot/dashboard.jinja" %}
|
||||||
|
<div>
|
||||||
|
Some text
|
||||||
|
another text
|
||||||
|
Third text
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user