init
This commit is contained in:
@@ -7,6 +7,7 @@ import {useRoute} from 'vue-router'
|
|||||||
import CreateChannelDialog from '@/components/channel/CreateChannelDialog.vue'
|
import CreateChannelDialog from '@/components/channel/CreateChannelDialog.vue'
|
||||||
import {type MenuItem, useContextMenu} from '@/composables/useContextMenu'
|
import {type MenuItem, useContextMenu} from '@/composables/useContextMenu'
|
||||||
import {useUserStore} from "@/stores/user.ts";
|
import {useUserStore} from "@/stores/user.ts";
|
||||||
|
import {useServerStore} from "@/stores/server.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
serverId: string
|
serverId: string
|
||||||
@@ -17,8 +18,8 @@ const route = useRoute()
|
|||||||
const channelStore = useChannelStore()
|
const channelStore = useChannelStore()
|
||||||
const categoryStore = useCategoryStore()
|
const categoryStore = useCategoryStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const {channels} = storeToRefs(channelStore)
|
const serverStore = useServerStore()
|
||||||
const {categories} = storeToRefs(categoryStore)
|
const {currentTree} = storeToRefs(serverStore)
|
||||||
const {openContextMenu} = useContextMenu()
|
const {openContextMenu} = useContextMenu()
|
||||||
|
|
||||||
|
|
||||||
@@ -29,9 +30,8 @@ const loadServerData = async (targetServerId: string) => {
|
|||||||
userStore.reset()
|
userStore.reset()
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
channelStore.fetchChannels(targetServerId),
|
userStore.fetchUsers(targetServerId),
|
||||||
categoryStore.fetchCategories(targetServerId),
|
serverStore.fetchServerTree(targetServerId)
|
||||||
userStore.fetchUsers(targetServerId)
|
|
||||||
])
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load server-scoped channels and categories:', error)
|
console.error('Failed to load server-scoped channels and categories:', error)
|
||||||
@@ -50,7 +50,24 @@ watch(
|
|||||||
|
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
|
|
||||||
// Right click menu
|
// Right click menu (sidebar)
|
||||||
|
function onSidebarContextMenu(event: MouseEvent) {
|
||||||
|
const menuItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Nouveau canal',
|
||||||
|
icon: 'mdi-plus',
|
||||||
|
action: () => console.log('Nouveau canal'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nouvelle catégorie',
|
||||||
|
icon: 'mdi-folder-plus',
|
||||||
|
action: () => console.log('Nouvelle catégorie'),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
openContextMenu(event, menuItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right click menu (channel)
|
||||||
async function openEditDialog(channel: any) {
|
async function openEditDialog(channel: any) {
|
||||||
console.log("edit dialog clicked")
|
console.log("edit dialog clicked")
|
||||||
}
|
}
|
||||||
@@ -80,10 +97,12 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
|||||||
]
|
]
|
||||||
openContextMenu(event, menuItems);
|
openContextMenu(event, menuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-navigation-drawer width="244">
|
<v-navigation-drawer width="244" @contextmenu="onSidebarContextMenu">
|
||||||
<v-sheet
|
<v-sheet
|
||||||
color="grey-lighten-5"
|
color="grey-lighten-5"
|
||||||
height="128"
|
height="128"
|
||||||
@@ -100,15 +119,34 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
|||||||
New Channel
|
New Channel
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-list>
|
<v-list density="compact">
|
||||||
<v-list-item
|
<template v-for="(item, index) in currentTree" :key="index">
|
||||||
v-for="channel in channels"
|
<!-- Catégorie et ses canaux enfants -->
|
||||||
:key="channel.id"
|
<v-list-group v-if="'Category' in item" :value="item.Category[0].id">
|
||||||
:title="channel.name"
|
<template #activator="{ props: groupProps }">
|
||||||
:to="`/server/${serverId}/channel/${channel.id}`"
|
<v-list-item :title="item.Category[0].name" v-bind="groupProps"/>
|
||||||
link
|
</template>
|
||||||
@contextmenu="onChannelContextMenu($event, channel)"
|
|
||||||
></v-list-item>
|
<v-list-item
|
||||||
|
v-for="channel in item.Category[1]"
|
||||||
|
:key="channel.id"
|
||||||
|
:title="channel.name"
|
||||||
|
:to="`/server/${serverId}/channel/${channel.id}`"
|
||||||
|
link
|
||||||
|
@contextmenu="onChannelContextMenu($event, channel)"
|
||||||
|
/>
|
||||||
|
</v-list-group>
|
||||||
|
|
||||||
|
<!-- Canal orphelin (racine) -->
|
||||||
|
<v-list-item
|
||||||
|
v-else-if="'Channel' in item"
|
||||||
|
:key="item.Channel.id"
|
||||||
|
:title="item.Channel.name"
|
||||||
|
:to="`/server/${serverId}/channel/${item.Channel.id}`"
|
||||||
|
link
|
||||||
|
@contextmenu="onChannelContextMenu($event, item.Channel)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {useApi} from "@/composables/useApi.ts";
|
import {useApi} from "@/composables/useApi.ts";
|
||||||
|
|
||||||
|
interface Category {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
server_id?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export const useCategoryStore = defineStore("category", {
|
export const useCategoryStore = defineStore("category", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
categories: []
|
categories: [] as Category[]
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async fetchCategories(serverId?: string) {
|
async fetchCategories(serverId?: string) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ interface Channel {
|
|||||||
channel_type: string
|
channel_type: string
|
||||||
server_id?: string | null
|
server_id?: string | null
|
||||||
category_id?: string | null
|
category_id?: string | null
|
||||||
position: number
|
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {useApi} from "@/composables/useApi.ts";
|
import {useApi} from "@/composables/useApi.ts";
|
||||||
|
import {useChannelStore} from "@/stores/channel.ts";
|
||||||
|
import {useCategoryStore} from "@/stores/category.ts";
|
||||||
|
|
||||||
interface Server {
|
interface Server {
|
||||||
id: string
|
id: string
|
||||||
@@ -13,7 +15,8 @@ export const useServerStore = defineStore("server", {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
servers: [] as Server[],
|
servers: [] as Server[],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null as string | null
|
error: null as string | null,
|
||||||
|
currentTree: [] as any[],
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async fetchServers() {
|
async fetchServers() {
|
||||||
@@ -41,6 +44,36 @@ export const useServerStore = defineStore("server", {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async fetchServerTree(serverId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
const channelStore = useChannelStore();
|
||||||
|
const categoryStore = useCategoryStore();
|
||||||
|
|
||||||
|
const response = await api.get(`/servers/${serverId}/tree`);
|
||||||
|
const tree = await response.json(); // { items: [...] }
|
||||||
|
|
||||||
|
this.currentTree = tree.items;
|
||||||
|
|
||||||
|
// Extraction à plat pour alimenter les stores spécialisés
|
||||||
|
const extractedCategories: any[] = [];
|
||||||
|
const extractedChannels: any[] = [];
|
||||||
|
|
||||||
|
for (const item of tree.items) {
|
||||||
|
if ("Category" in item) {
|
||||||
|
const [category, channels] = item.Category;
|
||||||
|
extractedCategories.push(category);
|
||||||
|
extractedChannels.push(...channels);
|
||||||
|
} else if ("Channel" in item) {
|
||||||
|
extractedChannels.push(item.Channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Population / Synchro des stores individuels
|
||||||
|
categoryStore.categories = extractedCategories;
|
||||||
|
channelStore.channels = extractedChannels;
|
||||||
|
|
||||||
|
return tree.items;
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.servers = [];
|
this.servers = [];
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use crate::domain::dto::category::CategoryResponse;
|
||||||
|
use crate::domain::dto::channel::ChannelResponse;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
@@ -50,3 +52,14 @@ pub struct ServerRolePermissionResponse {
|
|||||||
pub role_id: Uuid,
|
pub role_id: Uuid,
|
||||||
pub permissions: u64,
|
pub permissions: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub enum ServerExplorerItemResponse {
|
||||||
|
Category(CategoryResponse, Vec<ChannelResponse>),
|
||||||
|
Channel(ChannelResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ServerTreeResponse {
|
||||||
|
pub items: Vec<ServerExplorerItemResponse>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use utoipa::{Modify, OpenApi};
|
|||||||
server::handlers::create,
|
server::handlers::create,
|
||||||
server::handlers::update,
|
server::handlers::update,
|
||||||
server::handlers::delete,
|
server::handlers::delete,
|
||||||
|
server::handlers::get_tree,
|
||||||
category::handlers::get_all,
|
category::handlers::get_all,
|
||||||
category::handlers::get_by_id,
|
category::handlers::get_by_id,
|
||||||
category::handlers::create,
|
category::handlers::create,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::core::state::AppState;
|
use crate::core::state::AppState;
|
||||||
use crate::http::context::Superuser;
|
|
||||||
use crate::http::error::HTTPError;
|
|
||||||
use crate::repositories::types::{ChannelFilter, ServerTree};
|
|
||||||
use crate::domain::dto::server::{
|
use crate::domain::dto::server::{
|
||||||
CreateServerRequest, ServerResponse, ServerRolePermissionResponse,
|
CreateServerRequest, ServerResponse, ServerRolePermissionResponse, ServerTreeResponse,
|
||||||
ServerUserPermissionResponse, SetServerPermissionRequest, UpdateServerRequest,
|
ServerUserPermissionResponse, SetServerPermissionRequest, UpdateServerRequest,
|
||||||
};
|
};
|
||||||
|
use crate::http::context::Superuser;
|
||||||
|
use crate::http::error::HTTPError;
|
||||||
|
use crate::repositories::types::ChannelFilter;
|
||||||
use crate::routes::server::mapper;
|
use crate::routes::server::mapper;
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
@@ -83,7 +83,11 @@ pub async fn create(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<CreateServerRequest>,
|
Json(payload): Json<CreateServerRequest>,
|
||||||
) -> Result<(StatusCode, Json<ServerResponse>), HTTPError> {
|
) -> Result<(StatusCode, Json<ServerResponse>), HTTPError> {
|
||||||
let server = state.services.server.create_server(payload.name, payload.is_default).await?;
|
let server = state
|
||||||
|
.services
|
||||||
|
.server
|
||||||
|
.create_server(payload.name, payload.is_default)
|
||||||
|
.await?;
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
Json(mapper::server_model_to_server_response(server)),
|
Json(mapper::server_model_to_server_response(server)),
|
||||||
@@ -122,7 +126,11 @@ pub async fn update(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
let server = state.services.server.update_server(id, payload.name, payload.is_default).await?;
|
let server = state
|
||||||
|
.services
|
||||||
|
.server
|
||||||
|
.update_server(id, payload.name, payload.is_default)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(Json(mapper::server_model_to_server_response(server)))
|
Ok(Json(mapper::server_model_to_server_response(server)))
|
||||||
}
|
}
|
||||||
@@ -364,39 +372,42 @@ pub async fn remove_role_permission(
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/servers/{id}/tree",
|
path = "/servers/{server_id}/tree",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Arborescence du serveur récupérée avec succès"),
|
(status = 200, description = "Arborescence du serveur récupérée avec succès"),
|
||||||
(status = 404, description = "Serveur introuvable"),
|
(status = 404, description = "Serveur introuvable"),
|
||||||
(status = 500, description = "Erreur interne du serveur")
|
(status = 500, description = "Erreur interne du serveur")
|
||||||
),
|
),
|
||||||
params(
|
params(
|
||||||
("id" = Uuid, Path, description = "ID du serveur")
|
("server_id" = Uuid, Path, description = "ID du serveur")
|
||||||
),
|
),
|
||||||
tag = "Servers"
|
tag = "Servers"
|
||||||
)]
|
)]
|
||||||
pub async fn get_server_tree(
|
pub async fn get_tree(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<Uuid>,
|
Path(server_id): Path<Uuid>,
|
||||||
) -> Result<Json<ServerTree>, HTTPError> {
|
) -> Result<Json<ServerTreeResponse>, HTTPError> {
|
||||||
// Vérifier l'existence du serveur
|
// Vérifier l'existence du serveur
|
||||||
state
|
state
|
||||||
.repositories
|
.repositories
|
||||||
.server
|
.server
|
||||||
.get_by_id(id)
|
.get_by_id(server_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
// Exécution parallèle des 3 requêtes via leurs repositories respectifs
|
// Exécution parallèle des 3 requêtes via leurs repositories respectifs
|
||||||
let (orders, channels, categories) = tokio::try_join!(
|
let (orders, channels, categories) = tokio::try_join!(
|
||||||
state.repositories.server_item_order.get_by_server(id),
|
state
|
||||||
|
.repositories
|
||||||
|
.server_item_order
|
||||||
|
.get_by_server(server_id),
|
||||||
state.repositories.channel.filter(ChannelFilter {
|
state.repositories.channel.filter(ChannelFilter {
|
||||||
server_id: Some(id)
|
server_id: Some(server_id)
|
||||||
}),
|
}),
|
||||||
state.repositories.category.get_by_server(id),
|
state.repositories.category.get_by_server(server_id),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Assemblage du ServerTree via le mapper
|
// Assemblage du ServerTreeResponse via le mapper
|
||||||
let tree = mapper::build_server_tree(orders, channels, categories);
|
let tree = mapper::build_server_tree(orders, channels, categories);
|
||||||
|
|
||||||
Ok(Json(tree))
|
Ok(Json(tree))
|
||||||
|
|||||||
+25
-10
@@ -1,11 +1,12 @@
|
|||||||
|
use crate::domain::dto::server::{
|
||||||
|
CreateServerRequest, ServerExplorerItemResponse, ServerResponse, ServerRolePermissionResponse,
|
||||||
|
ServerTreeResponse, ServerUserPermissionResponse, UpdateServerRequest,
|
||||||
|
};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
category, channel, server, server_item_order, server_role_permission, server_user_permission,
|
category, channel, server, server_item_order, server_role_permission, server_user_permission,
|
||||||
};
|
};
|
||||||
use crate::repositories::types::{ServerExplorerItem, ServerTree};
|
use crate::routes::category::mapper::category_model_to_category_response;
|
||||||
use crate::domain::dto::server::{
|
use crate::routes::channel::mapper::channel_model_to_channel_response;
|
||||||
CreateServerRequest, ServerResponse, ServerRolePermissionResponse,
|
|
||||||
ServerUserPermissionResponse, UpdateServerRequest,
|
|
||||||
};
|
|
||||||
use sea_orm::Set;
|
use sea_orm::Set;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -66,7 +67,7 @@ pub fn build_server_tree(
|
|||||||
orders: Vec<server_item_order::Model>,
|
orders: Vec<server_item_order::Model>,
|
||||||
channels: Vec<channel::Model>,
|
channels: Vec<channel::Model>,
|
||||||
categories: Vec<category::Model>,
|
categories: Vec<category::Model>,
|
||||||
) -> ServerTree {
|
) -> ServerTreeResponse {
|
||||||
// Map d'accès rapide : resource_id -> order_key
|
// Map d'accès rapide : resource_id -> order_key
|
||||||
let order_map: HashMap<Uuid, i64> = orders
|
let order_map: HashMap<Uuid, i64> = orders
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -91,18 +92,32 @@ pub fn build_server_tree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
let mut root_items: Vec<(ServerExplorerItem, i64)> = Vec::new();
|
let mut root_items: Vec<(ServerExplorerItemResponse, i64)> = Vec::new();
|
||||||
|
|
||||||
for cat in categories {
|
for cat in categories {
|
||||||
let cat_id = cat.id;
|
let cat_id = cat.id;
|
||||||
let order_key = order_map.get(&cat_id).copied().unwrap_or(i64::MAX);
|
let order_key = order_map.get(&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();
|
||||||
root_items.push((ServerExplorerItem::Category(cat, chans), order_key));
|
|
||||||
|
let cat_response = category_model_to_category_response(cat);
|
||||||
|
let chan_responses = chans
|
||||||
|
.into_iter()
|
||||||
|
.map(channel_model_to_channel_response)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
root_items.push((
|
||||||
|
ServerExplorerItemResponse::Category(cat_response, chan_responses),
|
||||||
|
order_key,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for chan in orphan_channels {
|
for chan in orphan_channels {
|
||||||
let order_key = order_map.get(&chan.id).copied().unwrap_or(i64::MAX);
|
let order_key = order_map.get(&chan.id).copied().unwrap_or(i64::MAX);
|
||||||
root_items.push((ServerExplorerItem::Channel(chan), order_key));
|
let chan_response = channel_model_to_channel_response(chan);
|
||||||
|
root_items.push((
|
||||||
|
ServerExplorerItemResponse::Channel(chan_response),
|
||||||
|
order_key,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tri du niveau racine par order_key
|
// Tri du niveau racine par order_key
|
||||||
@@ -110,5 +125,5 @@ pub fn build_server_tree(
|
|||||||
|
|
||||||
let items = root_items.into_iter().map(|(item, _)| item).collect();
|
let items = root_items.into_iter().map(|(item, _)| item).collect();
|
||||||
|
|
||||||
ServerTree { items }
|
ServerTreeResponse { items }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::handlers;
|
use super::handlers;
|
||||||
use crate::core::state::AppState;
|
use crate::core::state::AppState;
|
||||||
use axum::{routing::get, Router};
|
use axum::{Router, routing::get};
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -11,6 +11,7 @@ pub fn router() -> Router<AppState> {
|
|||||||
.put(handlers::update)
|
.put(handlers::update)
|
||||||
.delete(handlers::delete),
|
.delete(handlers::delete),
|
||||||
)
|
)
|
||||||
|
.route("/servers/{server_id}/tree", get(handlers::get_tree))
|
||||||
.route(
|
.route(
|
||||||
"/servers/{server_id}/permissions/users/{user_id}",
|
"/servers/{server_id}/permissions/users/{user_id}",
|
||||||
get(handlers::get_user_permission)
|
get(handlers::get_user_permission)
|
||||||
|
|||||||
Reference in New Issue
Block a user