Business rules
Conditionnement des actions
Certaines actions sont désactivées, dans ce cas on veut indiquer la raison de cet état. Une action peut également être simplement cachée.
On utilise le type ActionState
(voir code) et les factories associées.
- L'état du bouton est calculé, de préférence dans une méthode pure prenant en paramètre les éléments permettant de calculer l'état. Si le bouton est désactivé on fournit la clé du libellé de la raison à afficher (et éventuellement des paramètres).
export type EditReasons = 'disabledRole' | 'otherReason'; // on définit une union de code décrivant les différentes raisons de désactivation possible
export type EditState = ActionState<EditReasons>; // on crée un type ActionState à partir de cette union (pour profiter de typescript et de l'autocomplétion)
export function computeEditState(role: RoleListItem): EditState {
const { valid, editable } = role;
if (!editable) {
return buildHiddenState(); //le bouton est caché
} else if (!valid) {
return buildDisabledState<EditReasons>( // le bouton est désactivé
'disabledRole', // on fournit le code associé - cela permet de réutiliser cet ActionState ailleurs en fonction du code (ex: affichage d'un message associé)
'ROLES.LIST.TOOLTIP_EDIT_BTN_DISABLED.DISABLED_ROLE', // clé du libellé à afficher
{
roleName: 'Mon Role' //exemple de param à injecter dans le libellé
}
);
}
return buildEnabledState(); // sinon le bouton est activé
}
- Ensuite on utilise cet ActionState dans le template. Pour permettre d'afficher un tooltip et d'avoir le focus sur le bouton, on utilise un
<div>
comme wrapper d'un bouton désactivé en laissant tous les attributs comme indiqué ci-dessous.
@if (!row.deleteButtonState.hidden) {
@if (!row.deleteButtonState.disabled) {
<!-- on affiche le premier bouton s'il n'est pas désactivé -->
<!--peut être un lien <a> également -->
<button
blocDeleteButton
*ngIf="!row.deleteButtonState.disabled"
(click)="openDeletionModal(row)"
></button>
} @else {
<!-- sinon le wrapper accessible -->
<div
*ngIf="row.deleteButtonState.disabled"
tabindex="0"
role="button"
[title]="row.deleteButtonState.messageKey | translate"
aria-disabled="true"
>
<button blocDeleteButton disabled="true"></button>
</div>
}
}
role="button"
indique que le wrapper joue le rôle de bouton
aria-disabled="true"
indique que le wrapper est un élément désactivé
tabindex=0
permet de rajouter l'élément désactivé en tant qu'élément focusable via le clavier
title
indique le message à afficher lors du survol ou annoncé par le lecteur d'écran lors du focus sur le wrapper
On utilise désormais directement le composant suivant :
<shared-disabled-button-wrapper
[title]="row.deleteButtonState.messageKey | translate"
>
<button blocDeleteButton disabled="true"></button>
</shared-disabled-button-wrapper>
ACL (conventions)
Les conventions ont une gestion plus fine des droits - les ACL - certaines actions sont conditionnés par les droits utilisateurs mais aussi l'état de la convention, etc.
Une action (ex: suppression d'un element) ou un groupement d'action (ex: gestion d'un element - edition, suppression, ..) est lié à un ACL.
La liste des ACL est chargée lors de l'accès à une convention et sont rechargés fréquemment (par ex: suite à une action) pour garantir un état fiable de l'interface.
La gestion de ces droits se fait via le GrantService
injecté au chargement d'une convention au niveau du routing. On vérifie d'abord avec conventionDetailGuard
si l'utilisateur a les droits.
{
path: ':id',
title: 'CONVENTIONS.DETAIL.TITLE',
providers: [GrantsService], //ici
canActivate: [conventionDetailGuard],
loadChildren: () =>
import('./conventions-detail.routes').then(
(mod) => mod.CONVENTIONS_DETAIL_ROUTES,
),
},
On peut ensuite vérifier si l'utilisateur a un ACL particulier de façon synchrone, ou asynchrone (avec un obs$).
L'observable n'émet seulement que si la valeur est différente de la précédente (utilisation de l'opérateur distinctUntilChanged
) pour éviter des recalculs.
readonly canManagePartenaire$ = this.grantsService.hasOneOfPermission$(
PermissionTarget.MANAGE_CONVENTION_PARTENAIRE,
)
Exemple de calcul d'état d'une action a partir d'une ACL :
private readonly canCreateState$ = this._service.canManagePartenaire$.pipe(
switchMap((hasPermission) => {
if (hasPermission) {
// on ne recalcule les droits que si l'utilisateur a d'abord les permissions
return combineLatest([
this._service.convention$.pipe(
map((convention) => convention.statusCode),
distinctUntilChanged(),
),
this._service.getAssignableTypes$(true),
]).pipe(
map(([conventionStatus, assignableTypes]) =>
computeAddState(hasPermission, conventionStatus, assignableTypes),
),
);
}
return of(buildHiddenState());
}),
);
Pour une action, on va bloquer l'interface mais on ne re-vérifie pas les ACL avant l'appel aux endpoints associés de l'action (ex: chargement des données nécessaires, et l'action en elle meme) par simplicité.
Ex:
Lorsque j'edite un élément, j'ai besoin de charger une liste d'elements pour le formulaire. -> je ne vérifie pas les ACL
Lorsque je valide, j'envoie une requête de modification -> je ne vérifie pas les ACL