Files
brewman/overlord/src/app/role/role-detail/role-detail.component.ts
T
2026-09-15 04:10:38 +00:00

231 lines
6.2 KiB
TypeScript

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