## Summary - add the RBAC role definition diff UX upgrade as the first concrete consumer of the shared diff presentation foundation - refine managed tenant onboarding draft routing, CTA labeling, and cancellation redirect behavior - tighten related Filament and diff rendering regression coverage ## Testing - updated focused Pest coverage for onboarding draft routing and lifecycle behavior - updated focused Pest coverage for shared diff partials and RBAC finding rendering ## Notes - Livewire v4.0+ compliance is preserved within the existing Filament v5 surfaces - provider registration remains unchanged in bootstrap/providers.php - no new Filament assets were added; existing deployment practice still relies on php artisan filament:assets when assets change Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #171
206 lines
6.1 KiB
PHP
206 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Diff;
|
|
|
|
final class RbacRoleDefinitionDiffBuilder
|
|
{
|
|
private const ROLE_FIELD_ORDER = [
|
|
'Role definition > Display name' => 100,
|
|
'Role definition > Description' => 200,
|
|
'Role definition > Role source' => 300,
|
|
'Role definition > Permission blocks' => 400,
|
|
'Role definition > Scope tag IDs' => 500,
|
|
];
|
|
|
|
private const PERMISSION_FIELD_ORDER = [
|
|
'Allowed actions' => 100,
|
|
'Denied actions' => 200,
|
|
'Conditions' => 300,
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly DiffPresenter $presenter,
|
|
) {}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
public function build(array $payload): DiffPresentation
|
|
{
|
|
$baseline = $this->normalizeSide(
|
|
is_array($payload['baseline'] ?? null) ? $payload['baseline'] : [],
|
|
);
|
|
$current = $this->normalizeSide(
|
|
is_array($payload['current'] ?? null) ? $payload['current'] : [],
|
|
);
|
|
$changedKeys = $this->changedKeys($payload);
|
|
|
|
$presentation = $this->presenter->present(
|
|
baseline: $baseline,
|
|
current: $current,
|
|
changedKeys: $changedKeys,
|
|
labels: $this->labelsFor($baseline, $current, $changedKeys),
|
|
);
|
|
|
|
if ($presentation->rows === []) {
|
|
return $presentation;
|
|
}
|
|
|
|
$rows = array_map(
|
|
fn (DiffRow $row): DiffRow => $this->normalizeRow($row),
|
|
$presentation->rows,
|
|
);
|
|
|
|
usort($rows, fn (DiffRow $left, DiffRow $right): int => $this->sortKey($left->key) <=> $this->sortKey($right->key));
|
|
|
|
return new DiffPresentation(
|
|
summary: DiffSummary::fromRows($rows, $presentation->summary->message),
|
|
rows: $rows,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return list<string>
|
|
*/
|
|
private function changedKeys(array $payload): array
|
|
{
|
|
$changedKeys = is_array($payload['changed_keys'] ?? null) ? $payload['changed_keys'] : [];
|
|
|
|
return array_values(array_filter(
|
|
$changedKeys,
|
|
fn (mixed $key): bool => is_string($key) && trim($key) !== '',
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $side
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function normalizeSide(array $side): array
|
|
{
|
|
$normalized = [];
|
|
$rows = is_array($side['normalized'] ?? null) ? $side['normalized'] : [];
|
|
|
|
foreach ($rows as $key => $value) {
|
|
if (! is_string($key) || trim($key) === '') {
|
|
continue;
|
|
}
|
|
|
|
$normalized[trim($key)] = $value;
|
|
}
|
|
|
|
if (! array_key_exists('Role definition > Role source', $normalized)) {
|
|
$roleSource = $this->roleSourceLabel($side['is_built_in'] ?? null);
|
|
|
|
if ($roleSource !== null) {
|
|
$normalized['Role definition > Role source'] = $roleSource;
|
|
}
|
|
}
|
|
|
|
if (
|
|
! array_key_exists('Role definition > Permission blocks', $normalized)
|
|
&& is_numeric($side['role_permission_count'] ?? null)
|
|
) {
|
|
$normalized['Role definition > Permission blocks'] = (int) $side['role_permission_count'];
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
private function roleSourceLabel(mixed $isBuiltIn): ?string
|
|
{
|
|
return match ($isBuiltIn) {
|
|
true => 'Built-in',
|
|
false => 'Custom',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $baseline
|
|
* @param array<string, mixed> $current
|
|
* @param list<string> $changedKeys
|
|
* @return array<string, string>
|
|
*/
|
|
private function labelsFor(array $baseline, array $current, array $changedKeys): array
|
|
{
|
|
$labels = [];
|
|
|
|
foreach ([array_keys($baseline), array_keys($current), $changedKeys] as $sourceKeys) {
|
|
foreach ($sourceKeys as $key) {
|
|
if (! is_string($key) || trim($key) === '') {
|
|
continue;
|
|
}
|
|
|
|
$labels[$key] = $key;
|
|
}
|
|
}
|
|
|
|
return $labels;
|
|
}
|
|
|
|
private function normalizeRow(DiffRow $row): DiffRow
|
|
{
|
|
$shouldRenderAsList = $row->isListLike && $this->isDesignatedListField($row->key);
|
|
|
|
if ($row->isListLike === $shouldRenderAsList) {
|
|
return $row;
|
|
}
|
|
|
|
return new DiffRow(
|
|
key: $row->key,
|
|
label: $row->label,
|
|
status: $row->status,
|
|
oldValue: $row->oldValue,
|
|
newValue: $row->newValue,
|
|
isListLike: $shouldRenderAsList,
|
|
addedItems: $shouldRenderAsList ? $row->addedItems : [],
|
|
removedItems: $shouldRenderAsList ? $row->removedItems : [],
|
|
unchangedItems: $shouldRenderAsList ? $row->unchangedItems : [],
|
|
meta: $row->meta,
|
|
);
|
|
}
|
|
|
|
private function isDesignatedListField(string $key): bool
|
|
{
|
|
if ($key === 'Role definition > Scope tag IDs') {
|
|
return true;
|
|
}
|
|
|
|
return preg_match('/^Permission block \d+ > (Allowed actions|Denied actions|Conditions)$/', $key) === 1;
|
|
}
|
|
|
|
/**
|
|
* @return array{int, int, int, string}
|
|
*/
|
|
private function sortKey(string $key): array
|
|
{
|
|
if (array_key_exists($key, self::ROLE_FIELD_ORDER)) {
|
|
return [0, self::ROLE_FIELD_ORDER[$key], 0, mb_strtolower($key)];
|
|
}
|
|
|
|
if (preg_match('/^Role definition > (?P<label>.+)$/', $key, $matches) === 1) {
|
|
return [0, 900, 0, mb_strtolower((string) ($matches['label'] ?? $key))];
|
|
}
|
|
|
|
if (preg_match('/^Permission block (?P<block>\d+) > (?P<label>.+)$/', $key, $matches) === 1) {
|
|
return [
|
|
1,
|
|
(int) ($matches['block'] ?? 0),
|
|
$this->permissionFieldOrder((string) ($matches['label'] ?? '')),
|
|
mb_strtolower((string) ($matches['label'] ?? $key)),
|
|
];
|
|
}
|
|
|
|
return [2, 0, 0, mb_strtolower($key)];
|
|
}
|
|
|
|
private function permissionFieldOrder(string $label): int
|
|
{
|
|
return self::PERMISSION_FIELD_ORDER[$label] ?? 900;
|
|
}
|
|
}
|