Aller au contenu principal

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.

  1. 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).
Fichier roles-list.ts placé au même niveau que le composant comportant l'état calculé
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é
}
  1. 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>
}
}
info

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

attention

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());
}),
);
info

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