init
This commit is contained in:
@@ -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 d’ajout' }
|
||||
}
|
||||
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>
|
||||
@@ -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/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 d’ajouter 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
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Role {
|
||||
id: string
|
||||
server_id: string
|
||||
name: string
|
||||
is_default: boolean
|
||||
created_at: string
|
||||
}
|
||||
Reference in New Issue
Block a user