Implements Spec 082 updates to the Filament Action Surface Contract: - New required list/table slot: InspectAffordance (clickable row via recordUrl preferred; also supports View action or primary link column) - Retrofit view-only tables to remove lone View row action buttons and use clickable rows - Update validator + guard tests, add golden regression assertions - Add docs: docs/ui/action-surface-contract.md Tests (local via Sail): - vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php - vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceValidatorTest.php - vendor/bin/sail artisan test --compact tests/Feature/Rbac/ActionSurfaceRbacSemanticsTest.php - vendor/bin/sail artisan test --compact tests/Feature/Filament/EntraGroupSyncRunResourceTest.php Notes: - Filament v5 / Livewire v4 compatible. - No destructive-action behavior changed in this PR. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #100
154 lines
6.5 KiB
PHP
154 lines
6.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDiscoveredComponent;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceExemptions;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceProfileDefinition;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceSlotRequirement;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceValidator;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceComponentType;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfacePanelScope;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
|
|
final class ActionSurfaceValidatorCompleteStub
|
|
{
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView)
|
|
->satisfy(ActionSurfaceSlot::ListHeader)
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value)
|
|
->satisfy(ActionSurfaceSlot::ListRowMoreMenu)
|
|
->satisfy(ActionSurfaceSlot::ListBulkMoreGroup)
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState)
|
|
->satisfy(ActionSurfaceSlot::DetailHeader);
|
|
}
|
|
}
|
|
|
|
final class ActionSurfaceValidatorMissingSlotStub
|
|
{
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView)
|
|
->satisfy(ActionSurfaceSlot::ListHeader)
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value);
|
|
}
|
|
}
|
|
|
|
final class ActionSurfaceValidatorRunLogNoExportStub
|
|
{
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::RunLog)
|
|
->withDefaults(new \App\Support\Ui\ActionSurface\ActionSurfaceDefaults(
|
|
moreGroupLabel: 'More',
|
|
exportIsDefaultBulkActionForReadOnly: false,
|
|
))
|
|
->satisfy(ActionSurfaceSlot::ListHeader)
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value)
|
|
->satisfy(ActionSurfaceSlot::ListBulkMoreGroup)
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState)
|
|
->satisfy(ActionSurfaceSlot::DetailHeader);
|
|
}
|
|
}
|
|
|
|
final class ActionSurfaceValidatorExemptSlotWithoutReasonStub
|
|
{
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView)
|
|
->setSlot(ActionSurfaceSlot::ListHeader, ActionSurfaceSlotRequirement::exempt())
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value)
|
|
->satisfy(ActionSurfaceSlot::ListRowMoreMenu)
|
|
->satisfy(ActionSurfaceSlot::ListBulkMoreGroup)
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState)
|
|
->satisfy(ActionSurfaceSlot::DetailHeader);
|
|
}
|
|
}
|
|
|
|
final class ActionSurfaceValidatorNoDeclarationStub {}
|
|
|
|
function actionSurfaceComponent(string $className): ActionSurfaceDiscoveredComponent
|
|
{
|
|
return new ActionSurfaceDiscoveredComponent(
|
|
className: $className,
|
|
componentType: ActionSurfaceComponentType::Resource,
|
|
panelScopes: [ActionSurfacePanelScope::Tenant],
|
|
);
|
|
}
|
|
|
|
it('passes when all required slots are declared', function (): void {
|
|
$validator = new ActionSurfaceValidator(
|
|
profileDefinition: new ActionSurfaceProfileDefinition,
|
|
exemptions: new ActionSurfaceExemptions([]),
|
|
);
|
|
|
|
$result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorCompleteStub::class)]);
|
|
|
|
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
|
|
});
|
|
|
|
it('fails when a required slot is missing', function (): void {
|
|
$validator = new ActionSurfaceValidator(
|
|
profileDefinition: new ActionSurfaceProfileDefinition,
|
|
exemptions: new ActionSurfaceExemptions([]),
|
|
);
|
|
|
|
$result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorMissingSlotStub::class)]);
|
|
|
|
expect($result->hasIssues())->toBeTrue();
|
|
expect($result->formatForAssertion())->toContain('Required slot is not declared');
|
|
});
|
|
|
|
it('fails missing declarations when no baseline exemption exists', function (): void {
|
|
$validator = new ActionSurfaceValidator(
|
|
profileDefinition: new ActionSurfaceProfileDefinition,
|
|
exemptions: new ActionSurfaceExemptions([]),
|
|
);
|
|
|
|
$result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorNoDeclarationStub::class)]);
|
|
|
|
expect($result->hasIssues())->toBeTrue();
|
|
expect($result->formatForAssertion())->toContain('Missing action-surface declaration');
|
|
});
|
|
|
|
it('accepts missing declarations when explicit baseline exemption exists', function (): void {
|
|
$validator = new ActionSurfaceValidator(
|
|
profileDefinition: new ActionSurfaceProfileDefinition,
|
|
exemptions: new ActionSurfaceExemptions([
|
|
ActionSurfaceValidatorNoDeclarationStub::class => 'Retrofit intentionally deferred in validator unit test.',
|
|
]),
|
|
);
|
|
|
|
$result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorNoDeclarationStub::class)]);
|
|
|
|
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
|
|
});
|
|
|
|
it('requires a bulk exemption reason when run-log export default is disabled', function (): void {
|
|
$validator = new ActionSurfaceValidator(
|
|
profileDefinition: new ActionSurfaceProfileDefinition,
|
|
exemptions: new ActionSurfaceExemptions([]),
|
|
);
|
|
|
|
$result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorRunLogNoExportStub::class)]);
|
|
|
|
expect($result->hasIssues())->toBeTrue();
|
|
expect($result->formatForAssertion())->toContain('ReadOnly/RunLog profile disables Export default');
|
|
});
|
|
|
|
it('fails when slot exemption reason is missing', function (): void {
|
|
$validator = new ActionSurfaceValidator(
|
|
profileDefinition: new ActionSurfaceProfileDefinition,
|
|
exemptions: new ActionSurfaceExemptions([]),
|
|
);
|
|
|
|
$result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorExemptSlotWithoutReasonStub::class)]);
|
|
|
|
expect($result->hasIssues())->toBeTrue();
|
|
expect($result->formatForAssertion())->toContain('Slot is marked exempt but exemption reason is missing or empty');
|
|
});
|