This commit is contained in:
2026-08-02 12:07:51 +02:00
parent aa486be6e5
commit bb4e17ba2f
15 changed files with 502 additions and 152 deletions
@@ -65,7 +65,7 @@ async function load() {
if (!props.channel) return
loading.value = true; error.value = null
try {
const [roleResponse] = await Promise.all([api.get('/groups'), users.fetchUsers(props.serverId)])
const [roleResponse] = await Promise.all([api.get(`/roles?server_id=${props.serverId}`), users.fetchUsers(props.serverId)])
if (!roleResponse.ok) throw new Error('Impossible de charger les rôles')
roles.value = (await roleResponse.json() as Role[]).filter(role => role.server_id === props.serverId)
configured.value = await permissionsApi.getChannelPermissions(props.channel.id)
@@ -0,0 +1,155 @@
<script lang="ts" setup>
import {computed, ref, watch} from 'vue'
import {storeToRefs} from 'pinia'
import {useServerStore} from '@/stores/server'
import {useRoleStore} from '@/stores/role'
import {useUserStore} from '@/stores/user'
import {usePermissions} from '@/composables/usePermissions'
import {toServerPermissionMask, type ServerPermissionMask} from '@/types/permissions'
import ServerPermissionEditor from '@/components/permissions/ServerPermissionEditor.vue'
const props = defineProps<{modelValue: boolean; serverId: string; serverName: string}>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const serverStore = useServerStore()
const roleStore = useRoleStore()
const userStore = useUserStore()
const permissionsApi = usePermissions()
const {roles} = storeToRefs(roleStore)
const activeTab = ref('general')
const selectedRoleId = ref<string | null>(null)
const name = ref('')
const loading = ref(false)
const saving = ref(false)
const error = ref<string | null>(null)
const roleForm = ref('')
const rolePermissions = ref<ServerPermissionMask>(toServerPermissionMask(0))
const selectedRole = computed(() => roles.value.find(role => role.id === selectedRoleId.value) || null)
const selectedMembers = computed(() => selectedRoleId.value ? roleStore.members[selectedRoleId.value] || [] : [])
const availableUsers = computed(() => userStore.users.filter(user => !selectedMembers.value.some(member => member.id === user.id)))
const memberToAdd = ref<string | null>(null)
watch(() => props.modelValue, async open => {
if (open) await load()
})
watch(selectedRoleId, async roleId => {
if (!roleId) return
roleForm.value = selectedRole.value?.name || ''
rolePermissions.value = toServerPermissionMask(0)
try {
await roleStore.fetchMembers(roleId)
const permission = await permissionsApi.getServerRolePermission(props.serverId, roleId)
rolePermissions.value = permission.permissions
} catch (e) {
if ((e as Error & {status?: number}).status !== 404) error.value = e instanceof Error ? e.message : 'Erreur de chargement'
}
})
async function load() {
loading.value = true
error.value = null
name.value = props.serverName
try {
await Promise.all([roleStore.fetchRoles(props.serverId), userStore.fetchUsers(props.serverId)])
} catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de chargement' }
finally { loading.value = false }
}
async function saveServer() {
if (!name.value.trim()) return
saving.value = true
try { await serverStore.updateServer(props.serverId, {name: name.value.trim()}) }
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de sauvegarde' }
finally { saving.value = false }
}
async function createRole() {
if (!roleForm.value.trim()) return
try { const role = await roleStore.createRole({server_id: props.serverId, name: roleForm.value.trim()}); selectedRoleId.value = role.id; roleForm.value = '' }
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de création' }
}
async function saveRole() {
if (!selectedRoleId.value || !roleForm.value.trim()) return
try {
await roleStore.updateRole(selectedRoleId.value, {name: roleForm.value.trim()})
await permissionsApi.setServerRolePermission(props.serverId, selectedRoleId.value, rolePermissions.value)
} catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de sauvegarde' }
}
async function deleteRole() {
if (!selectedRoleId.value || selectedRole.value?.is_default) return
try { await roleStore.deleteRole(selectedRoleId.value); selectedRoleId.value = null }
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de suppression' }
}
async function addMember() {
if (!selectedRoleId.value || !memberToAdd.value) return
try { await roleStore.addMember(selectedRoleId.value, memberToAdd.value); memberToAdd.value = null }
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur dajout' }
}
async function savePermissions(value: ServerPermissionMask) {
if (!selectedRoleId.value) return
try { await permissionsApi.setServerRolePermission(props.serverId, selectedRoleId.value, value); rolePermissions.value = value }
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de permissions' }
}
</script>
<template>
<v-dialog :model-value="modelValue" max-width="980" @update:model-value="emit('update:modelValue', $event)">
<v-card min-height="620">
<v-card-title>Paramètres du serveur</v-card-title>
<v-card-text>
<v-alert v-if="error" type="error" density="compact" class="mb-4">{{ error }}</v-alert>
<v-row class="settings-layout" no-gutters>
<v-col cols="12" md="3" class="settings-sidebar">
<v-list density="compact" nav>
<v-list-item title="Général" prepend-icon="mdi-cog" :active="activeTab === 'general'" @click="activeTab = 'general'" />
<v-list-item title="Rôles" prepend-icon="mdi-shield-account" :active="activeTab === 'roles'" @click="activeTab = 'roles'" />
</v-list>
</v-col>
<v-col cols="12" md="9" class="pa-5">
<v-progress-linear v-if="loading" indeterminate class="mb-4" />
<template v-if="activeTab === 'general'">
<div class="text-h6 mb-4">Général</div>
<v-text-field v-model="name" label="Nom du serveur" :disabled="loading" />
<v-btn color="primary" :loading="saving" :disabled="!name.trim()" @click="saveServer">Enregistrer</v-btn>
</template>
<template v-else>
<div class="text-h6 mb-4">Rôles</div>
<v-row>
<v-col cols="12" md="4">
<v-list border density="compact" class="role-list">
<v-list-item v-for="role in roles" :key="role.id" :title="role.name" :active="role.id === selectedRoleId" @click="selectedRoleId = role.id">
<template #append><v-icon v-if="role.is_default" icon="mdi-star" size="small" /></template>
</v-list-item>
</v-list>
<v-text-field v-model="roleForm" class="mt-3" label="Nouveau rôle" hide-details @keyup.enter="createRole" />
<v-btn class="mt-2" block color="primary" variant="tonal" @click="createRole">Ajouter un rôle</v-btn>
</v-col>
<v-col cols="12" md="8">
<template v-if="selectedRole">
<v-text-field v-model="roleForm" label="Nom du rôle" />
<div class="d-flex ga-2 mb-4"><v-btn color="primary" @click="saveRole">Enregistrer</v-btn><v-btn v-if="!selectedRole.is_default" color="error" variant="text" @click="deleteRole">Supprimer</v-btn></div>
<v-select v-model="memberToAdd" :items="availableUsers" item-title="username" item-value="id" label="Ajouter un membre" clearable @update:model-value="addMember" />
<v-list density="compact" border class="mb-4">
<v-list-item v-for="member in selectedMembers" :key="member.id" :title="member.username">
<template #append><v-btn icon="mdi-close" size="small" variant="text" @click="roleStore.removeMember(selectedRoleId!, member.id)" /></template>
</v-list-item>
</v-list>
<ServerPermissionEditor v-model="rolePermissions" title="Permissions du rôle" @save="savePermissions" />
</template>
<v-sheet v-else class="empty-selection d-flex align-center justify-center text-medium-emphasis" border rounded>Sélectionnez un rôle.</v-sheet>
</v-col>
</v-row>
</template>
</v-col>
</v-row>
</v-card-text>
<v-card-actions><v-spacer /><v-btn variant="text" @click="emit('update:modelValue', false)">Fermer</v-btn></v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.settings-layout { min-height: 500px; }
.settings-sidebar { border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); }
.role-list { max-height: 250px; overflow-y: auto; }
.empty-selection { min-height: 400px; }
@media (max-width: 959px) { .settings-sidebar { border-right: 0; border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); } }
</style>
+27 -1
View File
@@ -1,18 +1,24 @@
<script lang="ts" setup>
import {storeToRefs} from 'pinia'
import {useServerStore} from '@/stores/server'
import {useServerStore, type Server} from '@/stores/server'
import {computed, ref, watch} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import ContextMenu from "@/components/ContextMenu.vue";
import UserListDrawer from '@/components/UserListDrawer.vue'
import ServerSettingsDialog from '@/components/server/ServerSettingsDialog.vue'
import {useContextMenu} from '@/composables/useContextMenu'
const serverStore = useServerStore()
const route = useRoute()
const router = useRouter()
const {openContextMenu} = useContextMenu()
const {servers} = storeToRefs(serverStore)
const showUsersDrawer = ref(false)
const showServerSettings = ref(false)
const selectedServerId = ref<string | null>(null)
const selectedServerName = ref('')
const isServerContext = computed(() => Boolean(route.params.serverId))
watch(isServerContext, (isActive) => {
@@ -82,6 +88,18 @@ const getServerColor = (str: string): string => {
return `hsl(${hue}, 60%, 45%)`
}
function onServerContextMenu(event: MouseEvent, server: Server) {
openContextMenu(event, [{
label: 'Gérer le serveur',
icon: 'mdi-cog',
action: () => {
selectedServerId.value = server.id
selectedServerName.value = server.name
showServerSettings.value = true
},
}])
}
</script>
<template>
@@ -125,6 +143,7 @@ const getServerColor = (str: string): string => {
v-for="server in servers"
:key="server.id"
:to="`/server/${server.id}`"
@contextmenu="onServerContextMenu($event, server)"
>
<v-avatar
:style="{ backgroundColor: getServerColor(server.name) }"
@@ -150,6 +169,13 @@ const getServerColor = (str: string): string => {
v-model="showUsersDrawer"
/>
<ServerSettingsDialog
v-if="selectedServerId"
v-model="showServerSettings"
:server-id="selectedServerId"
:server-name="selectedServerName"
/>
<router-view/>
<!-- Menu contextuel global -->
<ContextMenu/>
+13 -6
View File
@@ -2,7 +2,7 @@
import {storeToRefs} from 'pinia'
import {useChannelStore} from '@/stores/channel'
import {useCategoryStore} from '@/stores/category'
import {ref, watch} from 'vue'
import {computed, ref, watch} from 'vue'
import {useRoute} from 'vue-router'
import CreateChannelDialog from '@/components/channel/CreateChannelDialog.vue'
import CreateCategoryDialog from '@/components/category/CreateCategoryDialog.vue'
@@ -11,6 +11,7 @@ import {useUserStore} from "@/stores/user.ts";
import {useServerStore} from "@/stores/server.ts";
import ChannelPermissionsDialog from '@/components/permissions/ChannelPermissionsDialog.vue'
import {useAuthStore} from '@/stores/auth'
import ServerSettingsDialog from '@/components/server/ServerSettingsDialog.vue'
const props = defineProps<{
serverId: string
@@ -27,6 +28,8 @@ const {openContextMenu} = useContextMenu()
const authStore = useAuthStore()
const showPermissionsDialog = ref(false)
const selectedChannel = ref<any | null>(null)
const showServerSettings = ref(false)
const serverName = computed(() => serverStore.servers.find(server => server.id === props.serverId)?.name || 'Serveur')
const loadServerData = async (targetServerId: string) => {
@@ -149,11 +152,9 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
<template>
<v-navigation-drawer width="244" @contextmenu="onSidebarContextMenu">
<v-sheet
color="grey-lighten-5"
height="128"
width="100%"
></v-sheet>
<v-sheet color="grey-lighten-5" height="128" width="100%" class="pa-3">
<v-btn block variant="text" prepend-icon="mdi-cog" @click="showServerSettings = true">Gérer le serveur</v-btn>
</v-sheet>
<v-list
v-model:opened="openedCategories"
@@ -213,6 +214,12 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
:server-id="serverId"
/>
<ServerSettingsDialog
v-model="showServerSettings"
:server-id="serverId"
:server-name="serverName"
/>
<v-main>
<router-view/>
</v-main>
+62
View File
@@ -0,0 +1,62 @@
import {defineStore} from 'pinia'
import {useApi} from '@/composables/useApi'
import type {Role} from '@/types/role'
import type {User} from '@/types/user'
export const useRoleStore = defineStore('role', {
state: () => ({
roles: [] as Role[],
members: {} as Record<string, User[]>,
loading: false,
}),
actions: {
async fetchRoles(serverId: string) {
const response = await useApi().get(`/roles?server_id=${serverId}`)
if (!response.ok) throw new Error('Impossible de charger les rôles')
this.roles = await response.json()
return this.roles
},
async createRole(payload: {server_id: string; name: string; is_default?: boolean}) {
const response = await useApi().post('/roles', payload)
if (!response.ok) throw new Error('Impossible de créer le rôle')
const role = await response.json()
this.roles.push(role)
return role
},
async updateRole(id: string, payload: {name: string; is_default?: boolean}) {
const response = await useApi().put(`/roles/${id}`, payload)
if (!response.ok) throw new Error('Impossible de modifier le rôle')
const role = await response.json()
const index = this.roles.findIndex(item => item.id === id)
if (index >= 0) this.roles[index] = role
return role
},
async deleteRole(id: string) {
const response = await useApi().delete(`/roles/${id}`)
if (!response.ok) throw new Error('Impossible de supprimer le rôle')
this.roles = this.roles.filter(role => role.id !== id)
delete this.members[id]
},
async fetchMembers(roleId: string) {
const response = await useApi().get(`/roles/${roleId}/members`)
if (!response.ok) throw new Error('Impossible de charger les membres')
this.members[roleId] = await response.json()
return this.members[roleId]
},
async addMember(roleId: string, userId: string) {
const response = await useApi().put(`/roles/${roleId}/members/${userId}`)
if (!response.ok) throw new Error('Impossible dajouter le membre')
await this.fetchMembers(roleId)
},
async removeMember(roleId: string, userId: string) {
const response = await useApi().delete(`/roles/${roleId}/members/${userId}`)
if (!response.ok) throw new Error('Impossible de retirer le membre')
await this.fetchMembers(roleId)
},
reset() {
this.roles = []
this.members = {}
this.loading = false
},
},
})
+18 -2
View File
@@ -3,7 +3,7 @@ import {useApi} from "@/composables/useApi.ts";
import {useChannelStore} from "@/stores/channel.ts";
import {useCategoryStore} from "@/stores/category.ts";
interface Server {
export interface Server {
id: string
name: string
is_default: boolean
@@ -44,6 +44,22 @@ export const useServerStore = defineStore("server", {
this.loading = false;
}
},
async updateServer(serverId: string, payload: { name: string; is_default?: boolean }) {
const api = useApi();
const response = await api.put(`/servers/${serverId}`, {
name: payload.name,
password: null,
is_default: payload.is_default ?? false,
});
if (!response.ok) {
const error = await response.json().catch(() => null);
throw new Error(error?.message || 'Failed to update server');
}
const updated = await response.json();
const index = this.servers.findIndex(server => server.id === serverId);
if (index >= 0) this.servers[index] = updated;
return updated;
},
async fetchServerTree(serverId: string) {
const api = useApi();
const channelStore = useChannelStore();
@@ -80,4 +96,4 @@ export const useServerStore = defineStore("server", {
this.error = null;
}
}
});
});
+7
View File
@@ -0,0 +1,7 @@
export interface Role {
id: string
server_id: string
name: string
is_default: boolean
created_at: string
}