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;
|
||||
|
@ -3,7 +3,23 @@ use axum::response::Html;
|
||||
use minijinja::{path_loader, Environment, Value};
|
||||
use minijinja_autoreload::AutoReloader;
|
||||
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)]
|
||||
pub struct Templates {
|
||||
@ -15,7 +31,8 @@ impl Templates {
|
||||
let reloader = AutoReloader::new(move |notifier| {
|
||||
let mut environment = Environment::new();
|
||||
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("markdown", markdown);
|
||||
environment.add_filter("yesno", filter_yesno);
|
||||
|
@ -1,14 +1,13 @@
|
||||
use async_trait::async_trait;
|
||||
use log::debug;
|
||||
use rear::admin::domain::*;
|
||||
use rear::admin::state::AdminRegistry;
|
||||
use rear::depot::prelude::*;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::models::UserRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl AdminRepository for UserRepository {
|
||||
impl DepotRepository for UserRepository {
|
||||
type Key = i64;
|
||||
|
||||
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 {
|
||||
let app_key = registry.register_app("Auth");
|
||||
pub fn register(registry: &mut DepotRegistry, db: DatabaseConnection) -> UserRepository {
|
||||
let section_key = registry.register_section("Auth");
|
||||
let repo = UserRepository::new(db);
|
||||
let model_config = AdminModelConfig {
|
||||
app_key: app_key,
|
||||
let model_config = DepotModelConfig {
|
||||
section_key: section_key,
|
||||
name: "User".to_owned(),
|
||||
};
|
||||
let model_result = registry.register_model(model_config, repo.clone());
|
||||
|
@ -23,7 +23,7 @@ pub struct NextUrl {
|
||||
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 {
|
||||
Router::new()
|
||||
.route("/login", post(self::post::login))
|
||||
@ -44,7 +44,7 @@ mod post {
|
||||
Ok(None) => {
|
||||
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 {
|
||||
login_url = format!("{}?next={}", login_url, next);
|
||||
};
|
||||
@ -70,7 +70,7 @@ mod post {
|
||||
|
||||
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
@ -80,7 +80,7 @@ mod get {
|
||||
use super::*;
|
||||
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,
|
||||
admin: State<S>,
|
||||
Query(NextUrl { next }): Query<NextUrl>,
|
||||
@ -90,7 +90,7 @@ mod get {
|
||||
messages: messages.into_iter().collect(),
|
||||
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 rear::depot::prelude::*;
|
||||
use serde_json::Value;
|
||||
|
||||
struct Repository {}
|
||||
@ -7,7 +7,7 @@ struct Repository {}
|
||||
impl Repository {}
|
||||
|
||||
#[async_trait]
|
||||
impl AdminRepository for Repository {
|
||||
impl DepotRepository for Repository {
|
||||
type Key = i64;
|
||||
|
||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::admin::domain::*;
|
||||
use async_trait::async_trait;
|
||||
use rear::admin::state::AdminRegistry;
|
||||
use rear::depot::prelude::*;
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@ -18,7 +17,7 @@ impl FileRepository {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AdminRepository for FileRepository {
|
||||
impl DepotRepository for FileRepository {
|
||||
type Key = String;
|
||||
|
||||
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) {
|
||||
let app_key = registry.register_app("Files");
|
||||
pub fn register(registry: &mut DepotRegistry, path: &str) {
|
||||
let section_key = registry.register_section("Files");
|
||||
let repo = FileRepository::new(path);
|
||||
let model_config = AdminModelConfig {
|
||||
app_key: app_key,
|
||||
let model_config = DepotModelConfig {
|
||||
section_key: section_key,
|
||||
name: "Files".to_owned(),
|
||||
};
|
||||
let model_result = registry.register_model(model_config, repo);
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::admin::domain::*;
|
||||
use crate::admin::state::AdminRegistry;
|
||||
use crate::depot::prelude::*;
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, warn};
|
||||
use log::debug;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// This is a showcase implementation with a static repository
|
||||
@ -35,7 +34,7 @@ impl MyStaticRepository {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AdminRepository for MyStaticRepository {
|
||||
impl DepotRepository for MyStaticRepository {
|
||||
type Key = i64;
|
||||
|
||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||
@ -156,11 +155,11 @@ impl AdminRepository for MyStaticRepository {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(registry: &mut AdminRegistry) {
|
||||
let app_key = registry.register_app("Example App");
|
||||
pub fn register(registry: &mut DepotRegistry) {
|
||||
let section_key = registry.register_section("Example App");
|
||||
let repo = MyStaticRepository::new();
|
||||
let model_config = AdminModelConfig {
|
||||
app_key: app_key,
|
||||
let model_config = DepotModelConfig {
|
||||
section_key: section_key,
|
||||
name: "ExampleModel".to_owned(),
|
||||
};
|
||||
let model_result = registry.register_model(model_config, repo);
|
||||
|
@ -23,7 +23,7 @@ use axum_login::AuthManagerLayerBuilder;
|
||||
use axum_messages::MessagesManagerLayer;
|
||||
use dotenvy::dotenv;
|
||||
use log::info;
|
||||
use rear::admin;
|
||||
use rear::depot;
|
||||
use rear::service::{handlers, templates};
|
||||
use std::env;
|
||||
use std::net::SocketAddr;
|
||||
@ -53,7 +53,7 @@ async fn main() {
|
||||
|
||||
// Prepare Application State Members
|
||||
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
|
||||
static_repository::register(&mut admin);
|
||||
@ -79,7 +79,7 @@ async fn main() {
|
||||
//.merge(admin_router)
|
||||
.nest(
|
||||
"/admin",
|
||||
admin::routes()
|
||||
depot::routes()
|
||||
.route_layer(login_required!(
|
||||
rear_auth::models::UserRepository,
|
||||
login_url = "/admin/login"
|
||||
|
@ -1,12 +1,12 @@
|
||||
use axum::extract::FromRef;
|
||||
|
||||
use rear::admin::state::{AdminState, SharedAdminRegistry};
|
||||
use rear::depot::state::{DepotState, SharedDepotRegistry};
|
||||
use rear::service::templates;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub templates: templates::Templates,
|
||||
pub admin: SharedAdminRegistry,
|
||||
pub admin: SharedDepotRegistry,
|
||||
}
|
||||
|
||||
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 {
|
||||
&self.templates
|
||||
}
|
||||
|
||||
fn get_registry(&self) -> SharedAdminRegistry {
|
||||
fn get_registry(&self) -> SharedDepotRegistry {
|
||||
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" %}
|
||||
|
||||
<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 extrahead %}{% endblock %}
|
||||
@ -42,14 +42,14 @@
|
||||
{% endblock usertools %}
|
||||
<div class="item">
|
||||
<a class="ui blue image label">
|
||||
<img src="/static/admin/teddy_bear.png">
|
||||
<img src="/static/rear/teddy_bear.png">
|
||||
Person
|
||||
<div class="detail">Admin</div>
|
||||
<div class="detail">Admiral</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
{% block sidebar %}
|
||||
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
|
||||
<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>
|
||||
<b>Administration</b>
|
||||
</a>
|
||||
@ -97,17 +97,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if available_apps %}
|
||||
{% for app in available_apps %}
|
||||
{% if sections %}
|
||||
{% for section in sections %}
|
||||
<div class="item">
|
||||
<i class="cog link icon" href="{{ app.admin_url }}"></i>
|
||||
<div class="header">{{ app.name }} </div>
|
||||
<i class="cog link icon" href="{{ section.section_url }}"></i>
|
||||
<div class="header">{{ section.name }} </div>
|
||||
<div class="menu">
|
||||
{% for model in app.models %}
|
||||
{% for model in section.models %}
|
||||
<div
|
||||
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||
{% if model.admin_url %}
|
||||
<a href="{{ model.admin_url }}" hx-get="{{ model.admin_url }}" hx-target="#main" hx-push-url="true" {% if
|
||||
{% if model.model_url %}
|
||||
<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>
|
||||
{% else %}
|
||||
<span>{{ model.name }}</span>
|
@ -1,15 +1,16 @@
|
||||
{% if app_list %}
|
||||
{% for app in app_list %}
|
||||
{% if sections %}
|
||||
{% for section in sections %}
|
||||
<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">
|
||||
<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>
|
||||
{% for model in app.models %}
|
||||
<tr class="model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||
{% if model.admin_url %}
|
||||
<th scope="row"><a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}
|
||||
{% for model in section.models %}
|
||||
<tr class="model-{{ model.key }}{% if model.model_url in request.path|urlencode %} current-model{% endif %}">
|
||||
{% if model.model_url %}
|
||||
<th scope="row"><a href="{{ model.model_url }}" {% if model.model_url in request.path|urlencode %}
|
||||
aria-current="page" {% endif %}>{{ model.name }}</a></th>
|
||||
{% else %}
|
||||
<th scope="row">{{ model.name }}</th>
|
||||
@ -21,11 +22,11 @@
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.admin_url and show_changelinks %}
|
||||
{% if model.model_url and show_changelinks %}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
{% elif show_changelinks %}
|
||||
<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