This commit is contained in:
2026-07-25 01:17:19 +02:00
parent 62f7c6edba
commit b54b60c988
12 changed files with 224 additions and 98 deletions
+81 -13
View File
@@ -243,12 +243,6 @@ impl MigrationTrait for Migration {
) )
.col(ColumnDef::new(Alias::new("server_id")).uuid().not_null()) .col(ColumnDef::new(Alias::new("server_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("name")).string().not_null()) .col(ColumnDef::new(Alias::new("name")).string().not_null())
.col(
ColumnDef::new(Alias::new("position"))
.integer()
.not_null()
.default(0),
)
.col( .col(
ColumnDef::new(Alias::new("created_at")) ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone() .timestamp_with_time_zone()
@@ -285,12 +279,6 @@ impl MigrationTrait for Migration {
) )
.col(ColumnDef::new(Alias::new("server_id")).uuid().null()) .col(ColumnDef::new(Alias::new("server_id")).uuid().null())
.col(ColumnDef::new(Alias::new("category_id")).uuid().null()) .col(ColumnDef::new(Alias::new("category_id")).uuid().null())
.col(
ColumnDef::new(Alias::new("position"))
.integer()
.not_null()
.default(0),
)
.col( .col(
ColumnDef::new(Alias::new("channel_type")) ColumnDef::new(Alias::new("channel_type"))
.integer() .integer()
@@ -327,6 +315,86 @@ impl MigrationTrait for Migration {
) )
.await?; .await?;
// ---------------------------------------------------------------------
// Ordre des catégories et des canaux (Polymorphique)
// ---------------------------------------------------------------------
manager
.create_table(
Table::create()
.table(Alias::new("server_item_order"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Alias::new("server_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("resource_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("resource_type"))
.integer()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("parent_category_id"))
.uuid()
.null(),
)
.col(
ColumnDef::new(Alias::new("order_key"))
.big_integer()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.foreign_key(
ForeignKey::create()
.name("fk_server_item_order_server")
.from(Alias::new("server_item_order"), Alias::new("server_id"))
.to(Alias::new("server"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_server_item_order_parent_category")
.from(
Alias::new("server_item_order"),
Alias::new("parent_category_id"),
)
.to(Alias::new("category"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade),
)
.index(
Index::create()
.name("uq_server_item_order_resource")
.col(Alias::new("server_id"))
.col(Alias::new("resource_type"))
.col(Alias::new("resource_id"))
.unique(),
)
.index(
Index::create()
.name("idx_server_item_order_scope")
.col(Alias::new("server_id"))
.col(Alias::new("parent_category_id"))
.col(Alias::new("order_key")),
)
.to_owned(),
)
.await?;
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// Membres des canaux // Membres des canaux
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
@@ -716,7 +784,6 @@ impl MigrationTrait for Migration {
} }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Les tables dépendantes doivent être supprimées avant leurs parents.
let tables = [ let tables = [
"computed_permission", "computed_permission",
"channel_user_permission", "channel_user_permission",
@@ -726,6 +793,7 @@ impl MigrationTrait for Migration {
"attachment", "attachment",
"message", "message",
"channel_user", "channel_user",
"server_item_order",
"channel", "channel",
"category", "category",
"role_user", "role_user",
+1 -2
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model] #[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@@ -12,7 +12,6 @@ pub struct Model {
pub id: Uuid, pub id: Uuid,
pub server_id: Uuid, pub server_id: Uuid,
pub name: String, pub name: String,
pub position: i32,
pub created_at: DateTimeUtc, pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc, pub updated_at: DateTimeUtc,
#[sea_orm(has_many)] #[sea_orm(has_many)]
+1 -2
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema; use utoipa::ToSchema;
@@ -28,7 +28,6 @@ pub struct Model {
pub id: Uuid, pub id: Uuid,
pub server_id: Option<Uuid>, pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>, pub category_id: Option<Uuid>,
pub position: i32,
pub channel_type: ChannelType, pub channel_type: ChannelType,
pub name: Option<String>, pub name: Option<String>,
pub created_at: DateTimeUtc, pub created_at: DateTimeUtc,
+1
View File
@@ -13,6 +13,7 @@ pub mod message;
pub mod role; pub mod role;
pub mod role_user; pub mod role_user;
pub mod server; pub mod server;
pub mod server_item_order;
pub mod server_role_permission; pub mod server_role_permission;
pub mod server_user; pub mod server_user;
pub mod server_user_permission; pub mod server_user_permission;
+1
View File
@@ -9,6 +9,7 @@ pub use super::message::Entity as Message;
pub use super::role::Entity as Group; pub use super::role::Entity as Group;
pub use super::role_user::Entity as GroupMember; pub use super::role_user::Entity as GroupMember;
pub use super::server::Entity as Server; pub use super::server::Entity as Server;
pub use super::server_item_order::Entity as ServerItemOrder;
pub use super::server_role_permission::Entity as ServerRolePermission; pub use super::server_role_permission::Entity as ServerRolePermission;
pub use super::server_user::Entity as ServerUser; pub use super::server_user::Entity as ServerUser;
pub use super::server_user_permission::Entity as ServerUserPermission; pub use super::server_user_permission::Entity as ServerUserPermission;
+74
View File
@@ -0,0 +1,74 @@
use sea_orm::Set;
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
)]
#[sea_orm(rs_type = "i32", db_type = "Integer")]
#[serde(rename_all = "snake_case")]
pub enum OrderedResourceType {
#[sea_orm(num_value = 0)]
Channel,
#[sea_orm(num_value = 1)]
Category,
}
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "server_item_order")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub server_id: Uuid,
/// UUID du channel ou de la catégorie ordonné(e).
pub resource_id: Uuid,
/// Type de la ressource référencée par resource_id.
pub resource_type: OrderedResourceType,
/// NULL pour la liste racine du serveur.
/// Renseigné uniquement pour un channel placé dans une catégorie.
pub parent_category_id: Option<Uuid>,
/// Clé de tri relative à (server_id, parent_category_id).
pub order_key: i64,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(
belongs_to,
from = "server_id",
to = "id",
on_update = "Cascade",
on_delete = "Cascade"
)]
pub server: HasOne<super::server::Entity>,
#[sea_orm(
belongs_to,
from = "parent_category_id",
to = "id",
on_update = "Cascade",
on_delete = "Cascade"
)]
pub parent_category: HasOne<super::category::Entity>,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}
+57 -59
View File
@@ -1,7 +1,5 @@
use super::types::{ServerExplorerItem, ServerTree};
use super::{AnyResult, RepositoryContext}; use super::{AnyResult, RepositoryContext};
use crate::models::{ use crate::models::{role, server, server_role_permission, server_user, server_user_permission,
category, channel, role, server, server_role_permission, server_user, server_user_permission,
}; };
use sea_orm::prelude::*; use sea_orm::prelude::*;
use sea_orm::{ActiveModelTrait, QuerySelect, Set}; use sea_orm::{ActiveModelTrait, QuerySelect, Set};
@@ -204,59 +202,59 @@ impl ServerRepository {
} }
// Helpers // Helpers
impl ServerRepository { // impl ServerRepository {
pub async fn get_tree(&self, server_id: Uuid) -> AnyResult<ServerTree> { // pub async fn get_tree(&self, server_id: Uuid) -> AnyResult<ServerTree> {
// 1. Récupération des catégories avec leurs channels // // 1. Récupération des catégories avec leurs channels
let categories_with_channels = category::Entity::find() // let categories_with_channels = category::Entity::find()
.filter(category::Column::ServerId.eq(server_id)) // .filter(category::Column::ServerId.eq(server_id))
.find_with_related(channel::Entity) // .find_with_related(channel::Entity)
.all(&self.context.db) // .all(&self.context.db)
.await?; // .await?;
//
// 2. Récupération des channels orphelins (sans catégorie) // // 2. Récupération des channels orphelins (sans catégorie)
let orphan_channels = channel::Entity::find() // let orphan_channels = channel::Entity::find()
.filter(channel::Column::ServerId.eq(server_id)) // .filter(channel::Column::ServerId.eq(server_id))
.filter(channel::Column::CategoryId.is_null()) // .filter(channel::Column::CategoryId.is_null())
.all(&self.context.db) // .all(&self.context.db)
.await?; // .await?;
//
// 3. Transformation et tri des enfants // // 3. Transformation et tri des enfants
let mut items: Vec<ServerExplorerItem> = Vec::new(); // let mut items: Vec<ServerExplorerItem> = Vec::new();
//
for (cat, mut channels) in categories_with_channels { // for (cat, mut channels) in categories_with_channels {
// On trie les channels internes (obligatoire car SQL ne garantit aucun ordre ici) // // On trie les channels internes (obligatoire car SQL ne garantit aucun ordre ici)
channels.sort_by(|a, b| { // channels.sort_by(|a, b| {
a.position // a.position
.cmp(&b.position) // .cmp(&b.position)
.then(a.created_at.cmp(&b.created_at)) // .then(a.created_at.cmp(&b.created_at))
}); // });
items.push(ServerExplorerItem::Category(cat, channels)); // items.push(ServerExplorerItem::Category(cat, channels));
} // }
//
for chan in orphan_channels { // for chan in orphan_channels {
items.push(ServerExplorerItem::Channel(chan)); // items.push(ServerExplorerItem::Channel(chan));
} // }
//
// 4. Tri final de la liste globale (Mélange catégories et orphelins) // // 4. Tri final de la liste globale (Mélange catégories et orphelins)
items.sort_by(|a, b| { // items.sort_by(|a, b| {
let pos_cmp = a.position().cmp(&b.position()); // let pos_cmp = a.position().cmp(&b.position());
//
if pos_cmp == std::cmp::Ordering::Equal { // if pos_cmp == std::cmp::Ordering::Equal {
// Départage par date si position identique // // Départage par date si position identique
let date_a = match a { // let date_a = match a {
ServerExplorerItem::Category(c, _) => c.created_at, // ServerExplorerItem::Category(c, _) => c.created_at,
ServerExplorerItem::Channel(c) => c.created_at, // ServerExplorerItem::Channel(c) => c.created_at,
}; // };
let date_b = match b { // let date_b = match b {
ServerExplorerItem::Category(c, _) => c.created_at, // ServerExplorerItem::Category(c, _) => c.created_at,
ServerExplorerItem::Channel(c) => c.created_at, // ServerExplorerItem::Channel(c) => c.created_at,
}; // };
date_a.cmp(&date_b) // date_a.cmp(&date_b)
} else { // } else {
pos_cmp // pos_cmp
} // }
}); // });
//
Ok(ServerTree { items }) // Ok(ServerTree { items })
} // }
} // }
+8 -8
View File
@@ -6,14 +6,14 @@ pub enum ServerExplorerItem {
} }
// Pour pouvoir trier facilement // Pour pouvoir trier facilement
impl ServerExplorerItem { // impl ServerExplorerItem {
pub fn position(&self) -> i32 { // pub fn position(&self) -> i32 {
match self { // match self {
ServerExplorerItem::Category(cat, _) => cat.position, // ServerExplorerItem::Category(cat, _) => cat.position,
ServerExplorerItem::Channel(chan) => chan.position, // ServerExplorerItem::Channel(chan) => chan.position,
} // }
} // }
} // }
pub struct ServerTree { pub struct ServerTree {
pub items: Vec<ServerExplorerItem>, pub items: Vec<ServerExplorerItem>,
-4
View File
@@ -8,15 +8,12 @@ pub struct CreateCategoryRequest {
pub server_id: Uuid, pub server_id: Uuid,
#[schema(example = "Discussion")] #[schema(example = "Discussion")]
pub name: String, pub name: String,
#[serde(default)]
pub position: i32,
} }
#[derive(Debug, Serialize, Deserialize, ToSchema)] #[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UpdateCategoryRequest { pub struct UpdateCategoryRequest {
#[schema(example = "Discussion (Maj)")] #[schema(example = "Discussion (Maj)")]
pub name: String, pub name: String,
pub position: i32,
} }
#[derive(Debug, Serialize, Deserialize, ToSchema)] #[derive(Debug, Serialize, Deserialize, ToSchema)]
@@ -24,7 +21,6 @@ pub struct CategoryResponse {
pub id: Uuid, pub id: Uuid,
pub server_id: Uuid, pub server_id: Uuid,
pub name: String, pub name: String,
pub position: i32,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
} }
-3
View File
@@ -10,7 +10,6 @@ pub fn category_model_to_category_response(model: category::Model) -> CategoryRe
id: model.id, id: model.id,
server_id: model.server_id, server_id: model.server_id,
name: model.name, name: model.name,
position: model.position,
created_at: model.created_at, created_at: model.created_at,
updated_at: model.updated_at, updated_at: model.updated_at,
} }
@@ -21,7 +20,6 @@ pub fn create_request_to_am(req: CreateCategoryRequest) -> category::ActiveModel
id: Set(Uuid::new_v4()), id: Set(Uuid::new_v4()),
server_id: Set(req.server_id), server_id: Set(req.server_id),
name: Set(req.name), name: Set(req.name),
position: Set(req.position),
..Default::default() ..Default::default()
} }
} }
@@ -35,7 +33,6 @@ pub fn update_request_to_am(
id: Set(id), id: Set(id),
server_id: Set(server_id), server_id: Set(server_id),
name: Set(req.name), name: Set(req.name),
position: Set(req.position),
..Default::default() ..Default::default()
} }
} }
-4
View File
@@ -8,8 +8,6 @@ use uuid::Uuid;
pub struct CreateChannelRequest { pub struct CreateChannelRequest {
pub server_id: Option<Uuid>, pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>, pub category_id: Option<Uuid>,
#[serde(default)]
pub position: i32,
pub channel_type: ChannelType, pub channel_type: ChannelType,
#[schema(example = "général")] #[schema(example = "général")]
pub name: Option<String>, pub name: Option<String>,
@@ -19,7 +17,6 @@ pub struct CreateChannelRequest {
pub struct UpdateChannelRequest { pub struct UpdateChannelRequest {
pub server_id: Option<Uuid>, pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>, pub category_id: Option<Uuid>,
pub position: i32,
pub channel_type: ChannelType, pub channel_type: ChannelType,
pub name: Option<String>, pub name: Option<String>,
} }
@@ -29,7 +26,6 @@ pub struct ChannelResponse {
pub id: Uuid, pub id: Uuid,
pub server_id: Option<Uuid>, pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>, pub category_id: Option<Uuid>,
pub position: i32,
pub channel_type: ChannelType, pub channel_type: ChannelType,
pub name: Option<String>, pub name: Option<String>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
-3
View File
@@ -11,7 +11,6 @@ pub fn channel_model_to_channel_response(model: channel::Model) -> ChannelRespon
id: model.id, id: model.id,
server_id: model.server_id, server_id: model.server_id,
category_id: model.category_id, category_id: model.category_id,
position: model.position,
channel_type: model.channel_type, channel_type: model.channel_type,
name: model.name, name: model.name,
created_at: model.created_at, created_at: model.created_at,
@@ -24,7 +23,6 @@ pub fn create_request_to_am(req: CreateChannelRequest) -> channel::ActiveModel {
id: Set(Uuid::new_v4()), id: Set(Uuid::new_v4()),
server_id: Set(req.server_id), server_id: Set(req.server_id),
category_id: Set(req.category_id), category_id: Set(req.category_id),
position: Set(req.position),
channel_type: Set(req.channel_type), channel_type: Set(req.channel_type),
name: Set(req.name), name: Set(req.name),
..Default::default() ..Default::default()
@@ -36,7 +34,6 @@ pub fn update_request_to_am(id: Uuid, req: UpdateChannelRequest) -> channel::Act
id: Set(id), id: Set(id),
server_id: Set(req.server_id), server_id: Set(req.server_id),
category_id: Set(req.category_id), category_id: Set(req.category_id),
position: Set(req.position),
channel_type: Set(req.channel_type), channel_type: Set(req.channel_type),
name: Set(req.name), name: Set(req.name),
..Default::default() ..Default::default()