refactor(core) : reponse aux retours de review

- extrait PermissionAccordion (modules/core/components) : source unique pour
  l'accordeon de permissions par module, utilise par RoleDrawer et UserRbacDrawer
  (supprime la duplication selectedCountFor/directSelectedCount + ~25 lignes de markup)
- expose le type PermissionModule dans shared/types/rbac
- audit-log : simplifie toggleEntity (filter au lieu de Set, valeurs uniques par construction)
- default.vue : commente les valeurs en dur (232/170/47) comme issues de la maquette Figma
- audit-log : corrige iconSize -> icon-size (warning eslint vue/attribute-hyphenation)
This commit is contained in:
2026-05-28 14:14:34 +02:00
parent 256b8d4ff2
commit 5c4bc32827
6 changed files with 111 additions and 110 deletions
+7
View File
@@ -1,3 +1,10 @@
<!--
Valeurs en dur issues de la maquette Figma (design Starseed) :
- sidebar depliee : 232px (w-[232px], repli laisse par defaut 72px)
- marge horizontale du contenu sur desktop : 170px (xl:px-[170px])
- bande blanche sticky sous la navbar : 47px (h-[47px])
A faire evoluer uniquement avec une mise a jour de maquette.
-->
<template> <template>
<div class="h-screen overflow-hidden"> <div class="h-screen overflow-hidden">
<div class="flex h-full"> <div class="flex h-full">
@@ -0,0 +1,71 @@
<template>
<!-- Accordeon de permissions groupees par module : un panneau par module,
avec compteur (selectionnees/total) dans le titre, case "Tout selectionner"
et liste des permissions individuelles. Source unique de cette UX, utilisee
par RoleDrawer (permissions du role) et UserRbacDrawer (permissions directes). -->
<MalioAccordion v-model="openModules">
<MalioAccordionItem
v-for="group in groupsByModule"
:key="group.module"
:value="group.module"
:title="`${group.module} (${selectedCountFor(group)}/${group.permissions.length})`"
header-class="capitalize"
>
<div class="flex flex-col gap-3">
<!-- Tout selectionner pour ce module -->
<MalioCheckbox
:id="`${idPrefix}-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) => emit('toggle-all', group.module, val)"
/>
<div class="flex flex-col gap-2">
<MalioCheckbox
v-for="perm in group.permissions"
:id="`${idPrefix}-perm-${perm.id}`"
:key="perm.id"
:label="perm.label"
:model-value="selectedIds.has(perm.id)"
label-class="text-sm text-neutral-600"
@update:model-value="(val: boolean) => emit('toggle', perm.id, val)"
/>
</div>
</div>
</MalioAccordionItem>
</MalioAccordion>
</template>
<script setup lang="ts">
import type { PermissionModule } from '~/shared/types/rbac'
const { t } = useI18n()
const props = defineProps<{
/** Groupes de permissions a afficher, un par module. */
groupsByModule: PermissionModule[]
/** Ids des permissions actuellement selectionnees. */
selectedIds: Set<number>
/** Prefixe pour les ids HTML : evite les collisions si plusieurs accordeons coexistent (ex: "role" vs "direct"). */
idPrefix: string
}>()
const emit = defineEmits<{
toggle: [permissionId: number, selected: boolean]
'toggle-all': [module: string, selected: boolean]
}>()
// Modules ouverts dans l'accordeon (mode multiple). Etat local : chaque instance
// du composant garde sa propre liste, pas de partage entre drawers.
const openModules = ref<string[]>([])
// Nombre de permissions selectionnees pour un module donne.
function selectedCountFor(group: PermissionModule): number {
return group.permissions.filter(p => props.selectedIds.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
}
</script>
@@ -50,39 +50,14 @@
<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>
<!-- Un panneau d'accordeon par module (mode multiple) ; le compteur <PermissionAccordion
selectionnees/total reste visible dans l'en-tete replie. --> v-else
<MalioAccordion v-else v-model="openModules"> :groups-by-module="permissionsByModule"
<MalioAccordionItem :selected-ids="selectedPermissionIds"
v-for="group in permissionsByModule" id-prefix="role"
:key="group.module" @toggle="handleTogglePermission"
:value="group.module" @toggle-all="handleToggleAll"
:title="`${group.module} (${selectedCountFor(group)}/${group.permissions.length})`" />
header-class="capitalize"
>
<div class="flex flex-col gap-3">
<!-- Tout selectionner pour ce module -->
<MalioCheckbox
: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>
</form> </form>
@@ -119,12 +94,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Permission, Role } from '~/shared/types/rbac' import type { Permission, PermissionModule, Role } from '~/shared/types/rbac'
interface PermissionModule {
module: string
permissions: Permission[]
}
const { t } = useI18n() const { t } = useI18n()
const api = useApi() const api = useApi()
@@ -156,19 +126,6 @@ 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
@@ -66,39 +66,14 @@
<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>
<!-- Un panneau d'accordeon par module (mode multiple) ; le compteur <PermissionAccordion
selectionnees/total reste visible dans l'en-tete replie. --> v-else
<MalioAccordion v-else v-model="openDirectModules"> :groups-by-module="permissionsByModule"
<MalioAccordionItem :selected-ids="selectedDirectPermissionIds"
v-for="group in permissionsByModule" id-prefix="direct"
:key="group.module" @toggle="handleTogglePermission"
:value="group.module" @toggle-all="handleToggleAll"
:title="`${group.module} (${directSelectedCount(group)}/${group.permissions.length})`" />
header-class="capitalize"
>
<div class="flex flex-col gap-3">
<!-- Tout selectionner pour ce module -->
<MalioCheckbox
: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) -->
@@ -153,14 +128,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Permission, Role, UserListItem, UserRbacDetail, EffectivePermission } from '~/shared/types/rbac' import type { Permission, PermissionModule, Role, UserListItem, UserRbacDetail, EffectivePermission } from '~/shared/types/rbac'
import type { Site } from '~/shared/types/sites' import type { Site } from '~/shared/types/sites'
interface PermissionModule {
module: string
permissions: Permission[]
}
const { t } = useI18n() const { t } = useI18n()
const api = useApi() const api = useApi()
const auth = useAuthStore() const auth = useAuthStore()
@@ -189,19 +159,6 @@ 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)
@@ -8,7 +8,7 @@
:label="t('audit.filters.title')" :label="t('audit.filters.title')"
icon-name="mdi:tune" icon-name="mdi:tune"
icon-position="left" icon-position="left"
iconSize="24" icon-size="24"
button-class="w-[184px] justify-start gap-4 text-black" button-class="w-[184px] justify-start gap-4 text-black"
@click="openFilters" @click="openFilters"
/> />
@@ -278,12 +278,12 @@ function openFilters(): void {
filterDrawerOpen.value = true filterDrawerOpen.value = true
} }
// Bascule un type d'entite dans le brouillon (multi-selection). // Bascule un type d'entite dans le brouillon (multi-selection). Les valeurs
// sont uniques par construction (v-for sur entityTypeOptions), pas besoin de Set.
function toggleEntity(value: string, selected: boolean): void { function toggleEntity(value: string, selected: boolean): void {
const set = new Set(draftEntityTypes.value) draftEntityTypes.value = selected
if (selected) set.add(value) ? [...draftEntityTypes.value, value]
else set.delete(value) : draftEntityTypes.value.filter(v => v !== value)
draftEntityTypes.value = [...set]
} }
// "Reinitialiser" : vide le brouillon ET les filtres actifs, puis recharge. // "Reinitialiser" : vide le brouillon ET les filtres actifs, puis recharge.
+9
View File
@@ -43,3 +43,12 @@ export interface EffectivePermission {
module: string module: string
sources: string[] sources: string[]
} }
/**
* Groupement de permissions par module pour l'affichage en accordeon.
* Construit cote consommateur a partir de la liste plate /api/permissions.
*/
export interface PermissionModule {
module: string
permissions: Permission[]
}