init
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {storeToRefs} from 'pinia'
|
||||||
|
import {useUserStore} from '@/stores/user'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const {users} = storeToRefs(userStore)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const getUserInitials = (username: string): string => {
|
||||||
|
if (!username) return '?'
|
||||||
|
return username.trim().slice(0, 2).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => emit('update:modelValue', false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-navigation-drawer
|
||||||
|
:model-value="modelValue"
|
||||||
|
location="right"
|
||||||
|
temporary
|
||||||
|
width="280"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<v-toolbar density="compact" flat>
|
||||||
|
<v-toolbar-title>Utilisateurs</v-toolbar-title>
|
||||||
|
<v-btn
|
||||||
|
aria-label="Fermer la liste des utilisateurs"
|
||||||
|
icon="mdi-close"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="close"
|
||||||
|
></v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-list v-if="users.length" density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:title="user.username"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-avatar color="primary" size="32">
|
||||||
|
<span class="text-caption font-weight-medium">{{ getUserInitials(user.username) }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<div v-else class="pa-4 text-medium-emphasis text-body-2">
|
||||||
|
Aucun utilisateur à afficher.
|
||||||
|
</div>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</template>
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {storeToRefs} from 'pinia'
|
import {storeToRefs} from 'pinia'
|
||||||
import {useServerStore} from '@/stores/server'
|
import {useServerStore} from '@/stores/server'
|
||||||
import {ref} from 'vue'
|
import {computed, ref, watch} from 'vue'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import ContextMenu from "@/components/ContextMenu.vue";
|
import ContextMenu from "@/components/ContextMenu.vue";
|
||||||
|
import UserListDrawer from '@/components/UserListDrawer.vue'
|
||||||
|
|
||||||
const serverStore = useServerStore()
|
const serverStore = useServerStore()
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const {servers} = storeToRefs(serverStore)
|
const {servers} = storeToRefs(serverStore)
|
||||||
|
|
||||||
|
const showUsersDrawer = ref(false)
|
||||||
|
const isServerContext = computed(() => Boolean(route.params.serverId))
|
||||||
|
|
||||||
|
watch(isServerContext, (isActive) => {
|
||||||
|
if (!isActive) showUsersDrawer.value = false
|
||||||
|
})
|
||||||
|
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -72,13 +81,13 @@ const getServerColor = (str: string): string => {
|
|||||||
const hue = Math.abs(hash) % 360
|
const hue = Math.abs(hash) % 360
|
||||||
return `hsl(${hue}, 60%, 45%)`
|
return `hsl(${hue}, 60%, 45%)`
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-app @contextmenu.prevent>
|
<v-app @contextmenu.prevent>
|
||||||
<!--Top bar-->
|
<!--Top bar-->
|
||||||
<v-system-bar>
|
<v-system-bar>
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
<v-icon>mdi-square</v-icon>
|
<v-icon>mdi-square</v-icon>
|
||||||
@@ -86,6 +95,15 @@ const getServerColor = (str: string): string => {
|
|||||||
<v-icon>mdi-circle</v-icon>
|
<v-icon>mdi-circle</v-icon>
|
||||||
|
|
||||||
<v-icon>mdi-triangle</v-icon>
|
<v-icon>mdi-triangle</v-icon>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
v-if="isServerContext"
|
||||||
|
aria-label="Afficher les utilisateurs"
|
||||||
|
icon="mdi-account-group"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="showUsersDrawer = !showUsersDrawer"
|
||||||
|
></v-btn>
|
||||||
</v-system-bar>
|
</v-system-bar>
|
||||||
|
|
||||||
<v-navigation-drawer
|
<v-navigation-drawer
|
||||||
@@ -127,6 +145,11 @@ const getServerColor = (str: string): string => {
|
|||||||
></v-btn>
|
></v-btn>
|
||||||
|
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<UserListDrawer
|
||||||
|
v-model="showUsersDrawer"
|
||||||
|
/>
|
||||||
|
|
||||||
<router-view/>
|
<router-view/>
|
||||||
<!-- Menu contextuel global -->
|
<!-- Menu contextuel global -->
|
||||||
<ContextMenu/>
|
<ContextMenu/>
|
||||||
|
|||||||
@@ -142,7 +142,11 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
></v-sheet>
|
></v-sheet>
|
||||||
|
|
||||||
<v-list v-model:opened="openedCategories" density="compact">
|
<v-list
|
||||||
|
v-model:opened="openedCategories"
|
||||||
|
open-strategy="multiple"
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
<template v-for="(item, index) in currentTree" :key="index">
|
<template v-for="(item, index) in currentTree" :key="index">
|
||||||
<!-- Catégorie et ses canaux enfants -->
|
<!-- Catégorie et ses canaux enfants -->
|
||||||
<v-list-group v-if="'Category' in item" :value="item.Category[0].id">
|
<v-list-group v-if="'Category' in item" :value="item.Category[0].id">
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use crate::models::server_item_order;
|
use crate::models::server_item_order;
|
||||||
use crate::models::server_item_order::OrderedResourceType;
|
|
||||||
use crate::repositories::{AnyResult, RepositoryContext};
|
use crate::repositories::{AnyResult, RepositoryContext};
|
||||||
use sea_orm::{
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
|
||||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, TransactionTrait,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -17,29 +14,9 @@ impl ServerItemOrderRepository {
|
|||||||
pub async fn get_by_server(&self, server_id: Uuid) -> AnyResult<Vec<server_item_order::Model>> {
|
pub async fn get_by_server(&self, server_id: Uuid) -> AnyResult<Vec<server_item_order::Model>> {
|
||||||
Ok(server_item_order::Entity::find()
|
Ok(server_item_order::Entity::find()
|
||||||
.filter(server_item_order::Column::ServerId.eq(server_id))
|
.filter(server_item_order::Column::ServerId.eq(server_id))
|
||||||
|
.order_by_asc(server_item_order::Column::ParentCategoryId)
|
||||||
.order_by_asc(server_item_order::Column::OrderKey)
|
.order_by_asc(server_item_order::Column::OrderKey)
|
||||||
.all(&self.context.db)
|
.all(&self.context.db)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Réorganise en bloc (reorder) une liste d'éléments dans un serveur ou une catégorie
|
|
||||||
pub async fn update_orders(
|
|
||||||
&self,
|
|
||||||
server_id: Uuid,
|
|
||||||
items: Vec<(Uuid, OrderedResourceType, Option<Uuid>, i64)>, // (resource_id, resource_type, parent_category_id, order_key)
|
|
||||||
) -> AnyResult<()> {
|
|
||||||
self.context
|
|
||||||
.db
|
|
||||||
.transaction::<_, (), anyhow::Error>(|txn| {
|
|
||||||
Box::pin(async move {
|
|
||||||
for (resource_id, res_type, parent_cat_id, order_key) in items {
|
|
||||||
// Logique de mise à jour / upsert des order_key
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::models::{category, channel, computed_permission, server_item_order};
|
|||||||
use crate::permissions::ChannelPermission;
|
use crate::permissions::ChannelPermission;
|
||||||
use crate::repositories::types::{CategoryWithPermissions, ChannelWithPermissions, ServerTreeData};
|
use crate::repositories::types::{CategoryWithPermissions, ChannelWithPermissions, ServerTreeData};
|
||||||
use crate::repositories::{AnyResult, RepositoryContext};
|
use crate::repositories::{AnyResult, RepositoryContext};
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -19,6 +19,8 @@ impl ServerTreeRepository {
|
|||||||
Ok::<_, anyhow::Error>(
|
Ok::<_, anyhow::Error>(
|
||||||
server_item_order::Entity::find()
|
server_item_order::Entity::find()
|
||||||
.filter(server_item_order::Column::ServerId.eq(server_id))
|
.filter(server_item_order::Column::ServerId.eq(server_id))
|
||||||
|
.order_by_asc(server_item_order::Column::ParentCategoryId)
|
||||||
|
.order_by_asc(server_item_order::Column::OrderKey)
|
||||||
.all(&self.context.db)
|
.all(&self.context.db)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
|
|||||||
+104
-5
@@ -67,9 +67,14 @@ pub fn build_server_tree(
|
|||||||
channels: Vec<ChannelWithPermissions>,
|
channels: Vec<ChannelWithPermissions>,
|
||||||
categories: Vec<CategoryWithPermissions>,
|
categories: Vec<CategoryWithPermissions>,
|
||||||
) -> ServerTreeResponse {
|
) -> ServerTreeResponse {
|
||||||
let order_map: HashMap<Uuid, i64> = orders
|
let order_map: HashMap<(Option<Uuid>, Uuid), i64> = orders
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|order| (order.resource_id, order.order_key))
|
.map(|order| {
|
||||||
|
(
|
||||||
|
(order.parent_category_id, order.resource_id),
|
||||||
|
order.order_key,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut category_channels: HashMap<Uuid, Vec<ChannelWithPermissions>> = HashMap::new();
|
let mut category_channels: HashMap<Uuid, Vec<ChannelWithPermissions>> = HashMap::new();
|
||||||
@@ -88,7 +93,12 @@ pub fn build_server_tree(
|
|||||||
|
|
||||||
// Trier les canaux dans chaque catégorie par leur order_key
|
// Trier les canaux dans chaque catégorie par leur order_key
|
||||||
for chans in category_channels.values_mut() {
|
for chans in category_channels.values_mut() {
|
||||||
chans.sort_by_key(|c| order_map.get(&c.channel.id).copied().unwrap_or(i64::MAX));
|
chans.sort_by_key(|c| {
|
||||||
|
order_map
|
||||||
|
.get(&(c.channel.category_id, c.channel.id))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(i64::MAX)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Préparer les éléments racine (catégories & canaux orphelins) avec leur order_key
|
// Préparer les éléments racine (catégories & canaux orphelins) avec leur order_key
|
||||||
@@ -96,7 +106,7 @@ pub fn build_server_tree(
|
|||||||
|
|
||||||
for cat_with_perm in categories {
|
for cat_with_perm in categories {
|
||||||
let cat_id = cat_with_perm.category.id;
|
let cat_id = cat_with_perm.category.id;
|
||||||
let order_key = order_map.get(&cat_id).copied().unwrap_or(i64::MAX);
|
let order_key = order_map.get(&(None, cat_id)).copied().unwrap_or(i64::MAX);
|
||||||
let chans = category_channels.remove(&cat_id).unwrap_or_default();
|
let chans = category_channels.remove(&cat_id).unwrap_or_default();
|
||||||
|
|
||||||
let cat_perm_bits = cat_with_perm.permissions.map(|p| p.bits()).unwrap_or(0);
|
let cat_perm_bits = cat_with_perm.permissions.map(|p| p.bits()).unwrap_or(0);
|
||||||
@@ -121,7 +131,7 @@ pub fn build_server_tree(
|
|||||||
|
|
||||||
for chan_with_perm in orphan_channels {
|
for chan_with_perm in orphan_channels {
|
||||||
let order_key = order_map
|
let order_key = order_map
|
||||||
.get(&chan_with_perm.channel.id)
|
.get(&(None, chan_with_perm.channel.id))
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(i64::MAX);
|
.unwrap_or(i64::MAX);
|
||||||
let chan_perm_bits = chan_with_perm.permissions.map(|p| p.bits()).unwrap_or(0);
|
let chan_perm_bits = chan_with_perm.permissions.map(|p| p.bits()).unwrap_or(0);
|
||||||
@@ -142,3 +152,92 @@ pub fn build_server_tree(
|
|||||||
|
|
||||||
ServerTreeResponse { items }
|
ServerTreeResponse { items }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::models::{category, channel};
|
||||||
|
use crate::permissions::ChannelPermission;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
fn channel(id: Uuid, category_id: Option<Uuid>, name: &str) -> ChannelWithPermissions {
|
||||||
|
ChannelWithPermissions {
|
||||||
|
channel: channel::Model {
|
||||||
|
id,
|
||||||
|
server_id: Some(Uuid::new_v4()),
|
||||||
|
category_id,
|
||||||
|
channel_type: channel::ChannelType::Text,
|
||||||
|
name: Some(name.to_owned()),
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
},
|
||||||
|
permissions: Some(ChannelPermission::empty()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sorts_channels_by_order_key_within_each_category() {
|
||||||
|
let category_id = Uuid::new_v4();
|
||||||
|
let first_id = Uuid::new_v4();
|
||||||
|
let second_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let category = CategoryWithPermissions {
|
||||||
|
category: category::Model {
|
||||||
|
id: category_id,
|
||||||
|
server_id: Uuid::new_v4(),
|
||||||
|
name: "Discussion".to_owned(),
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
},
|
||||||
|
permissions: Some(ChannelPermission::empty()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let orders = vec![
|
||||||
|
server_item_order::Model {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
server_id: category.category.server_id,
|
||||||
|
resource_id: category_id,
|
||||||
|
resource_type: server_item_order::OrderedResourceType::Category,
|
||||||
|
parent_category_id: None,
|
||||||
|
order_key: 1,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
},
|
||||||
|
server_item_order::Model {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
server_id: category.category.server_id,
|
||||||
|
resource_id: first_id,
|
||||||
|
resource_type: server_item_order::OrderedResourceType::Channel,
|
||||||
|
parent_category_id: Some(category_id),
|
||||||
|
order_key: 2,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
},
|
||||||
|
server_item_order::Model {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
server_id: category.category.server_id,
|
||||||
|
resource_id: second_id,
|
||||||
|
resource_type: server_item_order::OrderedResourceType::Channel,
|
||||||
|
parent_category_id: Some(category_id),
|
||||||
|
order_key: 1,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let response = build_server_tree(
|
||||||
|
orders,
|
||||||
|
vec![
|
||||||
|
channel(first_id, Some(category_id), "second"),
|
||||||
|
channel(second_id, Some(category_id), "first"),
|
||||||
|
],
|
||||||
|
vec![category],
|
||||||
|
);
|
||||||
|
|
||||||
|
let ServerExplorerItemResponse::Category(_, channels) = &response.items[0] else {
|
||||||
|
panic!("expected a category");
|
||||||
|
};
|
||||||
|
assert_eq!(channels[0].name.as_deref(), Some("first"));
|
||||||
|
assert_eq!(channels[1].name.as_deref(), Some("second"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+18
-27
@@ -1,9 +1,7 @@
|
|||||||
use crate::models::category;
|
use crate::models::category;
|
||||||
use crate::models::server_item_order::{self, OrderedResourceType};
|
use crate::models::server_item_order::OrderedResourceType;
|
||||||
use crate::services::ServicesContext;
|
use crate::services::ServicesContext;
|
||||||
use sea_orm::{
|
use sea_orm::{ActiveModelTrait, EntityTrait, Set, TransactionTrait};
|
||||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set, TransactionTrait,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -24,6 +22,13 @@ impl CategoryService {
|
|||||||
) -> Result<category::Model, anyhow::Error> {
|
) -> Result<category::Model, anyhow::Error> {
|
||||||
let db = &self.service_context.repositories.server.context.db;
|
let db = &self.service_context.repositories.server.context.db;
|
||||||
let event_bus = &self.service_context.event_bus;
|
let event_bus = &self.service_context.event_bus;
|
||||||
|
let server_order = self
|
||||||
|
.service_context
|
||||||
|
.services
|
||||||
|
.get()
|
||||||
|
.expect("services initialized")
|
||||||
|
.server_order
|
||||||
|
.clone();
|
||||||
|
|
||||||
let cat = db
|
let cat = db
|
||||||
.transaction::<_, category::Model, anyhow::Error>(|txn| {
|
.transaction::<_, category::Model, anyhow::Error>(|txn| {
|
||||||
@@ -35,25 +40,9 @@ impl CategoryService {
|
|||||||
};
|
};
|
||||||
let cat = active.insert(txn).await?;
|
let cat = active.insert(txn).await?;
|
||||||
|
|
||||||
let max_order: Option<i64> = server_item_order::Entity::find()
|
server_order
|
||||||
.filter(server_item_order::Column::ServerId.eq(server_id))
|
.append(txn, server_id, cat.id, OrderedResourceType::Category, None)
|
||||||
.filter(server_item_order::Column::ParentCategoryId.is_null())
|
.await?;
|
||||||
.select_only()
|
|
||||||
.column_as(server_item_order::Column::OrderKey.max(), "max_key")
|
|
||||||
.into_tuple::<Option<i64>>()
|
|
||||||
.one(txn)
|
|
||||||
.await?
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let order_item = server_item_order::ActiveModel {
|
|
||||||
server_id: Set(server_id),
|
|
||||||
resource_id: Set(cat.id),
|
|
||||||
resource_type: Set(OrderedResourceType::Category),
|
|
||||||
parent_category_id: Set(None),
|
|
||||||
order_key: Set(max_order.unwrap_or(0) + 1),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
order_item.insert(txn).await?;
|
|
||||||
|
|
||||||
Ok(cat)
|
Ok(cat)
|
||||||
})
|
})
|
||||||
@@ -98,10 +87,12 @@ impl CategoryService {
|
|||||||
|
|
||||||
let txn = db.begin().await?;
|
let txn = db.begin().await?;
|
||||||
|
|
||||||
server_item_order::Entity::delete_many()
|
self.service_context
|
||||||
.filter(server_item_order::Column::ResourceId.eq(id))
|
.services
|
||||||
.filter(server_item_order::Column::ResourceType.eq(OrderedResourceType::Category))
|
.get()
|
||||||
.exec(&txn)
|
.expect("services initialized")
|
||||||
|
.server_order
|
||||||
|
.remove(&txn, id, OrderedResourceType::Category)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let res = category::Entity::delete_by_id(id).exec(&txn).await?;
|
let res = category::Entity::delete_by_id(id).exec(&txn).await?;
|
||||||
|
|||||||
+35
-43
@@ -1,10 +1,11 @@
|
|||||||
use crate::domain::dto::channel::{CreateChannelRequest, UpdateChannelRequest};
|
use crate::domain::dto::channel::{CreateChannelRequest, UpdateChannelRequest};
|
||||||
|
use crate::models::server_item_order::OrderedResourceType;
|
||||||
use crate::models::{channel, role};
|
use crate::models::{channel, role};
|
||||||
use crate::permissions::PermissionSet;
|
use crate::permissions::PermissionSet;
|
||||||
use crate::services::ServicesContext;
|
use crate::services::ServicesContext;
|
||||||
use crate::services::permission::PermissionService;
|
use crate::services::permission::PermissionService;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set, TransactionTrait,
|
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -25,6 +26,13 @@ impl ChannelService {
|
|||||||
) -> Result<channel::Model, anyhow::Error> {
|
) -> Result<channel::Model, anyhow::Error> {
|
||||||
let db = &self.service_context.repositories.server.context.db;
|
let db = &self.service_context.repositories.server.context.db;
|
||||||
let event_bus = &self.service_context.event_bus;
|
let event_bus = &self.service_context.event_bus;
|
||||||
|
let server_order = self
|
||||||
|
.service_context
|
||||||
|
.services
|
||||||
|
.get()
|
||||||
|
.expect("services initialized")
|
||||||
|
.server_order
|
||||||
|
.clone();
|
||||||
|
|
||||||
let channel = db
|
let channel = db
|
||||||
.transaction::<_, channel::Model, anyhow::Error>(|txn| {
|
.transaction::<_, channel::Model, anyhow::Error>(|txn| {
|
||||||
@@ -42,35 +50,15 @@ impl ChannelService {
|
|||||||
|
|
||||||
// 2. Si server_id est présent, enregistrement de l'ordre d'affichage
|
// 2. Si server_id est présent, enregistrement de l'ordre d'affichage
|
||||||
if let Some(server_id) = payload.server_id {
|
if let Some(server_id) = payload.server_id {
|
||||||
let max_order: Option<i64> =
|
server_order
|
||||||
crate::models::server_item_order::Entity::find()
|
.append(
|
||||||
.filter(
|
txn,
|
||||||
crate::models::server_item_order::Column::ServerId
|
server_id,
|
||||||
.eq(server_id),
|
channel.id,
|
||||||
|
OrderedResourceType::Channel,
|
||||||
|
payload.category_id,
|
||||||
)
|
)
|
||||||
.select_only()
|
.await?;
|
||||||
.column_as(
|
|
||||||
crate::models::server_item_order::Column::OrderKey.max(),
|
|
||||||
"max_key",
|
|
||||||
)
|
|
||||||
.into_tuple::<Option<i64>>()
|
|
||||||
.one(txn)
|
|
||||||
.await?
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let next_order = max_order.unwrap_or(0) + 1;
|
|
||||||
|
|
||||||
let order_item = crate::models::server_item_order::ActiveModel {
|
|
||||||
server_id: Set(server_id),
|
|
||||||
resource_id: Set(channel.id),
|
|
||||||
resource_type: Set(
|
|
||||||
crate::models::server_item_order::OrderedResourceType::Channel,
|
|
||||||
),
|
|
||||||
parent_category_id: Set(payload.category_id),
|
|
||||||
order_key: Set(next_order),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
order_item.insert(txn).await?;
|
|
||||||
|
|
||||||
// 3. Attribution des permissions par défaut au rôle par défaut
|
// 3. Attribution des permissions par défaut au rôle par défaut
|
||||||
if let Some(default_role) = role::Entity::find()
|
if let Some(default_role) = role::Entity::find()
|
||||||
@@ -130,17 +118,19 @@ impl ChannelService {
|
|||||||
|
|
||||||
let channel = active.update(&txn).await?;
|
let channel = active.update(&txn).await?;
|
||||||
|
|
||||||
// Update server_item_order parent_category_id or server_id if needed
|
self.service_context
|
||||||
if let Some(server_id) = channel.server_id {
|
.services
|
||||||
crate::models::server_item_order::Entity::update_many()
|
.get()
|
||||||
.set(crate::models::server_item_order::ActiveModel {
|
.expect("services initialized")
|
||||||
parent_category_id: Set(channel.category_id),
|
.server_order
|
||||||
..Default::default()
|
.sync(
|
||||||
})
|
&txn,
|
||||||
.filter(crate::models::server_item_order::Column::ResourceId.eq(channel.id))
|
channel.id,
|
||||||
.exec(&txn)
|
OrderedResourceType::Channel,
|
||||||
|
channel.server_id,
|
||||||
|
channel.category_id,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
|
||||||
|
|
||||||
txn.commit().await?;
|
txn.commit().await?;
|
||||||
|
|
||||||
@@ -155,10 +145,12 @@ impl ChannelService {
|
|||||||
|
|
||||||
let txn = db.begin().await?;
|
let txn = db.begin().await?;
|
||||||
|
|
||||||
// Delete associated order records
|
self.service_context
|
||||||
crate::models::server_item_order::Entity::delete_many()
|
.services
|
||||||
.filter(crate::models::server_item_order::Column::ResourceId.eq(id))
|
.get()
|
||||||
.exec(&txn)
|
.expect("services initialized")
|
||||||
|
.server_order
|
||||||
|
.remove(&txn, id, OrderedResourceType::Channel)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let res = channel::Entity::delete_by_id(id).exec(&txn).await?;
|
let res = channel::Entity::delete_by_id(id).exec(&txn).await?;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
use crate::models::server_item_order::{self, OrderedResourceType};
|
||||||
use crate::services::ServicesContext;
|
use crate::services::ServicesContext;
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, QuerySelect, Set,
|
||||||
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ServerOrderService {
|
pub struct ServerOrderService {
|
||||||
@@ -11,9 +16,303 @@ impl ServerOrderService {
|
|||||||
Self { service_context }
|
Self { service_context }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn append(
|
||||||
|
&self,
|
||||||
|
txn: &DatabaseTransaction,
|
||||||
|
server_id: Uuid,
|
||||||
|
resource_id: Uuid,
|
||||||
|
resource_type: OrderedResourceType,
|
||||||
|
parent_category_id: Option<Uuid>,
|
||||||
|
) -> Result<server_item_order::Model, anyhow::Error> {
|
||||||
|
self.sync(
|
||||||
|
txn,
|
||||||
|
resource_id,
|
||||||
|
resource_type,
|
||||||
|
Some(server_id),
|
||||||
|
parent_category_id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Unable to create server order"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn sync(
|
||||||
|
&self,
|
||||||
|
txn: &DatabaseTransaction,
|
||||||
|
resource_id: Uuid,
|
||||||
|
resource_type: OrderedResourceType,
|
||||||
|
server_id: Option<Uuid>,
|
||||||
|
parent_category_id: Option<Uuid>,
|
||||||
|
) -> Result<Option<server_item_order::Model>, anyhow::Error> {
|
||||||
|
let existing = server_item_order::Entity::find()
|
||||||
|
.filter(server_item_order::Column::ResourceId.eq(resource_id))
|
||||||
|
.filter(server_item_order::Column::ResourceType.eq(resource_type))
|
||||||
|
.one(txn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let Some(server_id) = server_id else {
|
||||||
|
if let Some(order) = existing {
|
||||||
|
server_item_order::Entity::delete_by_id(order.id)
|
||||||
|
.exec(txn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let order_key = match existing.as_ref() {
|
||||||
|
Some(order)
|
||||||
|
if order.server_id == server_id
|
||||||
|
&& order.parent_category_id == parent_category_id =>
|
||||||
|
{
|
||||||
|
order.order_key
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.next_order_key(txn, server_id, parent_category_id)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let order = if let Some(order) = existing {
|
||||||
|
let mut active: server_item_order::ActiveModel = order.into();
|
||||||
|
active.server_id = Set(server_id);
|
||||||
|
active.parent_category_id = Set(parent_category_id);
|
||||||
|
active.order_key = Set(order_key);
|
||||||
|
active.update(txn).await?
|
||||||
|
} else {
|
||||||
|
server_item_order::ActiveModel {
|
||||||
|
server_id: Set(server_id),
|
||||||
|
resource_id: Set(resource_id),
|
||||||
|
resource_type: Set(resource_type),
|
||||||
|
parent_category_id: Set(parent_category_id),
|
||||||
|
order_key: Set(order_key),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(txn)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(order))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn remove(
|
||||||
|
&self,
|
||||||
|
txn: &DatabaseTransaction,
|
||||||
|
resource_id: Uuid,
|
||||||
|
resource_type: OrderedResourceType,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
server_item_order::Entity::delete_many()
|
||||||
|
.filter(server_item_order::Column::ResourceId.eq(resource_id))
|
||||||
|
.filter(server_item_order::Column::ResourceType.eq(resource_type))
|
||||||
|
.exec(txn)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn next_order_key(
|
||||||
|
&self,
|
||||||
|
txn: &DatabaseTransaction,
|
||||||
|
server_id: Uuid,
|
||||||
|
parent_category_id: Option<Uuid>,
|
||||||
|
) -> Result<i64, anyhow::Error> {
|
||||||
|
let mut query = server_item_order::Entity::find()
|
||||||
|
.filter(server_item_order::Column::ServerId.eq(server_id));
|
||||||
|
query = match parent_category_id {
|
||||||
|
Some(category_id) => {
|
||||||
|
query.filter(server_item_order::Column::ParentCategoryId.eq(category_id))
|
||||||
|
}
|
||||||
|
None => query.filter(server_item_order::Column::ParentCategoryId.is_null()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_order = query
|
||||||
|
.select_only()
|
||||||
|
.column_as(server_item_order::Column::OrderKey.max(), "max_key")
|
||||||
|
.into_tuple::<Option<i64>>()
|
||||||
|
.one(txn)
|
||||||
|
.await?
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(max_order + 1)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn exemple_call_other_service(&self) {
|
pub async fn exemple_call_other_service(&self) {
|
||||||
if let Some(services) = self.service_context.services.get() {
|
if let Some(services) = self.service_context.services.get() {
|
||||||
services.permission_sync.start_listen_event().await;
|
services.permission_sync.start_listen_event().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::models::{category, server};
|
||||||
|
use crate::repositories::Repositories;
|
||||||
|
use event_bus::EventBus;
|
||||||
|
use migration::{Migrator, MigratorTrait};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, EntityTrait, Set,
|
||||||
|
TransactionTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn service() -> ServerOrderService {
|
||||||
|
let db = Database::connect("sqlite::memory:").await.unwrap();
|
||||||
|
Migrator::up(&db, None).await.unwrap();
|
||||||
|
|
||||||
|
let context = Arc::new(ServicesContext {
|
||||||
|
repositories: Arc::new(Repositories::new(db)),
|
||||||
|
event_bus: Arc::new(EventBus::new()),
|
||||||
|
services: std::sync::OnceLock::new(),
|
||||||
|
});
|
||||||
|
ServerOrderService::new(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn seed_server(db: &DatabaseConnection, server_id: Uuid) {
|
||||||
|
server::ActiveModel {
|
||||||
|
id: Set(server_id),
|
||||||
|
name: Set("test".to_owned()),
|
||||||
|
is_default: Set(false),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sync_uses_scoped_keys_and_preserves_position() {
|
||||||
|
let service = service().await;
|
||||||
|
let db = &service.service_context.repositories.server.context.db;
|
||||||
|
let server_id = Uuid::new_v4();
|
||||||
|
let category_id = Uuid::new_v4();
|
||||||
|
let category_resource_id = Uuid::new_v4();
|
||||||
|
let root_channel_id = Uuid::new_v4();
|
||||||
|
let nested_channel_id = Uuid::new_v4();
|
||||||
|
seed_server(db, server_id).await;
|
||||||
|
category::ActiveModel {
|
||||||
|
id: Set(category_id),
|
||||||
|
server_id: Set(server_id),
|
||||||
|
name: Set("category".to_owned()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let txn = db.begin().await.unwrap();
|
||||||
|
service
|
||||||
|
.append(
|
||||||
|
&txn,
|
||||||
|
server_id,
|
||||||
|
category_resource_id,
|
||||||
|
OrderedResourceType::Category,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
service
|
||||||
|
.append(
|
||||||
|
&txn,
|
||||||
|
server_id,
|
||||||
|
root_channel_id,
|
||||||
|
OrderedResourceType::Channel,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
service
|
||||||
|
.append(
|
||||||
|
&txn,
|
||||||
|
server_id,
|
||||||
|
nested_channel_id,
|
||||||
|
OrderedResourceType::Channel,
|
||||||
|
Some(category_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
txn.commit().await.unwrap();
|
||||||
|
|
||||||
|
let txn = db.begin().await.unwrap();
|
||||||
|
let unchanged = service
|
||||||
|
.sync(
|
||||||
|
&txn,
|
||||||
|
nested_channel_id,
|
||||||
|
OrderedResourceType::Channel,
|
||||||
|
Some(server_id),
|
||||||
|
Some(category_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(unchanged.order_key, 1);
|
||||||
|
|
||||||
|
let moved = service
|
||||||
|
.sync(
|
||||||
|
&txn,
|
||||||
|
nested_channel_id,
|
||||||
|
OrderedResourceType::Channel,
|
||||||
|
Some(server_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(moved.order_key, 3);
|
||||||
|
txn.commit().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sync_none_removes_order_and_rollback_is_atomic() {
|
||||||
|
let service = service().await;
|
||||||
|
let db = &service.service_context.repositories.server.context.db;
|
||||||
|
let server_id = Uuid::new_v4();
|
||||||
|
let resource_id = Uuid::new_v4();
|
||||||
|
seed_server(db, server_id).await;
|
||||||
|
|
||||||
|
let txn = db.begin().await.unwrap();
|
||||||
|
service
|
||||||
|
.append(
|
||||||
|
&txn,
|
||||||
|
server_id,
|
||||||
|
resource_id,
|
||||||
|
OrderedResourceType::Channel,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
txn.rollback().await.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
server_item_order::Entity::find()
|
||||||
|
.filter(server_item_order::Column::ResourceId.eq(resource_id))
|
||||||
|
.one(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
|
let txn = db.begin().await.unwrap();
|
||||||
|
service
|
||||||
|
.append(
|
||||||
|
&txn,
|
||||||
|
server_id,
|
||||||
|
resource_id,
|
||||||
|
OrderedResourceType::Channel,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
service
|
||||||
|
.sync(&txn, resource_id, OrderedResourceType::Channel, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
txn.commit().await.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
server_item_order::Entity::find()
|
||||||
|
.filter(server_item_order::Column::ResourceId.eq(resource_id))
|
||||||
|
.one(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user