diff --git a/src/database/category.rs b/src/database/category.rs index 4f66e4b..e6a1b50 100644 --- a/src/database/category.rs +++ b/src/database/category.rs @@ -14,6 +14,7 @@ use crate::state::logger::LoggerError; /// /// Each category has a name and an associated path on disk, where /// symlinks to the content will be created. +#[sea_orm::model] #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "category")] pub struct Model { @@ -23,11 +24,10 @@ pub struct Model { pub name: String, #[sea_orm(unique)] pub path: String, + #[sea_orm(has_many)] + pub content_folders: HasMany, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - #[async_trait::async_trait] impl ActiveModelBehavior for ActiveModel {} @@ -44,8 +44,8 @@ pub enum CategoryError { IO { source: std::io::Error }, #[snafu(display("Database error"))] DB { source: sea_orm::DbErr }, - #[snafu(display("The category (ID: {id}) does not exist"))] - NotFound { id: i32 }, + #[snafu(display("The category ({details}) does not exist"))] + NotFound { details: String }, #[snafu(display("Failed to save the operation log"))] Logger { source: LoggerError }, } @@ -71,6 +71,37 @@ impl CategoryOperator { .context(DBSnafu) } + /// Find one category by ID + pub async fn find_by_id(&self, id: i32) -> Result { + let category = Entity::find_by_id(id) + .one(&self.state.database) + .await + .context(DBSnafu)?; + + match category { + Some(category) => Ok(category), + None => Err(CategoryError::NotFound { + details: format!("ID: {}", id), + }), + } + } + + /// Find one category by Name + pub async fn find_by_name(&self, name: String) -> Result { + let category = Entity::find() + .filter(Column::Name.contains(name.clone())) + .one(&self.state.database) + .await + .context(DBSnafu)?; + + match category { + Some(category) => Ok(category), + None => Err(CategoryError::NotFound { + details: format!("NAME: {}", name), + }), + } + } + /// Delete a category pub async fn delete(&self, id: i32, user: Option) -> Result { let db = &self.state.database; @@ -101,7 +132,9 @@ impl CategoryOperator { Ok(category_clone.name) } - None => Err(CategoryError::NotFound { id }), + None => Err(CategoryError::NotFound { + details: format!("ID: {}", id), + }), } } @@ -139,9 +172,11 @@ impl CategoryOperator { }); } + // Normalized path to avoid trailing slash + let normalized_path = dir.components().collect::(); let model = ActiveModel { name: Set(f.name.clone()), - path: Set(f.path.clone()), + path: Set(normalized_path.into_string()), ..Default::default() } .save(&self.state.database) diff --git a/src/database/content_folder.rs b/src/database/content_folder.rs new file mode 100644 index 0000000..09a345a --- /dev/null +++ b/src/database/content_folder.rs @@ -0,0 +1,176 @@ +use chrono::Utc; +use sea_orm::entity::prelude::*; +use sea_orm::*; +use snafu::prelude::*; + +use crate::database::category; +use crate::database::operation::{Operation, OperationId, OperationLog, OperationType, Table}; +use crate::extractors::user::User; +use crate::routes::content_folder::ContentFolderForm; +use crate::state::AppState; +use crate::state::logger::LoggerError; + +/// A content folder to store associated files. +/// +/// Each content folder has a name and an associated path on disk, a Category +/// and it can have an Parent Content Folder (None if it's the first folder +/// in category) +#[sea_orm::model] +#[derive(DeriveEntityModel, Clone, Debug, PartialEq, Eq)] +#[sea_orm(table_name = "content_folder")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + #[sea_orm(unique)] + pub path: String, + pub category_id: i32, + #[sea_orm(belongs_to, from = "category_id", to = "id")] + pub category: HasOne, + pub parent_id: Option, + #[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")] + pub parent: HasOne, +} + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub enum ContentFolderError { + #[snafu(display("There is already a content folder called `{name}`"))] + NameTaken { name: String }, + #[snafu(display("There is already a content folder in dir `{path}`"))] + PathTaken { path: String }, + #[snafu(display("The Content Folder (Path: {path}) does not exist"))] + NotFound { path: String }, + #[snafu(display("Database error"))] + DB { source: sea_orm::DbErr }, + #[snafu(display("Failed to save the operation log"))] + Logger { source: LoggerError }, +} + +#[derive(Clone, Debug)] +pub struct ContentFolderOperator { + pub state: AppState, + pub user: Option, +} + +impl ContentFolderOperator { + pub fn new(state: AppState, user: Option) -> Self { + Self { state, user } + } + + /// List Content Folders + /// + /// Should not fail, unless SQLite was corrupted for some reason. + pub async fn list_by_parent_and_category( + &self, + parent_id: Option, + category_id: i32, + ) -> Result, ContentFolderError> { + let mut query = Entity::find().filter(Column::CategoryId.eq(category_id)); + // parent_id can be None when it's the first folder in categories + match parent_id { + Some(parent_id) => query = query.filter(Column::ParentId.eq(parent_id)), + None => query = query.filter(Column::ParentId.is_null()), + } + + query.all(&self.state.database).await.context(DBSnafu) + } + + /// Find one Content Folder by path + /// + /// Should not fail, unless SQLite was corrupted for some reason. + pub async fn find_by_path(&self, path: String) -> Result { + let content_folder = Entity::find_by_path(path.clone()) + .one(&self.state.database) + .await + .context(DBSnafu)?; + + match content_folder { + Some(category) => Ok(category), + None => Err(ContentFolderError::NotFound { path }), + } + } + + /// Find one Content Folder by ID + /// + /// Should not fail, unless SQLite was corrupted for some reason. + pub async fn find_by_id(&self, id: i32) -> Result { + let content_folder = Entity::find_by_id(id) + .one(&self.state.database) + .await + .context(DBSnafu)?; + + match content_folder { + Some(category) => Ok(category), + None => Err(ContentFolderError::NotFound { + path: id.to_string(), + }), + } + } + + /// Create a new content folder + /// + /// Fails if: + /// + /// - name or path is already taken (they should be unique in one folder) + /// - path parent directory does not exist (to avoid completely wrong paths) + pub async fn create( + &self, + f: &ContentFolderForm, + user: Option, + ) -> Result { + // Check duplicates in same folder + let list = self + .list_by_parent_and_category(f.parent_id, f.category_id) + .await?; + + if list.iter().any(|x| x.name == f.name) { + return Err(ContentFolderError::NameTaken { + name: f.name.clone(), + }); + } + + if list.iter().any(|x| x.path == f.path) { + return Err(ContentFolderError::PathTaken { + path: f.path.clone(), + }); + } + + let model = ActiveModel { + name: Set(f.name.clone()), + path: Set(f.path.clone()), + category_id: Set(f.category_id), + parent_id: Set(f.parent_id), + ..Default::default() + } + .save(&self.state.database) + .await + .context(DBSnafu)?; + + // Should not fail + let model = model.try_into_model().unwrap(); + + let operation_log = OperationLog { + user, + date: Utc::now(), + table: Table::ContentFolder, + operation: OperationType::Create, + operation_id: OperationId { + object_id: model.id.to_owned(), + name: f.name.to_string(), + }, + operation_form: Some(Operation::ContentFolder(f.clone())), + }; + + self.state + .logger + .write(operation_log) + .await + .context(LoggerSnafu)?; + + Ok(model) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 9f25b45..d795369 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,3 +1,4 @@ // sea_orm example: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/ pub mod category; +pub mod content_folder; pub mod operation; diff --git a/src/database/operation.rs b/src/database/operation.rs index 549d35d..492f8c4 100644 --- a/src/database/operation.rs +++ b/src/database/operation.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::extractors::user::User; use crate::routes::category::CategoryForm; +use crate::routes::content_folder::ContentFolderForm; /// Type of operation applied to the database. #[derive(Clone, Debug, Display, Serialize, Deserialize)] @@ -22,6 +23,7 @@ pub struct OperationId { #[derive(Clone, Debug, Display, Serialize, Deserialize)] pub enum Table { Category, + ContentFolder, } /// Operation applied to the database. @@ -31,6 +33,7 @@ pub enum Table { #[serde(untagged)] pub enum Operation { Category(CategoryForm), + ContentFolder(ContentFolderForm), } impl std::fmt::Display for Operation { diff --git a/src/lib.rs b/src/lib.rs index ad27452..614d40c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,11 +20,18 @@ pub fn router(state: state::AppState) -> Router { Router::new() // Register dynamic routes .route("/", get(routes::index::index)) + .route("/file-system", get(routes::index::file_system)) .route("/progress/{view_request}", get(routes::progress::progress)) .route("/categories", get(routes::category::index)) .route("/categories", post(routes::category::create)) .route("/categories/new", get(routes::category::new)) .route("/categories/{id}/delete", get(routes::category::delete)) + .route("/folders/{category_id}", get(routes::category::show)) + .route( + "/folders/{category_name}/{*folder_path}", + get(routes::content_folder::show), + ) + .route("/folders", post(routes::content_folder::create)) .route("/logs", get(routes::logs::index)) // Register static assets routes .nest("/assets", static_router()) diff --git a/src/migration/m20251110_01_create_table_category.rs b/src/migration/m20251110_01_create_table_category.rs index 2cf8da4..327271f 100644 --- a/src/migration/m20251110_01_create_table_category.rs +++ b/src/migration/m20251110_01_create_table_category.rs @@ -27,7 +27,7 @@ impl MigrationTrait for Migration { } #[derive(DeriveIden)] -enum Category { +pub enum Category { Table, Id, Name, diff --git a/src/migration/m20251113_203047_add_content_folder.rs b/src/migration/m20251113_203047_add_content_folder.rs new file mode 100644 index 0000000..2d818a8 --- /dev/null +++ b/src/migration/m20251113_203047_add_content_folder.rs @@ -0,0 +1,57 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +use super::m20251110_01_create_table_category::Category; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(ContentFolder::Table) + .if_not_exists() + .col(pk_auto(ContentFolder::Id)) + .col(string(ContentFolder::Name)) + .col(string(ContentFolder::Path)) + .col( + ColumnDef::new(ContentFolder::CategoryId) + .integer() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk-content-file-category_id") + .from(ContentFolder::Table, ContentFolder::CategoryId) + .to(Category::Table, Category::Id), + ) + .col(ColumnDef::new(ContentFolder::ParentId).integer()) + .foreign_key( + ForeignKey::create() + .name("fk-content-folder-parent_id") + .from(ContentFolder::ParentId, ContentFolder::ParentId) + .to(ContentFolder::Table, ContentFolder::Id), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(ContentFolder::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +pub enum ContentFolder { + Table, + Id, + Name, + Path, + CategoryId, + ParentId, +} diff --git a/src/migration/m20251113_203899_add_uniq_to_content_folder.rs b/src/migration/m20251113_203899_add_uniq_to_content_folder.rs new file mode 100644 index 0000000..06bf1c9 --- /dev/null +++ b/src/migration/m20251113_203899_add_uniq_to_content_folder.rs @@ -0,0 +1,37 @@ +use sea_orm_migration::prelude::*; + +use super::m20251113_203047_add_content_folder::ContentFolder; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_index( + Index::create() + .name("content_folder_uniq_path") + .table(ContentFolder::Table) + .col(ContentFolder::Path) + .unique() + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_index( + Index::drop() + .name("content_folder_uniq_path") + .table(ContentFolder::Table) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/src/migration/mod.rs b/src/migration/mod.rs index 55574d2..ee2294a 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -1,12 +1,18 @@ pub use sea_orm_migration::prelude::*; mod m20251110_01_create_table_category; +mod m20251113_203047_add_content_folder; +mod m20251113_203899_add_uniq_to_content_folder; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20251110_01_create_table_category::Migration)] + vec![ + Box::new(m20251110_01_create_table_category::Migration), + Box::new(m20251113_203047_add_content_folder::Migration), + Box::new(m20251113_203899_add_uniq_to_content_folder::Migration), + ] } } diff --git a/src/routes/category.rs b/src/routes/category.rs index ca0b88d..c09d7f3 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use snafu::prelude::*; use crate::database::category::CategoryError; +use crate::database::content_folder::{self, ContentFolderOperator}; use crate::database::{category, category::CategoryOperator}; use crate::extractors::user::User; use crate::state::flash_message::{OperationStatus, get_cookie}; @@ -45,6 +46,19 @@ pub struct NewCategoryTemplate { pub category_form: Option, } +#[derive(Template, WebTemplate)] +#[template(path = "categories/show.html")] +pub struct CategoryShowTemplate { + /// Global application state + pub state: AppStateContext, + /// Categories found in database + pub content_folders: Vec, + /// Logged-in user. + pub user: Option, + /// Category + category: category::Model, +} + pub async fn new( State(app_state): State, user: Option, @@ -144,3 +158,30 @@ pub async fn index( }, )) } + +pub async fn show( + State(app_state): State, + user: Option, + Path(category_name): Path, +) -> Result { + let app_state_context = app_state.context().await?; + + let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) + .find_by_name(category_name.to_string()) + .await + .context(CategorySnafu)?; + + // get all content folders in this category + let content_folders: Vec = + ContentFolderOperator::new(app_state.clone(), user.clone()) + .list_by_parent_and_category(None, category.id) + .await + .context(ContentFolderSnafu)?; + + Ok(CategoryShowTemplate { + content_folders, + category, + state: app_state_context, + user, + }) +} diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs new file mode 100644 index 0000000..eaba307 --- /dev/null +++ b/src/routes/content_folder.rs @@ -0,0 +1,205 @@ +use askama::Template; +use askama_web::WebTemplate; +use axum::Form; +use axum::extract::{Path, State}; +use axum::response::{IntoResponse, Redirect}; +use axum_extra::extract::CookieJar; +use camino::Utf8PathBuf; +use serde::{Deserialize, Serialize}; +use snafu::prelude::*; + +use crate::database::category::CategoryOperator; +use crate::database::content_folder::ContentFolderOperator; +use crate::database::{category, content_folder}; +use crate::extractors::user::User; +use crate::state::flash_message::{OperationStatus, get_cookie}; +use crate::state::{AppState, AppStateContext, error::*}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ContentFolderForm { + pub name: String, + pub parent_id: Option, + pub path: String, + pub category_id: i32, +} + +#[derive(Template, WebTemplate)] +#[template(path = "content_folders/show.html")] +pub struct ContentFolderShowTemplate { + /// Global application state + pub state: AppStateContext, + /// current folder + pub current_content_folder: content_folder::Model, + /// Folders with parent_id set to current folder + pub sub_content_folders: Vec, + /// Logged-in user. + pub user: Option, + /// Category + pub category: category::Model, + /// BreadCrumb extract from path + pub breadcrumb_items: Vec, + /// Parent Folder if exist. If None, the parent is category + pub parent_folder: Option, + /// Operation status for UI confirmation (Cookie) + pub flash: Option, +} + +pub struct PathBreadcrumb { + pub name: String, + pub path: String, +} + +pub async fn show( + State(app_state): State, + user: Option, + Path((_category_name, folder_path)): Path<(String, String)>, + jar: CookieJar, +) -> Result<(CookieJar, ContentFolderShowTemplate), AppStateError> { + let app_state_context = app_state.context().await?; + + let content_folder_operator = ContentFolderOperator::new(app_state.clone(), user.clone()); + + // get current content folders with Path + let current_content_folder = content_folder_operator + // must format to add "/" in front of path like in DB + .find_by_path(format!("/{}", folder_path)) + .await + .context(ContentFolderSnafu)?; + + // Get all sub content folders of the current folder + let sub_content_folders: Vec = content_folder_operator + .list_by_parent_and_category( + Some(current_content_folder.id), + current_content_folder.category_id, + ) + .await + .context(ContentFolderSnafu)?; + + // Get current categories + let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) + .find_by_id(current_content_folder.category_id) + .await + .context(CategorySnafu)?; + + // create breadcrumb with ancestor of current folders + let mut content_folder_ancestors: Vec = Vec::new(); + // To get Current Parent Folder + let mut parent_folder: Option = None; + + content_folder_ancestors.push(PathBreadcrumb { + name: current_content_folder.name.clone(), + path: current_content_folder.path.clone(), + }); + + let mut current_id = current_content_folder.parent_id; + while let Some(id) = current_id { + let folder = content_folder_operator + .find_by_id(id) + .await + .context(ContentFolderSnafu)?; + + if parent_folder.is_none() { + parent_folder = Some(folder.clone()); + } + + content_folder_ancestors.push(PathBreadcrumb { + name: folder.name, + path: folder.path, + }); + + current_id = folder.parent_id; + } + + // Reverse the ancestor to create Breadrumb + content_folder_ancestors.reverse(); + + let (jar, operation_status) = get_cookie(jar); + + Ok(( + jar, + ContentFolderShowTemplate { + parent_folder, + breadcrumb_items: content_folder_ancestors, + sub_content_folders, + current_content_folder, + category, + state: app_state_context, + user, + flash: operation_status, + }, + )) +} + +pub async fn create( + State(app_state): State, + user: Option, + jar: CookieJar, + Form(mut form): Form, +) -> Result { + // let app_state_context = app_state.context().await?; + let content_folder = ContentFolderOperator::new(app_state.clone(), user.clone()); + + // build path with Parent folder path (or category path if parent is None) + folder.name + let parent_path = match form.parent_id { + Some(parent_id) => { + let parent_folder = content_folder + .find_by_id(parent_id) + .await + .context(ContentFolderSnafu)?; + parent_folder.path + } + None => String::from(""), + }; + + // Get folder category + let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) + .find_by_id(form.category_id) + .await + .context(CategorySnafu)?; + + // If name contains "/" returns an error + if form.name.contains("/") { + let operation_status = OperationStatus { + success: false, + message: format!( + "Failed to create Folder, {} is not valid (it contains '/')", + form.name + ), + }; + let jar = operation_status.set_cookie(jar); + + let uri = format!("/folders/{}{}", category.name, parent_path); + + return Ok((jar, Redirect::to(uri.as_str()).into_response())); + } + + // build final path with parent_path and path of form and normalized it with Utf8PathBuf + form.path = Utf8PathBuf::from(format!("{}/{}", parent_path, form.name)) + .components() + .collect::() + .into_string(); + + let created = content_folder.create(&form, user.clone()).await; + + match created { + Ok(created) => { + tokio::fs::create_dir_all(format!("{}/{}", category.path, created.path.clone())) + .await + .context(IOSnafu)?; + + let operation_status = OperationStatus { + success: true, + message: format!( + "The folder {} has been successfully created (ID: {})", + created.name, created.id + ), + }; + + let jar = operation_status.set_cookie(jar); + let uri = format!("/folders/{}{}", category.name, created.path); + + Ok((jar, Redirect::to(uri.as_str()).into_response())) + } + Err(_error) => Ok((jar, Redirect::to("/").into_response())), + } +} diff --git a/src/routes/index.rs b/src/routes/index.rs index 62e83d4..03d7e3e 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -4,7 +4,7 @@ use axum::extract::State; use snafu::prelude::*; // TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/ -use crate::database::category::CategoryOperator; +use crate::database::category::{self, CategoryOperator}; use crate::extractors::user::User; use crate::state::{AppState, AppStateContext, error::*}; @@ -19,6 +19,17 @@ pub struct IndexTemplate { pub categories: Vec, } +#[derive(Template, WebTemplate)] +#[template(path = "file_system.html")] +pub struct FileSystemTemplate { + /// Global application state (errors/warnings) + pub state: AppStateContext, + /// Logged-in user. + pub user: Option, + /// Categories + pub categories: Vec, +} + impl IndexTemplate { pub async fn new(app_state: AppState, user: Option) -> Result { let categories: Vec = CategoryOperator::new(app_state.clone(), user.clone()) @@ -37,9 +48,34 @@ impl IndexTemplate { } } +impl FileSystemTemplate { + pub async fn new(app_state: AppState, user: Option) -> Result { + let app_state_context = app_state.context().await?; + + let categories = CategoryOperator::new(app_state.clone(), user.clone()) + .list() + .await + .context(CategorySnafu)?; + + Ok(FileSystemTemplate { + state: app_state_context, + user, + categories, + }) + } +} + pub async fn index( State(app_state): State, user: Option, ) -> Result { IndexTemplate::new(app_state, user).await } + +// TODO : Put that to index +pub async fn file_system( + State(app_state): State, + user: Option, +) -> Result { + FileSystemTemplate::new(app_state, user).await +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 75462de..34f239e 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,5 @@ pub mod category; +pub mod content_folder; pub mod index; pub mod logs; pub mod progress; diff --git a/src/state/error.rs b/src/state/error.rs index b84254f..11371b6 100644 --- a/src/state/error.rs +++ b/src/state/error.rs @@ -6,6 +6,7 @@ use snafu::ErrorCompat; use snafu::prelude::*; use crate::database::category::CategoryError; +use crate::database::content_folder::ContentFolderError; use crate::migration::DbErr as MigrationError; use crate::state::free_space::FreeSpaceError; use crate::state::logger::LoggerError; @@ -29,8 +30,12 @@ pub enum AppStateError { }, #[snafu(display("Category error"))] Category { source: CategoryError }, + #[snafu(display("Content Folder error"))] + ContentFolder { source: ContentFolderError }, #[snafu(display("Error during migration"))] Migration { source: MigrationError }, + #[snafu(display("Error during modify fylesystem"))] + IO { source: std::io::Error }, } impl AppStateError { diff --git a/src/state/flash_message.rs b/src/state/flash_message.rs index ad601a1..0694d23 100644 --- a/src/state/flash_message.rs +++ b/src/state/flash_message.rs @@ -1,5 +1,6 @@ use axum_extra::extract::{CookieJar, cookie::Cookie}; +#[derive(Debug)] pub struct OperationStatus { /// Status of operation pub success: bool, @@ -38,9 +39,14 @@ pub fn get_cookie(jar: CookieJar) -> (CookieJar, Option) { _ => None, }; + let mut operation_status_success_cookie = Cookie::from("operation_status_success"); + operation_status_success_cookie.set_path("/"); + let mut operation_status_message_cookie = Cookie::from("operation_status_message"); + operation_status_message_cookie.set_path("/"); + let jar = jar - .remove(Cookie::from("operation_status_success")) - .remove(Cookie::from("operation_status_message")); + .remove(operation_status_success_cookie) + .remove(operation_status_message_cookie); (jar, operation_status) } diff --git a/templates/categories/show.html b/templates/categories/show.html new file mode 100644 index 0000000..328aba4 --- /dev/null +++ b/templates/categories/show.html @@ -0,0 +1,81 @@ +{% extends "layouts/file_system_base.html" %} + +{% block breadcrumb %} + + + +{% endblock %} + +{% block folder_title %} + {{ category.name }} +{% endblock%} + +{% block additional_buttons %} + + Go up + +{% endblock %} + +{% block system_list %} + {% if content_folders.is_empty() %} + {% include "shared/empty_state.html" %} + {% endif %} + + {% for folder in content_folders %} +
  • + +
    +
    +
    + +
    + {{ folder.name }} +
    +
    +
    +
    +

    + +

    +
    +
    +
    +
  • + {% endfor %} +{% endblock %} + + +{% block actions_buttons %} + {% include "content_folders/dropdown_actions.html" %} +{% endblock %} + +{% block content_folder_form %} +
    +
    + + +
    + + + + +
    + +
    +
    +{% endblock %} + diff --git a/templates/content_folders/dropdown_actions.html b/templates/content_folders/dropdown_actions.html new file mode 100644 index 0000000..acc76b6 --- /dev/null +++ b/templates/content_folders/dropdown_actions.html @@ -0,0 +1,10 @@ + diff --git a/templates/content_folders/show.html b/templates/content_folders/show.html new file mode 100644 index 0000000..68186aa --- /dev/null +++ b/templates/content_folders/show.html @@ -0,0 +1,107 @@ +{% extends "layouts/file_system_base.html" %} + +{% block breadcrumb %} + + + + + {% for i in (0..breadcrumb_items.len()) %} + {% let breadcrumb_item = breadcrumb_items[i] %} + + + {% endfor %} +{% endblock %} + +{% block folder_title %} + {{ current_content_folder.name }} +{% endblock%} + +{% block additional_buttons %} + {% if let Some(p) = parent_folder %} + + Go up + + {% else %} + + Go up + + {% endif %} +{% endblock %} + +{% block alert_message %} + {% include "shared/alert_operation_status.html" %} +{% endblock %} + + +{% block system_list %} + {% if sub_content_folders.is_empty() %} + {% include "shared/empty_state.html" %} + {% endif %} + + {% for folder in sub_content_folders %} +
  • + +
    +
    +
    + +
    + {{ folder.name }} +
    +
    +
    +
    +

    + +

    +
    +
    +
    +
  • + {% endfor %} +{% endblock %} + + +{% block actions_buttons %} + {% include "content_folders/dropdown_actions.html" %} +{% endblock %} + +{% block content_folder_form %} +
    +
    + + +
    + + + + + +
    + +
    +
    +{% endblock %} + diff --git a/templates/file_system.html b/templates/file_system.html new file mode 100644 index 0000000..32d0dc2 --- /dev/null +++ b/templates/file_system.html @@ -0,0 +1,39 @@ +{% extends "layouts/file_system_base.html" %} + +{% block breadcrumb %} + +{% endblock %} + +{% block folder_title %} + All categories +{% endblock%} + +{% block system_list %} + {% for category in categories %} +
  • + +
    +
    +
    + +
    + {{ category.name }} {{ category.path }} +
    +
    +
    +
    +

    + +

    +
    +
    +
    +
  • + {% endfor %} +{% endblock %} + + diff --git a/templates/layouts/file_system_base.html b/templates/layouts/file_system_base.html new file mode 100644 index 0000000..3dca376 --- /dev/null +++ b/templates/layouts/file_system_base.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block main %} +
    +

    File System

    + + {% block alert_message %}{% endblock alert_message %} + + {% if state.free_space.free_space_percent < 5 %} +
    + The disks are almost full : {{ state.free_space.free_space_gib }} GiB left +
    + {% endif %} + +
    +
    + +
    + +
    + {% block actions_buttons %}{% endblock actions_buttons %} +
    +
    + +
    +
    +
    +
    +

    {% block folder_title %}{% endblock folder_title %}

    {% block additional_buttons %}{% endblock additional_buttons %}
    +
    + +
      + {% block system_list %}{% endblock system_list %} +
    +
    +
    +
    +
    + + + +{% endblock %} diff --git a/templates/menus/header.html b/templates/menus/header.html index 33071ae..8ef0803 100755 --- a/templates/menus/header.html +++ b/templates/menus/header.html @@ -10,6 +10,9 @@