import { Component, computed, effect, inject, input, linkedSignal, untracked, afterNextRender, } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ErrorStateComponent } from '../../shared/error-state/error-state.component'; import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component'; import { Permission } from '../permission'; import { Role, RoleItem } from '../role'; import { RoleService } from '../role.service'; export interface RoleDetailFormData { name: string; permissions: { id: string; name: string; permission: boolean; direct: boolean; disabled: boolean; }[]; includedRoles: { id: string; name: string; role: boolean; permissionIds: string[]; }[]; } @Component({ selector: 'app-role-detail', templateUrl: './role-detail.component.html', styleUrls: ['./role-detail.component.css'], imports: [ FormField, FormRoot, MatFormFieldModule, MatInputModule, MatDividerModule, MatCheckboxModule, MatButtonModule, SkeletonLoaderComponent, ErrorStateComponent, ], }) export class RoleDetailComponent { private router = inject(Router); private snackBar = inject(MatSnackBar); private dialog = inject(MatDialog); private ser = inject(RoleService); id = input(null, { transform: (v: string | null | undefined) => v ?? null }); itemResource = this.ser.get(this.id); item = computed(() => this.itemResource.value() ?? new Role()); model = linkedSignal({ source: this.item, computation: (itemVal): RoleDetailFormData => { const implied = new Set(); (itemVal.includedRoles || []).forEach((r) => { if (r.enabled) { (r.permissionIds || []).forEach((pid) => implied.add(pid)); } }); return { name: itemVal.name ?? '', permissions: (itemVal.permissions || []).map((p) => { const isImplied = implied.has(p.id as string); const isDirect = p.enabled === true; return { id: p.id ?? '', name: p.name, permission: isImplied || isDirect, direct: isDirect, disabled: isImplied, }; }), includedRoles: (itemVal.includedRoles || []).map((r) => ({ id: r.id ?? '', name: r.name, role: r.enabled === true, permissionIds: r.permissionIds || [], })), }; }, }); form = createForm(this.model); private enabledIncludedRoleIds = computed(() => { return this.model() .includedRoles.filter((r) => r.role) .map((r) => r.id) .sort() .join(','); }); constructor() { afterNextRender(() => { this.form.name().focusBoundControl(); }); effect(() => { // Recompute permission locks whenever the set of enabled included roles changes this.enabledIncludedRoleIds(); untracked(() => { this.applyIncludedRolePermissionLocks(); }); }); } private applyIncludedRolePermissionLocks(): void { this.model.update((currentModel) => { const implied = new Set(); currentModel.includedRoles.forEach((role) => { if (role.role) { (role.permissionIds || []).forEach((pid) => implied.add(pid)); } }); let changed = false; const newPerms = currentModel.permissions.map((p) => { // If not disabled, direct state tracks the user's checkbox value const currentDirect = p.disabled ? p.direct : p.permission; const isImplied = implied.has(p.id); const newPermission = isImplied ? true : currentDirect; const newDisabled = isImplied; if ( p.direct !== currentDirect || p.permission !== newPermission || p.disabled !== newDisabled ) { changed = true; return { ...p, direct: currentDirect, permission: newPermission, disabled: newDisabled, }; } return p; }); if (changed) { return { ...currentModel, permissions: newPerms }; } return currentModel; }); } save() { this.ser.saveOrUpdate(this.getItem()).subscribe({ next: () => { this.snackBar.open('', 'Success'); this.router.navigateByUrl('/roles'); }, error: (error) => { this.snackBar.open(error as string, 'Danger'); }, }); } delete() { this.ser.delete(this.item().id as string).subscribe({ next: () => { this.snackBar.open('', 'Success'); this.router.navigateByUrl('/roles'); }, error: (error) => { this.snackBar.open(error as string, 'Danger'); }, }); } confirmDelete(): void { const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '250px', data: { title: 'Delete Role?', content: 'Are you sure? This cannot be undone.' }, }); dialogRef.afterClosed().subscribe((result: boolean) => { if (result) { this.delete(); } }); } getItem(): Role { const formModel = this.model(); return new Role({ ...this.item(), name: formModel.name.trim(), permissions: formModel.permissions.map( (p) => new Permission({ id: p.id, name: p.name, enabled: p.disabled ? p.direct : p.permission, }), ), includedRoles: formModel.includedRoles.map( (r) => new RoleItem({ id: r.id, name: r.name, enabled: r.role, permissionIds: r.permissionIds, }), ), }); } }