feat(front) : maj @malio/layer-ui 1.7.1 et refonte des drawers

- adaptation a l'API MalioDrawer 1.7 (titre via slot #header, footer frere du body scrollable)
- footer fixe : boutons 150px, justify-between, bordures header/footer
- permissions en accordeon Malio par module (RoleDrawer + UserRbacDrawer)
- cases a cocher sur une seule colonne
- suppression de PermissionGroup, devenu inutilise

Ajustements restants (layers/titres) a suivre.
This commit is contained in:
2026-05-27 16:53:06 +02:00
parent 43d80df1e1
commit 7c2e570fa0
7 changed files with 198 additions and 160 deletions
@@ -1,66 +0,0 @@
<template>
<div class="rounded-lg border border-neutral-200 overflow-hidden">
<!-- En-tete du groupe avec checkbox "tout selectionner" -->
<div class="flex items-center gap-3 bg-neutral-50 px-4 py-3 border-b border-neutral-200">
<MalioCheckbox
:id="`group-${module}`"
:label="moduleLabel"
:model-value="allSelected"
label-class="font-semibold text-sm text-neutral-700 capitalize"
@update:model-value="toggleAll"
/>
<span class="ml-auto text-xs text-neutral-400">
{{ selectedCount }}/{{ permissions.length }}
</span>
</div>
<!-- Liste des permissions individuelles -->
<div class="grid grid-cols-1 gap-1 p-3 sm:grid-cols-2">
<MalioCheckbox
v-for="perm in permissions"
:key="perm.id"
:id="`perm-${perm.id}`"
:label="perm.label"
:model-value="selectedIds.has(perm.id)"
label-class="text-sm text-neutral-600"
@update:model-value="(val: boolean) => togglePermission(perm.id, val)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { Permission } from '~/shared/types/rbac'
const props = defineProps<{
module: string
moduleLabel: string
permissions: Permission[]
selectedIds: Set<number>
}>()
const emit = defineEmits<{
toggle: [permissionId: number, selected: boolean]
toggleAll: [module: string, selected: boolean]
}>()
// Nombre de permissions selectionnees dans ce groupe
const selectedCount = computed(() =>
props.permissions.filter(p => props.selectedIds.has(p.id)).length
)
// Vrai si toutes les permissions du groupe sont selectionnees
const allSelected = computed(() =>
props.permissions.length > 0 && selectedCount.value === props.permissions.length
)
// Emet l'evenement de bascule pour une permission individuelle
function togglePermission(id: number, selected: boolean) {
emit('toggle', id, selected)
}
// Emet l'evenement de bascule pour toutes les permissions du groupe
function toggleAll(selected: boolean) {
emit('toggleAll', props.module, selected)
}
</script>
+81 -36
View File
@@ -1,11 +1,17 @@
<template> <template>
<MalioDrawer <MalioDrawer
:model-value="modelValue" :model-value="modelValue"
:title="isEditMode ? t('admin.roles.editRole') : t('admin.roles.createRole')"
drawer-class="w-full max-w-lg" drawer-class="w-full max-w-lg"
header-class="border-b border-black"
footer-class="justify-between border-t border-black p-6"
@update:model-value="emit('update:modelValue', $event)" @update:model-value="emit('update:modelValue', $event)"
> >
<form class="flex flex-col gap-6 p-4" @submit.prevent="handleSave"> <template #header>
<h2 class="text-[24px] font-bold">
{{ isEditMode ? t('admin.roles.editRole') : t('admin.roles.createRole') }}
</h2>
</template>
<form class="flex flex-col gap-4 py-4" @submit.prevent="handleSave">
<!-- Champs du role --> <!-- Champs du role -->
<MalioInputText <MalioInputText
v-model="form.label" v-model="form.label"
@@ -44,45 +50,71 @@
<div v-else-if="permissionsByModule.length === 0" class="text-sm text-neutral-400"> <div v-else-if="permissionsByModule.length === 0" class="text-sm text-neutral-400">
{{ t('admin.roles.permissions.noPermissions') }} {{ t('admin.roles.permissions.noPermissions') }}
</div> </div>
<div class="flex flex-col gap-4"> <!-- Un panneau d'accordeon par module (mode multiple) ; le compteur
<PermissionGroup selectionnees/total reste visible dans l'en-tete replie. -->
<MalioAccordion v-else v-model="openModules">
<MalioAccordionItem
v-for="group in permissionsByModule" v-for="group in permissionsByModule"
:key="group.module" :key="group.module"
:module="group.module" :value="group.module"
:module-label="group.module" :title="`${group.module} (${selectedCountFor(group)}/${group.permissions.length})`"
:permissions="group.permissions" header-class="capitalize"
:selected-ids="selectedPermissionIds" >
@toggle="handleTogglePermission" <div class="flex flex-col gap-3">
@toggle-all="handleToggleAll" <!-- Tout selectionner pour ce module -->
/> <MalioCheckbox
</div> :id="`role-group-${group.module}`"
:label="t('admin.roles.permissions.selectAll')"
:model-value="allSelectedFor(group)"
label-class="font-semibold text-sm text-neutral-700"
@update:model-value="(val: boolean) => handleToggleAll(group.module, val)"
/>
<div class="flex flex-col gap-2">
<MalioCheckbox
v-for="perm in group.permissions"
:id="`role-perm-${perm.id}`"
:key="perm.id"
:label="perm.label"
:model-value="selectedPermissionIds.has(perm.id)"
label-class="text-sm text-neutral-600"
@update:model-value="(val: boolean) => handleTogglePermission(perm.id, val)"
/>
</div>
</div>
</MalioAccordionItem>
</MalioAccordion>
</div> </div>
<!-- Boutons -->
<div class="flex justify-end gap-3 border-t border-neutral-200 pt-4">
<MalioButton
v-if="isEditMode"
:label="t('common.delete')"
variant="danger"
icon-name="mdi:delete-outline"
icon-position="left"
:disabled="role?.isSystem"
@click="emit('delete')"
/>
<MalioButton
v-else
:label="t('common.cancel')"
variant="tertiary"
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="t('common.save')"
variant="primary"
:disabled="saving || permissionsLoadFailed"
@click="handleSave"
/>
</div>
</form> </form>
<!-- Footer fixe : depuis la 1.7.1 le slot #footer est un frere du body
scrollable (shrink-0), donc reellement fige sans sticky. -->
<template #footer>
<MalioButton
v-if="isEditMode"
:label="t('common.delete')"
variant="danger"
icon-name="mdi:delete-outline"
icon-position="left"
button-class="w-[150px]"
:disabled="role?.isSystem"
@click="emit('delete')"
/>
<MalioButton
v-else
:label="t('common.cancel')"
variant="tertiary"
button-class="w-[150px]"
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="t('common.save')"
variant="primary"
button-class="w-[150px]"
:disabled="saving || permissionsLoadFailed"
@click="handleSave"
/>
</template>
</MalioDrawer> </MalioDrawer>
</template> </template>
@@ -124,6 +156,19 @@ const form = ref({
const selectedPermissionIds = ref(new Set<number>()) const selectedPermissionIds = ref(new Set<number>())
// Modules ouverts dans l'accordeon des permissions (mode multiple)
const openModules = ref<string[]>([])
// Nombre de permissions selectionnees pour un module donne
function selectedCountFor(group: PermissionModule): number {
return group.permissions.filter(p => selectedPermissionIds.value.has(p.id)).length
}
// Vrai si toutes les permissions du module sont selectionnees
function allSelectedFor(group: PermissionModule): boolean {
return group.permissions.length > 0 && selectedCountFor(group) === group.permissions.length
}
const isEditMode = computed(() => props.role !== null) const isEditMode = computed(() => props.role !== null)
// Grouper les permissions par module // Grouper les permissions par module
@@ -1,11 +1,17 @@
<template> <template>
<MalioDrawer <MalioDrawer
:model-value="modelValue" :model-value="modelValue"
:title="t('admin.users.drawer.title', { username: user?.username ?? '' })" drawer-class="w-full max-w-[450px]"
drawer-class="w-full max-w-lg" header-class="border-b border-black"
footer-class="justify-between border-t border-black p-6"
@update:model-value="emit('update:modelValue', $event)" @update:model-value="emit('update:modelValue', $event)"
> >
<div class="flex flex-col gap-6 p-4"> <template #header>
<h2 class="text-[24px] font-bold">
{{ t('admin.users.drawer.title', { username: user?.username ?? '' }) }}
</h2>
</template>
<div class="flex flex-col gap-4 py-4">
<!-- Etat d'erreur de chargement des referentiels : bloque la <!-- Etat d'erreur de chargement des referentiels : bloque la
sauvegarde pour empecher un ecrasement silencieux des droits. --> sauvegarde pour empecher un ecrasement silencieux des droits. -->
<div <div
@@ -60,18 +66,39 @@
<div v-if="permissionsByModule.length === 0" class="text-sm text-neutral-400"> <div v-if="permissionsByModule.length === 0" class="text-sm text-neutral-400">
{{ t('admin.roles.permissions.noPermissions') }} {{ t('admin.roles.permissions.noPermissions') }}
</div> </div>
<div class="flex flex-col gap-4"> <!-- Un panneau d'accordeon par module (mode multiple) ; le compteur
<PermissionGroup selectionnees/total reste visible dans l'en-tete replie. -->
<MalioAccordion v-else v-model="openDirectModules">
<MalioAccordionItem
v-for="group in permissionsByModule" v-for="group in permissionsByModule"
:key="group.module" :key="group.module"
:module="group.module" :value="group.module"
:module-label="group.module" :title="`${group.module} (${directSelectedCount(group)}/${group.permissions.length})`"
:permissions="group.permissions" header-class="capitalize"
:selected-ids="selectedDirectPermissionIds" >
@toggle="handleTogglePermission" <div class="flex flex-col gap-3">
@toggle-all="handleToggleAll" <!-- Tout selectionner pour ce module -->
/> <MalioCheckbox
</div> :id="`direct-group-${group.module}`"
:label="t('admin.roles.permissions.selectAll')"
:model-value="directAllSelected(group)"
label-class="font-semibold text-sm text-neutral-700"
@update:model-value="(val: boolean) => handleToggleAll(group.module, val)"
/>
<div class="flex flex-col gap-2">
<MalioCheckbox
v-for="perm in group.permissions"
:id="`direct-perm-${perm.id}`"
:key="perm.id"
:label="perm.label"
:model-value="selectedDirectPermissionIds.has(perm.id)"
label-class="text-sm text-neutral-600"
@update:model-value="(val: boolean) => handleTogglePermission(perm.id, val)"
/>
</div>
</div>
</MalioAccordionItem>
</MalioAccordion>
</div> </div>
<!-- Section Sites autorises (ticket 2 module Sites) --> <!-- Section Sites autorises (ticket 2 module Sites) -->
@@ -103,21 +130,25 @@
<EffectivePermissions :permissions="effectivePermissions" /> <EffectivePermissions :permissions="effectivePermissions" />
</div> </div>
<!-- Boutons -->
<div class="flex justify-end gap-3 border-t border-neutral-200 pt-4">
<MalioButton
:label="t('common.cancel')"
variant="tertiary"
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="t('common.save')"
variant="primary"
:disabled="saving || loadFailed"
@click="handleSave"
/>
</div>
</div> </div>
<!-- Footer fixe : depuis la 1.7.1 le slot #footer est un frere du body
scrollable (shrink-0), donc reellement fige sans sticky. -->
<template #footer>
<MalioButton
:label="t('common.cancel')"
variant="tertiary"
button-class="w-[150px]"
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="t('common.save')"
variant="primary"
button-class="w-[150px]"
:disabled="saving || loadFailed"
@click="handleSave"
/>
</template>
</MalioDrawer> </MalioDrawer>
</template> </template>
@@ -158,6 +189,19 @@ const selectedRoleIds = ref(new Set<number>())
const selectedDirectPermissionIds = ref(new Set<number>()) const selectedDirectPermissionIds = ref(new Set<number>())
const selectedSiteIds = ref(new Set<number>()) const selectedSiteIds = ref(new Set<number>())
// Modules ouverts dans l'accordeon des permissions directes (mode multiple)
const openDirectModules = ref<string[]>([])
// Nombre de permissions directes selectionnees pour un module donne
function directSelectedCount(group: PermissionModule): number {
return group.permissions.filter(p => selectedDirectPermissionIds.value.has(p.id)).length
}
// Vrai si toutes les permissions directes du module sont selectionnees
function directAllSelected(group: PermissionModule): boolean {
return group.permissions.length > 0 && directSelectedCount(group) === group.permissions.length
}
// Detecter l'auto-edition // Detecter l'auto-edition
const isSelfEdit = computed(() => props.user?.id === auth.user?.id) const isSelfEdit = computed(() => props.user?.id === auth.user?.id)
@@ -126,9 +126,13 @@
<!-- Drawer detail : diff courant + timeline complete de l'entite --> <!-- Drawer detail : diff courant + timeline complete de l'entite -->
<MalioDrawer <MalioDrawer
v-model="drawerOpen" v-model="drawerOpen"
:title="drawerTitle"
drawer-class="max-w-2xl" drawer-class="max-w-2xl"
> >
<template #header>
<h2 class="text-[24px] font-bold">
{{ drawerTitle }}
</h2>
</template>
<div v-if="selectedEntry"> <div v-if="selectedEntry">
<AuditLogDetail :entry="selectedEntry" /> <AuditLogDetail :entry="selectedEntry" />
<div class="mt-4 border-t border-gray-200 pt-3"> <div class="mt-4 border-t border-gray-200 pt-3">
@@ -1,11 +1,17 @@
<template> <template>
<MalioDrawer <MalioDrawer
:model-value="modelValue" :model-value="modelValue"
:title="isEditMode ? t('admin.sites.editSite') : t('admin.sites.createSite')"
drawer-class="w-full max-w-lg" drawer-class="w-full max-w-lg"
header-class="border-b border-black"
footer-class="justify-between border-t border-black p-6"
@update:model-value="emit('update:modelValue', $event)" @update:model-value="emit('update:modelValue', $event)"
> >
<form class="flex flex-col gap-6 p-4" @submit.prevent="handleSave"> <template #header>
<h2 class="text-[24px] font-bold">
{{ isEditMode ? t('admin.sites.editSite') : t('admin.sites.createSite') }}
</h2>
</template>
<form class="flex flex-col gap-4 py-4" @submit.prevent="handleSave">
<MalioInputText <MalioInputText
v-model="form.name" v-model="form.name"
:label="t('admin.sites.form.name')" :label="t('admin.sites.form.name')"
@@ -70,30 +76,35 @@
</p> </p>
</div> </div>
<!-- Boutons -->
<div class="flex justify-end gap-3 border-t border-neutral-200 pt-4">
<MalioButton
v-if="isEditMode"
:label="t('common.delete')"
variant="danger"
icon-name="mdi:delete-outline"
icon-position="left"
@click="emit('delete')"
/>
<MalioButton
v-else
:label="t('common.cancel')"
variant="tertiary"
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="t('common.save')"
variant="primary"
:disabled="saving || !isValidHex"
@click="handleSave"
/>
</div>
</form> </form>
<!-- Footer fixe : depuis la 1.7.1 le slot #footer est un frere du body
scrollable (shrink-0), donc reellement fige sans sticky. -->
<template #footer>
<MalioButton
v-if="isEditMode"
:label="t('common.delete')"
variant="danger"
icon-name="mdi:delete-outline"
icon-position="left"
button-class="w-[150px]"
@click="emit('delete')"
/>
<MalioButton
v-else
:label="t('common.cancel')"
variant="tertiary"
button-class="w-[150px]"
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="t('common.save')"
variant="primary"
button-class="w-[150px]"
:disabled="saving || !isValidHex"
@click="handleSave"
/>
</template>
</MalioDrawer> </MalioDrawer>
</template> </template>
+4 -4
View File
@@ -7,7 +7,7 @@
"name": "starseed-frontend", "name": "starseed-frontend",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@malio/layer-ui": "^1.5.0", "@malio/layer-ui": "^1.7.1",
"@nuxt/icon": "^2.2.1", "@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.3", "@nuxtjs/i18n": "^10.2.3",
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
@@ -1866,9 +1866,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@malio/layer-ui": { "node_modules/@malio/layer-ui": {
"version": "1.5.0", "version": "1.7.1",
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.5.0/layer-ui-1.5.0.tgz", "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.7.1/layer-ui-1.7.1.tgz",
"integrity": "sha512-uVuG8kRakWgpWYQCMUf1LFD+gjx0iRFfNJn/jlqjxiZmZyGZMckcMW2qA9hGZBiheBsTJWw1pRR4ufuyAYPY0A==", "integrity": "sha512-RYMMappWt/fgjD+BM7//h2O6kxD6WH9Fui8hoC29xtKySRQsqD61XKTdR7BRRkpktbxKmV39q/hblyAFBqV5yw==",
"dependencies": { "dependencies": {
"@nuxt/icon": "^2.2.1", "@nuxt/icon": "^2.2.1",
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
+1 -1
View File
@@ -17,7 +17,7 @@
"test:e2e:ui": "playwright test --ui" "test:e2e:ui": "playwright test --ui"
}, },
"dependencies": { "dependencies": {
"@malio/layer-ui": "^1.5.0", "@malio/layer-ui": "^1.7.1",
"@nuxt/icon": "^2.2.1", "@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.3", "@nuxtjs/i18n": "^10.2.3",
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",