Spec 316: implement workspace hub clear filter contract (#371)
## Summary - centralize workspace hub environment filter reset behavior across the affected Filament workspace hubs - add a shared page concern and resetter service to clear environment-like URL, Livewire, table, deferred, and persisted filter state consistently - update hub clear actions and clean-entry flows to route back to the canonical clean workspace hub state - add focused feature and browser coverage for the clear-filter contract - include Spec 316 artifacts for the workspace hub clear filter contract ## Testing - not run as part of this commit/push/PR workflow Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #371
This commit is contained in:
parent
eced9ad50c
commit
9b097f97f9
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Concerns;
|
||||
|
||||
use App\Support\Navigation\WorkspaceHubFilterStateResetter;
|
||||
use App\Support\Navigation\WorkspaceHubRegistry;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait ClearsWorkspaceHubEnvironmentFilterState
|
||||
{
|
||||
protected function resetWorkspaceHubEnvironmentFilterStateForCleanEntry(?Request $request = null): void
|
||||
{
|
||||
$resetter = $this->workspaceHubFilterStateResetter();
|
||||
$resetter->neutralizeEnvironmentLikeQueryState($request);
|
||||
|
||||
if (
|
||||
! $resetter->shouldResetForCleanWorkspaceHubEntry($request)
|
||||
&& WorkspaceHubRegistry::requestHasEnvironmentFilterQuery($request)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->clearWorkspaceHubEnvironmentFilterState($request);
|
||||
}
|
||||
|
||||
protected function clearWorkspaceHubEnvironmentFilterState(?Request $request = null): void
|
||||
{
|
||||
$this->forgetWorkspaceHubEnvironmentFilterSessionState($request);
|
||||
$this->clearWorkspaceHubEnvironmentTableFilterState();
|
||||
}
|
||||
|
||||
protected function cleanWorkspaceHubUrl(string $url): string
|
||||
{
|
||||
return $this->workspaceHubFilterStateResetter()->cleanUrl($url);
|
||||
}
|
||||
|
||||
protected function redirectToCleanWorkspaceHubUrl(string $url, ?Request $request = null): void
|
||||
{
|
||||
$this->clearWorkspaceHubEnvironmentFilterState($request);
|
||||
$this->redirect($this->cleanWorkspaceHubUrl($url), navigate: true);
|
||||
}
|
||||
|
||||
private function forgetWorkspaceHubEnvironmentFilterSessionState(?Request $request = null): void
|
||||
{
|
||||
if (! method_exists($this, 'getTableFiltersSessionKey')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->workspaceHubFilterStateResetter()
|
||||
->forgetPersistedEnvironmentLikeFilters($this->getTableFiltersSessionKey(), $request);
|
||||
}
|
||||
|
||||
private function clearWorkspaceHubEnvironmentTableFilterState(): void
|
||||
{
|
||||
$resetter = $this->workspaceHubFilterStateResetter();
|
||||
|
||||
$tableFilters = $this->tableFilters ?? null;
|
||||
|
||||
if (is_array($tableFilters)) {
|
||||
$this->tableFilters = $resetter->clearLivewireTableFilterState($tableFilters);
|
||||
}
|
||||
|
||||
$tableDeferredFilters = $this->tableDeferredFilters ?? null;
|
||||
|
||||
if (is_array($tableDeferredFilters)) {
|
||||
$this->tableDeferredFilters = $resetter->clearLivewireTableFilterState($tableDeferredFilters);
|
||||
}
|
||||
}
|
||||
|
||||
private function workspaceHubFilterStateResetter(): WorkspaceHubFilterStateResetter
|
||||
{
|
||||
return app(WorkspaceHubFilterStateResetter::class);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\Pages\Governance;
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Filament\Resources\FindingExceptionResource;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\ManagedEnvironment;
|
||||
@ -14,7 +15,6 @@
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\Badges\BadgeDomain;
|
||||
use App\Support\Badges\BadgeRenderer;
|
||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\GovernanceDecisions\GovernanceDecisionRegisterBuilder;
|
||||
use App\Support\Navigation\CanonicalNavigationContext;
|
||||
@ -40,6 +40,7 @@
|
||||
|
||||
class DecisionRegister extends Page implements HasTable
|
||||
{
|
||||
use ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static bool $isDiscovered = false;
|
||||
@ -131,10 +132,10 @@ public static function canAccess(): bool
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
app(CanonicalAdminTenantFilterState::class)
|
||||
->forgetEnvironmentLikeFiltersForCleanWorkspaceHubEntry($this->getTableFiltersSessionKey(), request());
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
|
||||
$this->mountInteractsWithTable();
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->authorizeWorkspaceMembership();
|
||||
$this->applyRequestedTenantPrefilter();
|
||||
$this->registerState = $this->resolveRequestedRegisterState();
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\Pages\Monitoring;
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
@ -11,7 +12,6 @@
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
@ -41,6 +41,7 @@
|
||||
|
||||
class EvidenceOverview extends Page implements HasTable
|
||||
{
|
||||
use ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected const MONITORING_PAGE_STATE_CONTRACT = [
|
||||
@ -152,14 +153,12 @@ public static function monitoringPageStateContract(): array
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorizeWorkspaceAccess();
|
||||
if (! request()->query->has('environment_id')) {
|
||||
app(CanonicalAdminTenantFilterState::class)
|
||||
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
|
||||
}
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->seedTableStateFromQuery();
|
||||
$this->rows = $this->rowsForState($this->tableFilters ?? [], $this->tableSearch)->values()->all();
|
||||
|
||||
$this->mountInteractsWithTable();
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->rows = $this->rowsForState($this->tableFilters ?? [], $this->tableSearch)->values()->all();
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
@ -252,10 +251,11 @@ public function clearOverviewFilters(): void
|
||||
$this->tableSearch = '';
|
||||
$this->rows = $this->rowsForState($this->tableFilters, $this->tableSearch)->values()->all();
|
||||
|
||||
session()->put($this->getTableFiltersSessionKey(), $this->tableFilters);
|
||||
session()->forget($this->getTableFiltersSessionKey());
|
||||
session()->put($this->getTableSearchSessionKey(), $this->tableSearch);
|
||||
$this->clearWorkspaceHubEnvironmentFilterState(request());
|
||||
|
||||
$this->redirect($this->overviewUrl(), navigate: true);
|
||||
$this->redirectToCleanWorkspaceHubUrl($this->overviewUrl(), request());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -271,7 +271,7 @@ public function environmentFilterChip(): ?array
|
||||
|
||||
return [
|
||||
'label' => (string) $tenant->name,
|
||||
'clear_url' => $this->overviewUrl(),
|
||||
'clear_url' => $this->cleanWorkspaceHubUrl($this->overviewUrl()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\Pages\Monitoring;
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Filament\Resources\FindingExceptionResource;
|
||||
use App\Filament\Resources\FindingResource;
|
||||
use App\Models\FindingException;
|
||||
@ -51,6 +52,7 @@
|
||||
|
||||
class FindingExceptionsQueue extends Page implements HasTable
|
||||
{
|
||||
use ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected const MONITORING_PAGE_STATE_CONTRACT = [
|
||||
@ -193,12 +195,10 @@ public static function canAccess(): bool
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (! request()->query->has('environment_id')) {
|
||||
app(CanonicalAdminTenantFilterState::class)
|
||||
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
|
||||
}
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
|
||||
$this->mountInteractsWithTable();
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->applyRequestedTenantPrefilter();
|
||||
$requestedExceptionId = is_numeric(request()->query('exception')) ? (int) request()->query('exception') : null;
|
||||
|
||||
@ -229,13 +229,7 @@ protected function getHeaderActions(): array
|
||||
->icon('heroicon-o-x-mark')
|
||||
->color('gray')
|
||||
->visible(fn (): bool => $this->hasActiveQueueFilters())
|
||||
->action(function (): void {
|
||||
$this->removeTableFilter('managed_environment_id');
|
||||
$this->removeTableFilter('status');
|
||||
$this->removeTableFilter('current_validity_state');
|
||||
$this->selectedFindingExceptionId = null;
|
||||
$this->resetTable();
|
||||
});
|
||||
->action(fn (): mixed => $this->clearQueueFilters());
|
||||
|
||||
$actions[] = Action::make('view_tenant_register')
|
||||
->label('View environment findings')
|
||||
@ -448,13 +442,7 @@ public function table(Table $table): Table
|
||||
->label('Clear filters')
|
||||
->icon('heroicon-o-x-mark')
|
||||
->color('gray')
|
||||
->action(function (): void {
|
||||
$this->removeTableFilter('managed_environment_id');
|
||||
$this->removeTableFilter('status');
|
||||
$this->removeTableFilter('current_validity_state');
|
||||
$this->selectedFindingExceptionId = null;
|
||||
$this->resetTable();
|
||||
}),
|
||||
->action(fn (): mixed => $this->clearQueueFilters()),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -520,6 +508,22 @@ public function clearSelectedException(): void
|
||||
$this->selectedFindingExceptionId = null;
|
||||
}
|
||||
|
||||
public function clearQueueFilters(): void
|
||||
{
|
||||
$hadEnvironmentFilter = $this->currentTenantFilterId() !== null;
|
||||
|
||||
$this->removeTableFilter('managed_environment_id');
|
||||
$this->removeTableFilter('status');
|
||||
$this->removeTableFilter('current_validity_state');
|
||||
$this->selectedFindingExceptionId = null;
|
||||
$this->clearWorkspaceHubEnvironmentFilterState(request());
|
||||
$this->resetTable();
|
||||
|
||||
if ($hadEnvironmentFilter) {
|
||||
$this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label: string, clear_url: string}|null
|
||||
*/
|
||||
@ -533,7 +537,7 @@ public function environmentFilterChip(): ?array
|
||||
|
||||
return [
|
||||
'label' => (string) $tenant->name,
|
||||
'clear_url' => static::getUrl(panel: 'admin'),
|
||||
'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\Pages\Monitoring;
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Filament\Resources\OperationRunResource;
|
||||
use App\Filament\Widgets\Operations\OperationsKpiHeader;
|
||||
use App\Models\ManagedEnvironment;
|
||||
@ -42,6 +43,7 @@
|
||||
|
||||
class Operations extends Page implements HasForms, HasTable
|
||||
{
|
||||
use ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
|
||||
@ -200,16 +202,10 @@ public function mount(): void
|
||||
|
||||
$this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null;
|
||||
|
||||
if (! request()->query->has('environment_id')) {
|
||||
app(CanonicalAdminTenantFilterState::class)
|
||||
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
|
||||
}
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
|
||||
$this->mountInteractsWithTable();
|
||||
if (! request()->query->has('environment_id')) {
|
||||
$this->tableFilters['managed_environment_id']['value'] = null;
|
||||
$this->tableDeferredFilters['managed_environment_id']['value'] = null;
|
||||
}
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->applyRequestedDashboardPrefilter();
|
||||
}
|
||||
|
||||
@ -342,9 +338,9 @@ public function environmentFilterChip(): ?array
|
||||
|
||||
return [
|
||||
'label' => (string) $tenant->name,
|
||||
'clear_url' => route('admin.operations.index', [
|
||||
'clear_url' => $this->cleanWorkspaceHubUrl(route('admin.operations.index', [
|
||||
'workspace' => app(WorkspaceContext::class)->currentWorkspace(request()),
|
||||
]),
|
||||
])),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\Pages\Reviews;
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Filament\Resources\EnvironmentReviewResource;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
@ -18,7 +19,6 @@
|
||||
use App\Support\Audit\AuditActionId;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\EnvironmentReviewCompletenessState;
|
||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Findings\FindingOutcomeSemantics;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
@ -50,6 +50,7 @@
|
||||
|
||||
class CustomerReviewWorkspace extends Page implements HasTable
|
||||
{
|
||||
use ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use InteractsWithTable;
|
||||
|
||||
public const string DETAIL_CONTEXT_QUERY_KEY = 'customer_workspace';
|
||||
@ -113,12 +114,10 @@ public static function tenantPrefilterUrl(ManagedEnvironment $tenant): string
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorizePageAccess();
|
||||
if (! request()->query->has('environment_id')) {
|
||||
app(CanonicalAdminTenantFilterState::class)
|
||||
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
|
||||
}
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->applyRequestedTenantPrefilter();
|
||||
$this->mountInteractsWithTable();
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
$this->auditWorkspaceOpen();
|
||||
}
|
||||
|
||||
@ -258,7 +257,7 @@ public function environmentFilterChip(): ?array
|
||||
|
||||
return [
|
||||
'label' => (string) $tenant->name,
|
||||
'clear_url' => static::getUrl(panel: 'admin'),
|
||||
'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')),
|
||||
];
|
||||
}
|
||||
|
||||
@ -431,7 +430,14 @@ private function hasActiveFilters(): bool
|
||||
|
||||
private function clearWorkspaceFilters(): void
|
||||
{
|
||||
$hadEnvironmentFilter = $this->currentTenantFilterId() !== null;
|
||||
|
||||
$this->removeTableFilters();
|
||||
$this->clearWorkspaceHubEnvironmentFilterState(request());
|
||||
|
||||
if ($hadEnvironmentFilter) {
|
||||
$this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request());
|
||||
}
|
||||
}
|
||||
|
||||
private function workspaceEmptyStateHeading(): string
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\Pages\Reviews;
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Filament\Resources\EnvironmentReviewResource;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\ManagedEnvironment;
|
||||
@ -16,7 +17,6 @@
|
||||
use App\Support\Badges\BadgeDomain;
|
||||
use App\Support\Badges\BadgeRenderer;
|
||||
use App\Support\EnvironmentReviewCompletenessState;
|
||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||
use App\Support\Filament\FilterPresets;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Findings\FindingOutcomeSemantics;
|
||||
@ -45,6 +45,7 @@
|
||||
|
||||
class ReviewRegister extends Page implements HasTable
|
||||
{
|
||||
use ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static bool $isDiscovered = false;
|
||||
@ -80,13 +81,11 @@ public function mount(): void
|
||||
{
|
||||
$this->authorizePageAccess();
|
||||
|
||||
if (! request()->query->has('environment_id')) {
|
||||
app(CanonicalAdminTenantFilterState::class)
|
||||
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
|
||||
}
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
|
||||
$this->applyRequestedTenantPrefilter();
|
||||
$this->mountInteractsWithTable();
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
@ -241,7 +240,7 @@ public function environmentFilterChip(): ?array
|
||||
|
||||
return [
|
||||
'label' => (string) $tenant->name,
|
||||
'clear_url' => static::getUrl(panel: 'admin'),
|
||||
'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')),
|
||||
];
|
||||
}
|
||||
|
||||
@ -338,7 +337,14 @@ private function hasActiveFilters(): bool
|
||||
|
||||
private function clearRegisterFilters(): void
|
||||
{
|
||||
$hadEnvironmentFilter = $this->currentTenantFilterId() !== null;
|
||||
|
||||
$this->removeTableFilters();
|
||||
$this->clearWorkspaceHubEnvironmentFilterState(request());
|
||||
|
||||
if ($hadEnvironmentFilter) {
|
||||
$this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request());
|
||||
}
|
||||
}
|
||||
|
||||
private function currentTenantFilterId(): ?int
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Filament\Resources\ProviderConnectionResource\Pages;
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Filament\Resources\ProviderConnectionResource;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\CapabilityResolver;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Schemas\Components\EmbeddedTable;
|
||||
@ -18,16 +18,16 @@
|
||||
|
||||
class ListProviderConnections extends ListRecords
|
||||
{
|
||||
use ClearsWorkspaceHubEnvironmentFilterState;
|
||||
|
||||
protected static string $resource = ProviderConnectionResource::class;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (! request()->query->has('environment_id')) {
|
||||
app(CanonicalAdminTenantFilterState::class)
|
||||
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
|
||||
}
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
|
||||
parent::mount();
|
||||
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
||||
}
|
||||
|
||||
private function tableHasRecords(): bool
|
||||
@ -256,7 +256,7 @@ private function environmentFilterChip(): ?array
|
||||
|
||||
return [
|
||||
'label' => (string) $environment->name,
|
||||
'clear_url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
|
||||
'clear_url' => $this->cleanWorkspaceHubUrl(ProviderConnectionResource::getUrl('index', panel: 'admin')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
namespace App\Support\Filament;
|
||||
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Support\Navigation\WorkspaceHubFilterStateResetter;
|
||||
use App\Support\Navigation\WorkspaceHubRegistry;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use Illuminate\Http\Request;
|
||||
@ -15,7 +16,10 @@ final class CanonicalAdminTenantFilterState
|
||||
{
|
||||
private const STATE_PREFIX = 'filament.admin_tenant_filter_state';
|
||||
|
||||
public function __construct(private readonly OperateHubShell $operateHubShell) {}
|
||||
public function __construct(
|
||||
private readonly OperateHubShell $operateHubShell,
|
||||
private readonly WorkspaceHubFilterStateResetter $workspaceHubFilterStateResetter,
|
||||
) {}
|
||||
|
||||
public function currentFilterValue(
|
||||
string $filtersSessionKey,
|
||||
@ -119,15 +123,8 @@ public function forgetEnvironmentLikeFilters(
|
||||
$persistedFilters = [];
|
||||
}
|
||||
|
||||
foreach (WorkspaceHubRegistry::environmentLikeFilterKeys() as $filterName) {
|
||||
Arr::forget($persistedFilters, $filterName);
|
||||
}
|
||||
|
||||
if ($persistedFilters === []) {
|
||||
$session->forget($filtersSessionKey);
|
||||
} else {
|
||||
$session->put($filtersSessionKey, $persistedFilters);
|
||||
}
|
||||
$persistedFilters = $this->workspaceHubFilterStateResetter
|
||||
->forgetPersistedEnvironmentLikeFilters($filtersSessionKey, $request, $persistedFilters);
|
||||
|
||||
$session->forget($this->stateKey($filtersSessionKey));
|
||||
|
||||
|
||||
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Navigation;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Session\Store;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
final class WorkspaceHubFilterStateResetter
|
||||
{
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function environmentLikeFilterKeys(): array
|
||||
{
|
||||
return WorkspaceHubRegistry::environmentLikeFilterKeys();
|
||||
}
|
||||
|
||||
public function shouldResetForCleanWorkspaceHubEntry(?Request $request = null): bool
|
||||
{
|
||||
return WorkspaceHubRegistry::isCleanWorkspaceHubEntry($request);
|
||||
}
|
||||
|
||||
public function neutralizeEnvironmentLikeQueryState(?Request $request = null): void
|
||||
{
|
||||
$request ??= request();
|
||||
$hasCanonicalEnvironmentFilter = WorkspaceHubRegistry::requestHasEnvironmentFilterQuery($request);
|
||||
|
||||
foreach (WorkspaceHubRegistry::forbiddenQueryKeys() as $queryKey) {
|
||||
if ($queryKey === 'environment_id' && $hasCanonicalEnvironmentFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$request->query->remove($queryKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $persistedFilters
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function forgetPersistedEnvironmentLikeFilters(
|
||||
string $filtersSessionKey,
|
||||
?Request $request = null,
|
||||
?array $persistedFilters = null,
|
||||
): array {
|
||||
$session = $this->session($request);
|
||||
|
||||
$persistedFilters ??= $session->get($filtersSessionKey, []);
|
||||
|
||||
if (! is_array($persistedFilters)) {
|
||||
$persistedFilters = [];
|
||||
}
|
||||
|
||||
$persistedFilters = $this->forgetEnvironmentLikeFilterPaths($persistedFilters);
|
||||
|
||||
if ($persistedFilters === []) {
|
||||
$session->forget($filtersSessionKey);
|
||||
} else {
|
||||
$session->put($filtersSessionKey, $persistedFilters);
|
||||
}
|
||||
|
||||
return $persistedFilters;
|
||||
}
|
||||
|
||||
public function forgetPersistedEnvironmentLikeFiltersForCleanWorkspaceHubEntry(
|
||||
string $filtersSessionKey,
|
||||
?Request $request = null,
|
||||
): void {
|
||||
if (! $this->shouldResetForCleanWorkspaceHubEntry($request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forgetPersistedEnvironmentLikeFilters($filtersSessionKey, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $filters
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function clearLivewireTableFilterState(?array $filters): array
|
||||
{
|
||||
if (! is_array($filters)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($this->environmentLikeFilterKeys() as $filterName) {
|
||||
if (! array_key_exists($filterName, $filters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filterState = $filters[$filterName];
|
||||
|
||||
if (is_array($filterState)) {
|
||||
$filters[$filterName] = ['value' => null];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Arr::forget($filters, $filterName);
|
||||
}
|
||||
|
||||
return $this->forgetNestedEnvironmentLikeFilterState($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function cleanParameters(array $parameters): array
|
||||
{
|
||||
return WorkspaceHubRegistry::cleanParameters($parameters);
|
||||
}
|
||||
|
||||
public function cleanUrl(string $url): string
|
||||
{
|
||||
return WorkspaceHubRegistry::cleanUrl($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $state
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function forgetEnvironmentLikeFilterPaths(array $state): array
|
||||
{
|
||||
foreach ($this->environmentLikeFilterKeys() as $filterName) {
|
||||
Arr::forget($state, $filterName);
|
||||
}
|
||||
|
||||
return $this->forgetNestedEnvironmentLikeFilterState($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $state
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function forgetNestedEnvironmentLikeFilterState(array $state): array
|
||||
{
|
||||
foreach ($this->environmentLikeFilterKeys() as $filterName) {
|
||||
Arr::forget($state, "tableFilters.{$filterName}");
|
||||
Arr::forget($state, "tableDeferredFilters.{$filterName}");
|
||||
}
|
||||
|
||||
foreach (['tableFilters', 'tableDeferredFilters'] as $tableStateKey) {
|
||||
if (data_get($state, $tableStateKey) === []) {
|
||||
Arr::forget($state, $tableStateKey);
|
||||
}
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
private function session(?Request $request = null): Store
|
||||
{
|
||||
return ($request && $request->hasSession()) ? $request->session() : app('session.store');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Governance\DecisionRegister;
|
||||
use App\Filament\Pages\Governance\GovernanceInbox;
|
||||
use App\Filament\Pages\Monitoring\EvidenceOverview;
|
||||
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||
use App\Filament\Resources\ProviderConnectionResource;
|
||||
use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\FindingExceptionDecision;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\User;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Livewire\Livewire;
|
||||
|
||||
pest()->browser()->timeout(60_000);
|
||||
|
||||
it('Spec316 smokes filtered workspace hub clear and reload behavior', function (): void {
|
||||
[$user, $environmentA, $environmentB] = spec316BrowserClearFilterWorkspace();
|
||||
$workspace = $environmentA->workspace()->firstOrFail();
|
||||
|
||||
$this->actingAs($user)->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
||||
(string) $workspace->getKey() => (int) $environmentA->getKey(),
|
||||
],
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
||||
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
|
||||
(string) $workspace->getKey() => (int) $environmentA->getKey(),
|
||||
]);
|
||||
|
||||
$hubs = [
|
||||
'operations' => [
|
||||
'filtered_url' => OperationRunLinks::index($environmentA),
|
||||
'clean_url' => OperationRunLinks::index(),
|
||||
'wide_text' => 'Inventory sync',
|
||||
],
|
||||
'provider connections' => [
|
||||
'filtered_url' => ProviderConnectionResource::getUrl('index', [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
], panel: 'admin'),
|
||||
'clean_url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
|
||||
'wide_text' => 'Spec316 Browser Provider B',
|
||||
],
|
||||
'finding exceptions queue' => [
|
||||
'filtered_url' => FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
]),
|
||||
'clean_url' => FindingExceptionsQueue::getUrl(panel: 'admin'),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
'evidence overview' => [
|
||||
'filtered_url' => route('admin.evidence.overview', [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
]),
|
||||
'clean_url' => route('admin.evidence.overview'),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
'review register' => [
|
||||
'filtered_url' => ReviewRegister::getUrl(panel: 'admin', parameters: [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
]),
|
||||
'clean_url' => ReviewRegister::getUrl(panel: 'admin'),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
'customer review workspace' => [
|
||||
'filtered_url' => CustomerReviewWorkspace::tenantPrefilterUrl($environmentA),
|
||||
'clean_url' => CustomerReviewWorkspace::getUrl(panel: 'admin'),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
'governance inbox' => [
|
||||
'filtered_url' => GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
]),
|
||||
'clean_url' => GovernanceInbox::getUrl(panel: 'admin'),
|
||||
'wide_text' => 'Spec316 Browser Governance B',
|
||||
],
|
||||
'decision register' => [
|
||||
'filtered_url' => DecisionRegister::getUrl(panel: 'admin', parameters: [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
]),
|
||||
'clean_url' => DecisionRegister::getUrl(panel: 'admin'),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($hubs as $hub) {
|
||||
$cleanPath = json_encode((string) parse_url($hub['clean_url'], PHP_URL_PATH), JSON_THROW_ON_ERROR);
|
||||
|
||||
$page = visit($hub['filtered_url'])
|
||||
->waitForText('Environment filter:')
|
||||
->assertSee($environmentA->name)
|
||||
->assertDontSee($hub['wide_text'])
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
$page
|
||||
->click('[data-testid="workspace-hub-environment-filter-clear"]')
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee($hub['wide_text'])
|
||||
->assertScript("window.location.pathname === {$cleanPath}", true)
|
||||
->assertScript('! window.location.search.includes("environment_id=")', true)
|
||||
->assertScript('! window.location.search.includes("tenant=")', true)
|
||||
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
|
||||
->assertScript('! window.location.search.includes("tenant_scope=")', true)
|
||||
->assertScript('! window.location.search.includes("tableFilters")', true)
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
$page->script('window.location.reload();');
|
||||
|
||||
$page
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee($hub['wide_text'])
|
||||
->assertScript("window.location.pathname === {$cleanPath}", true)
|
||||
->assertScript('! window.location.search.includes("environment_id=")', true)
|
||||
->assertNoJavaScriptErrors();
|
||||
}
|
||||
});
|
||||
|
||||
it('Spec316 smokes browser back and forward alignment for high risk hubs', function (): void {
|
||||
[$user, $environmentA, $environmentB] = spec316BrowserClearFilterWorkspace();
|
||||
$workspace = $environmentA->workspace()->firstOrFail();
|
||||
|
||||
$this->actingAs($user)->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
||||
|
||||
$hubs = [
|
||||
ProviderConnectionResource::getUrl('index', [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
], panel: 'admin'),
|
||||
FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
]),
|
||||
CustomerReviewWorkspace::tenantPrefilterUrl($environmentA),
|
||||
route('admin.evidence.overview', [
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
]),
|
||||
];
|
||||
|
||||
foreach ($hubs as $filteredUrl) {
|
||||
$page = visit($filteredUrl)
|
||||
->waitForText('Environment filter:')
|
||||
->assertSee($environmentA->name)
|
||||
->assertDontSee($environmentB->name);
|
||||
|
||||
$page
|
||||
->click('[data-testid="workspace-hub-environment-filter-clear"]')
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee($environmentB->name);
|
||||
|
||||
$page->script('window.history.back();');
|
||||
|
||||
$page
|
||||
->waitForText('Environment filter:')
|
||||
->assertSee($environmentA->name)
|
||||
->assertDontSee($environmentB->name)
|
||||
->assertScript('window.location.search.includes("environment_id=")', true);
|
||||
|
||||
$page->script('window.history.forward();');
|
||||
|
||||
$page
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee($environmentB->name)
|
||||
->assertScript('! window.location.search.includes("environment_id=")', true)
|
||||
->assertNoJavaScriptErrors();
|
||||
}
|
||||
});
|
||||
|
||||
it('Spec316 smokes persisted environment filters do not survive clean browser entry', function (): void {
|
||||
[$user, $environmentA, $environmentB] = spec316BrowserClearFilterWorkspace();
|
||||
$workspace = $environmentA->workspace()->firstOrFail();
|
||||
|
||||
$this->actingAs($user)->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
||||
|
||||
$cases = [
|
||||
[
|
||||
'component' => ListProviderConnections::class,
|
||||
'url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
|
||||
'filter_name' => 'tenant',
|
||||
'filter_value' => (string) $environmentA->external_id,
|
||||
'wide_text' => 'Spec316 Browser Provider B',
|
||||
],
|
||||
[
|
||||
'component' => FindingExceptionsQueue::class,
|
||||
'url' => FindingExceptionsQueue::getUrl(panel: 'admin'),
|
||||
'filter_name' => 'managed_environment_id',
|
||||
'filter_value' => (string) $environmentA->getKey(),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
[
|
||||
'component' => CustomerReviewWorkspace::class,
|
||||
'url' => CustomerReviewWorkspace::getUrl(panel: 'admin'),
|
||||
'filter_name' => 'managed_environment_id',
|
||||
'filter_value' => (string) $environmentA->getKey(),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
[
|
||||
'component' => EvidenceOverview::class,
|
||||
'url' => route('admin.evidence.overview'),
|
||||
'filter_name' => 'managed_environment_id',
|
||||
'filter_value' => (string) $environmentA->getKey(),
|
||||
'wide_text' => $environmentB->name,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($cases as $case) {
|
||||
$component = Livewire::actingAs($user)->test($case['component']);
|
||||
$filtersSessionKey = $component->instance()->getTableFiltersSessionKey();
|
||||
|
||||
session()->put($filtersSessionKey, [
|
||||
$case['filter_name'] => ['value' => $case['filter_value']],
|
||||
]);
|
||||
|
||||
visit($case['url'])
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee($case['wide_text'])
|
||||
->assertNoJavaScriptErrors();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment}
|
||||
*/
|
||||
function spec316BrowserClearFilterWorkspace(): array
|
||||
{
|
||||
$environmentA = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec316 Browser Environment A',
|
||||
'external_id' => 'spec316-browser-environment-a',
|
||||
]);
|
||||
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
$environmentB = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'name' => 'Spec316 Browser Environment B',
|
||||
'external_id' => 'spec316-browser-environment-b',
|
||||
]);
|
||||
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
|
||||
OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
|
||||
|
||||
spec316BrowserFindingException($environmentA, $user, 'Spec316 Browser Governance A', 'Spec316 Browser Decision A');
|
||||
spec316BrowserFindingException($environmentB, $user, 'Spec316 Browser Governance B', 'Spec316 Browser Decision B');
|
||||
|
||||
ProviderConnection::factory()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentA->getKey(),
|
||||
'display_name' => 'Spec316 Browser Provider A',
|
||||
]);
|
||||
ProviderConnection::factory()->create([
|
||||
'workspace_id' => (int) $environmentB->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentB->getKey(),
|
||||
'display_name' => 'Spec316 Browser Provider B',
|
||||
]);
|
||||
|
||||
$snapshotA = spec316BrowserEvidenceSnapshot($environmentA);
|
||||
$snapshotB = spec316BrowserEvidenceSnapshot($environmentB);
|
||||
|
||||
spec316BrowserPublishedReview($environmentA, $user, $snapshotA);
|
||||
spec316BrowserPublishedReview($environmentB, $user, $snapshotB);
|
||||
|
||||
return [$user, $environmentA, $environmentB];
|
||||
}
|
||||
|
||||
function spec316BrowserFindingException(
|
||||
ManagedEnvironment $environment,
|
||||
User $actor,
|
||||
string $requestReason,
|
||||
string $decisionReason,
|
||||
): FindingException {
|
||||
$finding = Finding::factory()->for($environment)->riskAccepted()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'subject_external_id' => str()->slug($requestReason),
|
||||
]);
|
||||
|
||||
$exception = FindingException::query()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'finding_id' => (int) $finding->getKey(),
|
||||
'requested_by_user_id' => (int) $actor->getKey(),
|
||||
'owner_user_id' => (int) $actor->getKey(),
|
||||
'status' => FindingException::STATUS_PENDING,
|
||||
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
'request_reason' => $requestReason,
|
||||
'requested_at' => now()->subDay(),
|
||||
'review_due_at' => now()->addDay(),
|
||||
'evidence_summary' => ['reference_count' => 0],
|
||||
]);
|
||||
|
||||
$decision = $exception->decisions()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'actor_user_id' => (int) $actor->getKey(),
|
||||
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
||||
'reason' => $decisionReason,
|
||||
'metadata' => [],
|
||||
'decided_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
|
||||
|
||||
return $exception->fresh(['currentDecision']);
|
||||
}
|
||||
|
||||
function spec316BrowserEvidenceSnapshot(ManagedEnvironment $environment): EvidenceSnapshot
|
||||
{
|
||||
return EvidenceSnapshot::query()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'summary' => ['missing_dimensions' => 0, 'stale_dimensions' => 0],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
function spec316BrowserPublishedReview(ManagedEnvironment $environment, User $user, EvidenceSnapshot $snapshot): void
|
||||
{
|
||||
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
||||
$review->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
}
|
||||
@ -101,7 +101,13 @@
|
||||
|
||||
$component
|
||||
->callAction('clear_filters')
|
||||
->assertActionHidden('clear_filters')
|
||||
->assertRedirect(ReviewRegister::getUrl(panel: 'admin'));
|
||||
|
||||
Livewire::withHeaders(['referer' => ReviewRegister::getUrl(panel: 'admin')])
|
||||
->withQueryParams([])
|
||||
->actingAs($user)
|
||||
->test(ReviewRegister::class)
|
||||
->assertSet('tableFilters.managed_environment_id.value', null)
|
||||
->assertCanSeeTableRecords([$reviewA, $reviewB]);
|
||||
|
||||
expect(app(WorkspaceContext::class)->lastTenantId())->toBe((int) $tenantA->getKey());
|
||||
|
||||
@ -81,8 +81,8 @@
|
||||
'tenant' => (string) $tenantB->external_id,
|
||||
])
|
||||
->test(FindingExceptionsQueue::class)
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
|
||||
->assertActionVisible('view_tenant_register');
|
||||
->assertSet('tableFilters.managed_environment_id.value', null)
|
||||
->assertActionHidden('view_tenant_register');
|
||||
|
||||
$filtersComponent = Livewire::test(FindingExceptionsQueue::class);
|
||||
$queueInstance = $filtersComponent->instance();
|
||||
|
||||
@ -0,0 +1,335 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Governance\DecisionRegister;
|
||||
use App\Filament\Pages\Governance\GovernanceInbox;
|
||||
use App\Filament\Pages\Monitoring\EvidenceOverview;
|
||||
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
||||
use App\Filament\Pages\Monitoring\Operations;
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||
use App\Filament\Resources\ProviderConnectionResource;
|
||||
use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\FindingExceptionDecision;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\User;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\Navigation\WorkspaceHubFilterStateResetter;
|
||||
use App\Support\Navigation\WorkspaceHubRegistry;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Livewire\Livewire;
|
||||
|
||||
it('Spec316 resetter removes environment-like persisted table filters without deleting unrelated filters', function (): void {
|
||||
$filtersSessionKey = 'filament.spec316.filters';
|
||||
|
||||
session()->put($filtersSessionKey, [
|
||||
'tenant' => ['value' => 'legacy-tenant'],
|
||||
'tenant_id' => ['value' => '101'],
|
||||
'managed_environment_id' => ['value' => '101'],
|
||||
'environment_id' => ['value' => '101'],
|
||||
'environment' => ['value' => 'legacy-environment'],
|
||||
'tenant_scope' => ['value' => 'environment'],
|
||||
'tableFilters' => [
|
||||
'tenant' => ['value' => 'legacy-tenant'],
|
||||
'managed_environment_id' => ['value' => '101'],
|
||||
'status' => ['value' => 'pending'],
|
||||
],
|
||||
'status' => ['value' => 'pending'],
|
||||
]);
|
||||
|
||||
app(WorkspaceHubFilterStateResetter::class)
|
||||
->forgetPersistedEnvironmentLikeFilters($filtersSessionKey, request());
|
||||
|
||||
$persistedFilters = session()->get($filtersSessionKey);
|
||||
|
||||
expect($persistedFilters)
|
||||
->toMatchArray([
|
||||
'status' => ['value' => 'pending'],
|
||||
'tableFilters' => [
|
||||
'status' => ['value' => 'pending'],
|
||||
],
|
||||
]);
|
||||
|
||||
foreach (WorkspaceHubRegistry::environmentLikeFilterKeys() as $filterName) {
|
||||
expect(data_get($persistedFilters, "{$filterName}.value"))->toBeNull()
|
||||
->and(data_get($persistedFilters, "tableFilters.{$filterName}.value"))->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('Spec316 clear filter result is reload safe across table backed workspace hubs', function (): void {
|
||||
[$user, $environmentA, $environmentB, $records] = spec316ClearFilterWorkspace();
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
|
||||
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
|
||||
(string) $environmentA->workspace_id => (int) $environmentA->getKey(),
|
||||
]);
|
||||
|
||||
$cases = [
|
||||
'operations' => [
|
||||
'component' => Operations::class,
|
||||
'clean_url' => OperationRunLinks::index(),
|
||||
'filtered_records' => [$records['runA']],
|
||||
'hidden_records' => [$records['runB']],
|
||||
'wide_records' => [$records['runA'], $records['runB']],
|
||||
'session_environment_value' => (string) $environmentA->getKey(),
|
||||
],
|
||||
'finding exceptions queue' => [
|
||||
'component' => FindingExceptionsQueue::class,
|
||||
'clean_url' => FindingExceptionsQueue::getUrl(panel: 'admin'),
|
||||
'filtered_records' => [$records['exceptionA']],
|
||||
'hidden_records' => [$records['exceptionB']],
|
||||
'wide_records' => [$records['exceptionA'], $records['exceptionB']],
|
||||
'session_environment_value' => (string) $environmentA->getKey(),
|
||||
],
|
||||
'provider connections' => [
|
||||
'component' => ListProviderConnections::class,
|
||||
'clean_url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
|
||||
'filtered_records' => [$records['connectionA']],
|
||||
'hidden_records' => [$records['connectionB']],
|
||||
'wide_records' => [$records['connectionA'], $records['connectionB']],
|
||||
'session_environment_value' => (string) $environmentA->external_id,
|
||||
],
|
||||
'evidence overview' => [
|
||||
'component' => EvidenceOverview::class,
|
||||
'clean_url' => route('admin.evidence.overview'),
|
||||
'filtered_records' => [(string) $records['snapshotA']->getKey()],
|
||||
'hidden_records' => [(string) $records['snapshotB']->getKey()],
|
||||
'wide_records' => [(string) $records['snapshotA']->getKey(), (string) $records['snapshotB']->getKey()],
|
||||
'session_environment_value' => (string) $environmentA->getKey(),
|
||||
],
|
||||
'review register' => [
|
||||
'component' => ReviewRegister::class,
|
||||
'clean_url' => ReviewRegister::getUrl(panel: 'admin'),
|
||||
'filtered_records' => [$records['reviewA']->fresh()],
|
||||
'hidden_records' => [$records['reviewB']->fresh()],
|
||||
'wide_records' => [$records['reviewA']->fresh(), $records['reviewB']->fresh()],
|
||||
'session_environment_value' => (string) $environmentA->getKey(),
|
||||
],
|
||||
'customer review workspace' => [
|
||||
'component' => CustomerReviewWorkspace::class,
|
||||
'clean_url' => CustomerReviewWorkspace::getUrl(panel: 'admin'),
|
||||
'filtered_records' => [$environmentA->fresh()],
|
||||
'hidden_records' => [$environmentB->fresh()],
|
||||
'wide_records' => [$environmentA->fresh(), $environmentB->fresh()],
|
||||
'session_environment_value' => (string) $environmentA->getKey(),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($cases as $case) {
|
||||
$filteredComponent = Livewire::withQueryParams(['environment_id' => (int) $environmentA->getKey()])
|
||||
->actingAs($user)
|
||||
->test($case['component'])
|
||||
->assertSee('Environment filter:')
|
||||
->assertSee('Clear filter')
|
||||
->assertCanSeeTableRecords($case['filtered_records'])
|
||||
->assertCanNotSeeTableRecords($case['hidden_records']);
|
||||
|
||||
$filtersSessionKey = $filteredComponent->instance()->getTableFiltersSessionKey();
|
||||
spec316PersistLegacyEnvironmentFilterState($filtersSessionKey, $case['session_environment_value']);
|
||||
|
||||
$this->get($case['clean_url'])
|
||||
->assertOk()
|
||||
->assertDontSee('Environment filter:');
|
||||
|
||||
spec316AssertEnvironmentLikeFiltersForgotten($filtersSessionKey);
|
||||
|
||||
$this->get($case['clean_url'])
|
||||
->assertOk()
|
||||
->assertDontSee('Environment filter:');
|
||||
|
||||
Livewire::withQueryParams([])
|
||||
->actingAs($user)
|
||||
->test($case['component'])
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertCanSeeTableRecords($case['wide_records']);
|
||||
}
|
||||
});
|
||||
|
||||
it('Spec316 clear filter result is clean for governance and decision workspace hubs', function (): void {
|
||||
[$user, $environmentA, $environmentB, $records] = spec316ClearFilterWorkspace();
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
|
||||
|
||||
$this->get(GovernanceInbox::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environmentA->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Environment filter:')
|
||||
->assertSee($environmentA->name)
|
||||
->assertDontSee('Spec316 Governance B');
|
||||
|
||||
$this->get(GovernanceInbox::getUrl(panel: 'admin'))
|
||||
->assertOk()
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee('Spec316 Governance B');
|
||||
|
||||
$this->get(DecisionRegister::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environmentA->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Environment filter:')
|
||||
->assertSee($environmentA->name)
|
||||
->assertDontSee('Spec316 Decision B');
|
||||
|
||||
$this->get(DecisionRegister::getUrl(panel: 'admin'))
|
||||
->assertOk()
|
||||
->assertDontSee('Environment filter:');
|
||||
|
||||
Livewire::withQueryParams([])
|
||||
->actingAs($user)
|
||||
->test(DecisionRegister::class)
|
||||
->assertCanSeeTableRecords([$records['exceptionA'], $records['exceptionB']]);
|
||||
|
||||
expect($environmentB->workspace_id)->toBe($environmentA->workspace_id);
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment, 3: array<string, mixed>}
|
||||
*/
|
||||
function spec316ClearFilterWorkspace(): array
|
||||
{
|
||||
$environmentA = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec316 Environment A',
|
||||
'external_id' => 'spec316-environment-a',
|
||||
]);
|
||||
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
$environmentB = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'name' => 'Spec316 Environment B',
|
||||
'external_id' => 'spec316-environment-b',
|
||||
]);
|
||||
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
|
||||
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
|
||||
|
||||
$exceptionA = spec316ClearFilterFindingException($environmentA, $user, 'Spec316 Governance A', 'Spec316 Decision A');
|
||||
$exceptionB = spec316ClearFilterFindingException($environmentB, $user, 'Spec316 Governance B', 'Spec316 Decision B');
|
||||
|
||||
$connectionA = ProviderConnection::factory()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentA->getKey(),
|
||||
'display_name' => 'Spec316 Provider A',
|
||||
]);
|
||||
$connectionB = ProviderConnection::factory()->create([
|
||||
'workspace_id' => (int) $environmentB->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentB->getKey(),
|
||||
'display_name' => 'Spec316 Provider B',
|
||||
]);
|
||||
|
||||
$snapshotA = spec316ClearFilterEvidenceSnapshot($environmentA);
|
||||
$snapshotB = spec316ClearFilterEvidenceSnapshot($environmentB);
|
||||
|
||||
$reviewA = composeEnvironmentReviewForTest($environmentA, $user, $snapshotA);
|
||||
$reviewA->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
$reviewB = composeEnvironmentReviewForTest($environmentB, $user, $snapshotB);
|
||||
$reviewB->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
return [$user, $environmentA, $environmentB, compact(
|
||||
'runA',
|
||||
'runB',
|
||||
'exceptionA',
|
||||
'exceptionB',
|
||||
'connectionA',
|
||||
'connectionB',
|
||||
'snapshotA',
|
||||
'snapshotB',
|
||||
'reviewA',
|
||||
'reviewB',
|
||||
)];
|
||||
}
|
||||
|
||||
function spec316ClearFilterFindingException(
|
||||
ManagedEnvironment $environment,
|
||||
User $actor,
|
||||
string $requestReason,
|
||||
string $decisionReason,
|
||||
): FindingException {
|
||||
$finding = Finding::factory()->for($environment)->riskAccepted()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'subject_external_id' => str()->slug($requestReason),
|
||||
]);
|
||||
|
||||
$exception = FindingException::query()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'finding_id' => (int) $finding->getKey(),
|
||||
'requested_by_user_id' => (int) $actor->getKey(),
|
||||
'owner_user_id' => (int) $actor->getKey(),
|
||||
'status' => FindingException::STATUS_PENDING,
|
||||
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
'request_reason' => $requestReason,
|
||||
'requested_at' => now()->subDay(),
|
||||
'review_due_at' => now()->addDay(),
|
||||
'evidence_summary' => ['reference_count' => 0],
|
||||
]);
|
||||
|
||||
$decision = $exception->decisions()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'actor_user_id' => (int) $actor->getKey(),
|
||||
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
||||
'reason' => $decisionReason,
|
||||
'metadata' => [],
|
||||
'decided_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
|
||||
|
||||
return $exception->fresh(['currentDecision']);
|
||||
}
|
||||
|
||||
function spec316ClearFilterEvidenceSnapshot(ManagedEnvironment $environment): EvidenceSnapshot
|
||||
{
|
||||
return EvidenceSnapshot::query()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'summary' => ['missing_dimensions' => 0, 'stale_dimensions' => 0],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
function spec316PersistLegacyEnvironmentFilterState(string $filtersSessionKey, string $environmentValue): void
|
||||
{
|
||||
session()->put($filtersSessionKey, [
|
||||
'tenant' => ['value' => $environmentValue],
|
||||
'tenant_id' => ['value' => $environmentValue],
|
||||
'managed_environment_id' => ['value' => $environmentValue],
|
||||
'environment_id' => ['value' => $environmentValue],
|
||||
'environment' => ['value' => $environmentValue],
|
||||
'tenant_scope' => ['value' => 'environment'],
|
||||
'tableFilters' => [
|
||||
'tenant' => ['value' => $environmentValue],
|
||||
'managed_environment_id' => ['value' => $environmentValue],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
function spec316AssertEnvironmentLikeFiltersForgotten(string $filtersSessionKey): void
|
||||
{
|
||||
$persistedFilters = session()->get($filtersSessionKey, []);
|
||||
|
||||
foreach (WorkspaceHubRegistry::environmentLikeFilterKeys() as $filterName) {
|
||||
expect(data_get($persistedFilters, "{$filterName}.value"))->toBeNull()
|
||||
->and(data_get($persistedFilters, "tableFilters.{$filterName}.value"))->toBeNull();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
# Requirements Checklist: Workspace Hub Clear Filter Contract
|
||||
|
||||
**Spec**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/spec.md)
|
||||
**Plan**: [plan.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/plan.md)
|
||||
**Generated**: 2026-05-16
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] Explicit user-provided Spec 316 request was selected as the source of truth for this preparation pass.
|
||||
- [x] Completed-spec guardrail checked that no existing `specs/316-*` artifact was present before generation.
|
||||
- [x] Specs 313, 314, and 315 were treated as completed historical baseline context, not rewritten.
|
||||
- [x] Close alternatives were identified as follow-up specs 317 and 318 rather than merged into this spec.
|
||||
- [x] The selected slice is clear-filter lifecycle hardening only.
|
||||
|
||||
## Spec Readiness
|
||||
|
||||
- [x] Problem statement is operator-visible and tied to stale clear-filter state.
|
||||
- [x] Hard-cutover policy is explicit.
|
||||
- [x] `environment_id` remains the only canonical Environment filter source.
|
||||
- [x] Legacy keys are explicitly neutralized as filter sources.
|
||||
- [x] URL, Livewire, Filament table, deferred, session/persisted, chip/header, data, reload, and back/forward layers are named.
|
||||
- [x] Required hubs are named.
|
||||
- [x] Optional hub handling is bounded and does not add new filter support.
|
||||
- [x] Workspace isolation and authorization constraints are explicit.
|
||||
- [x] Follow-up boundaries for Specs 317 and 318 are explicit.
|
||||
|
||||
## Plan Readiness
|
||||
|
||||
- [x] Laravel, Filament, Livewire, Pest, and PostgreSQL context is recorded.
|
||||
- [x] No migration, seeder, package, env var, queue, scheduler, or storage change is planned.
|
||||
- [x] Shared reset mechanism proportionality is justified.
|
||||
- [x] Existing repo surfaces are named: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `CanonicalAdminTenantFilterState`, chip partial, and required hub pages/resources.
|
||||
- [x] Provider/platform boundary is classified.
|
||||
- [x] OperationRun impact is N/A.
|
||||
- [x] Browser verification scope is defined.
|
||||
- [x] Deployment impact is assessed.
|
||||
|
||||
## Task Readiness
|
||||
|
||||
- [x] Tasks are ordered from guardrails and tests through runtime changes and validation.
|
||||
- [x] Tests are required before or alongside runtime work.
|
||||
- [x] Critical hubs are named in tasks.
|
||||
- [x] Optional hubs are classified instead of assumed.
|
||||
- [x] Legacy alias neutralization has explicit tasks.
|
||||
- [x] Spec 314 and Spec 315 regressions have explicit tasks.
|
||||
- [x] Browser reload and back/forward flows have explicit tasks.
|
||||
- [x] Non-tasks prevent scope creep into Specs 317 and 318.
|
||||
|
||||
## Constitution / Guardrail Coverage
|
||||
|
||||
- [x] Workspace isolation is covered.
|
||||
- [x] RBAC and no-access behavior are covered.
|
||||
- [x] No Graph write/read integration change is introduced.
|
||||
- [x] No destructive action behavior is introduced.
|
||||
- [x] No persisted truth is introduced.
|
||||
- [x] No status/reason family is introduced.
|
||||
- [x] Filament v5 / Livewire v4 compliance is recorded.
|
||||
- [x] Native/shared UI primitive preference is recorded.
|
||||
- [x] Test governance classification and lanes are recorded.
|
||||
- [x] Proportionality review is completed for the shared reset helper.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [x] No blocking requirements questions remain for preparation.
|
||||
- [x] Runtime implementation must still confirm exact Filament persisted filter/session keys per hub.
|
||||
- [x] Runtime implementation must document optional pages intentionally excluded.
|
||||
- [x] Browser back/forward limitations must be documented if tooling is unstable.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- [x] Spec artifacts are ready for `/spec-kit-implementation-loop` or equivalent implementation pass.
|
||||
- [x] No application implementation was performed during preparation.
|
||||
342
specs/316-workspace-hub-clear-filter-contract/plan.md
Normal file
342
specs/316-workspace-hub-clear-filter-contract/plan.md
Normal file
@ -0,0 +1,342 @@
|
||||
# Implementation Plan: Workspace Hub Clear Filter Contract
|
||||
|
||||
**Branch**: `316-workspace-hub-clear-filter-contract` | **Date**: 2026-05-16 | **Spec**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/spec.md)
|
||||
**Input**: Feature specification from `/specs/316-workspace-hub-clear-filter-contract/spec.md`
|
||||
|
||||
**Preparation status**: Specification artifacts only. No runtime implementation has been performed by this preparation step.
|
||||
|
||||
## Summary
|
||||
|
||||
Spec 316 completes the active Workspace Hub Environment filter lifecycle:
|
||||
|
||||
```text
|
||||
314: sidebar/global entry -> clean workspace-wide hub
|
||||
315: Environment CTA -> workspace hub ?environment_id=...
|
||||
316: Clear filter -> clean workspace-wide hub, reload-safe
|
||||
```
|
||||
|
||||
The implementation must make clear-filter behavior shared, complete, and reliable across URL query state, Livewire/page state, Filament table/deferred filters, persisted/session filters, visible chip state, header/scope wording, rendered data scope, reload, and focused browser history flows.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
|
||||
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Laravel Sail, Laravel Socialite, Laravel MCP
|
||||
**Storage**: PostgreSQL; no schema changes for this spec
|
||||
**Testing**: Pest 4.3.1 / PHPUnit 12.5.4; focused browser smoke where applicable
|
||||
**Validation Lanes**: fast-feedback, confidence for Filament/Livewire state, browser for reload/history smoke
|
||||
**Target Platform**: Laravel admin application under `apps/platform`, local development through Sail, staging/production through Dokploy
|
||||
**Project Type**: Web application, Laravel/Filament admin panel
|
||||
**Performance Goals**: No material performance change. Reset logic should touch bounded session/filter arrays and existing page state only.
|
||||
**Constraints**: No migrations, seeders, new packages, env vars, queues, scheduler, storage, compatibility aliases, compatibility redirects, or broad legacy cleanup.
|
||||
**Scale/Scope**: Cross-cutting filter-state hardening across required Spec 315 workspace hubs.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: Changed operator-facing scope truth for clear-filter behavior on existing workspace hubs.
|
||||
- **Native vs custom classification summary**: Native Filament/Livewire pages/resources with the existing shared chip partial. No redesign and no new styling system.
|
||||
- **Shared-family relevance**: Scope signals, filter summaries, clear links/actions, workspace hub navigation, and table filter state.
|
||||
- **State layers in scope**: URL query, Livewire public properties, Filament `tableFilters`, `tableDeferredFilters`, persisted/session table filters, page-derived state, chip/header wording, data scope, reload, and focused browser history.
|
||||
- **Audience modes in scope**: Operator-MSP and support-platform. Customer-read-only applies only to existing Customer Review Workspace behavior.
|
||||
- **Decision/diagnostic/raw hierarchy plan**: Filter truth stays default-visible. Existing diagnostics and raw/support evidence remain unchanged.
|
||||
- **Raw/support gating plan**: No raw evidence exposure changes.
|
||||
- **One-primary-action / duplicate-truth control**: When filtered, the chip is the single page-level truth and `Clear filter` is the exit. After clear, absence of chip plus workspace-wide rows is the truth.
|
||||
- **Handling modes by drift class or surface**: Hard-stop for legacy aliases recreating Environment-filtered state; review-mandatory for any page-specific clear exception.
|
||||
- **Repository-signal treatment**: Feature tests and focused browser screenshots/notes are required evidence.
|
||||
- **Special surface test profiles**: global-context-shell, monitoring-state-page, standard-native-filament.
|
||||
- **Required tests or manual smoke**: Clear-state contract tests, persisted-state tests, clean-entry equivalence tests, Spec 314/315 regression tests, reload smoke, and high-risk back/forward smoke.
|
||||
- **Exception path and spread control**: Optional hubs are classified and excluded unless already Environment-filterable via `environment_id`. Any browser limitation is documented in the implementation report.
|
||||
- **Active feature PR close-out entry**: Guardrail and Smoke Coverage.
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes.
|
||||
- **Systems touched**: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `CanonicalAdminTenantFilterState`, possible `WorkspaceHubFilterStateResetter`, workspace hub Filament pages/resources, shared chip view, table/session filter keys, and Pest/browser tests.
|
||||
- **Shared abstractions reused**: `WorkspaceHubRegistry` for hub paths, clean URLs, and Environment-like key lists; `WorkspaceHubEnvironmentFilter` for valid `environment_id`; existing chip partial for visible state; existing Filament table state APIs/patterns.
|
||||
- **New abstraction introduced? why?**: Prefer formalizing one shared reset service/helper only if existing helpers cannot safely own all required Environment-like state layers. The abstraction is justified by repeated current hubs and reload defects.
|
||||
- **Why the existing abstraction was sufficient or insufficient**: Existing pieces solve entry and immediate visible filter display, but the reset path is not yet complete across table/deferred/session/page state.
|
||||
- **Bounded deviation / spread control**: Page-specific data refresh can stay local, but Environment-like key removal and persisted filter/session reset must use the shared contract or a documented equivalent.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no.
|
||||
- **Central contract reused**: N/A.
|
||||
- **Delegated UX behaviors**: N/A.
|
||||
- **Surface-owned behavior kept local**: Existing Operations hub inspect/detail behavior remains unchanged.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: N/A.
|
||||
- **Exception path**: none.
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes.
|
||||
- **Provider-owned seams**: Existing Provider Connection records and provider external tenant identifiers remain model/provider data only.
|
||||
- **Platform-core seams**: Workspace hub query contract, Environment filter reset contract, session/table filter key cleanup, and scope wording.
|
||||
- **Neutral platform terms / contracts preserved**: `Workspace`, `Environment`, `environment_id`, `Environment filter`, `Clear filter`.
|
||||
- **Retained provider-specific semantics and why**: Provider-specific connection identity remains unchanged and cannot be fallback filter state.
|
||||
- **Bounded extraction or follow-up path**: Broader old Tenant naming and compatibility seam cleanup is Spec 317.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before implementation. Re-check after runtime changes.*
|
||||
|
||||
- Inventory-first: no inventory/snapshot truth changes.
|
||||
- Read/write separation: no Graph writes or destructive operations are added.
|
||||
- Graph contract path: no Graph calls are introduced.
|
||||
- Deterministic capabilities: existing page/resource capabilities remain; clear does not grant access.
|
||||
- RBAC-UX: workspace/page authorization remains authoritative. Non-member access remains 404/safe no-access; member-missing-capability remains existing 403 behavior.
|
||||
- Workspace isolation: clear keeps the current Workspace and does not infer or switch Environment shell context.
|
||||
- Tenant isolation: Environment filters are never authorization substitutes; clearing widens only to the user's entitled workspace data.
|
||||
- Run observability: no new OperationRun lifecycle behavior.
|
||||
- Test governance (TEST-GOV-001): lane, fixture cost, browser scope, and reviewer handoff are explicit in spec/plan/tasks.
|
||||
- Proportionality (PROP-001): shared reset behavior is justified by cross-hub current-release stale state.
|
||||
- No premature abstraction (ABSTR-001): reset helper stays bounded to Environment-like workspace hub filter state.
|
||||
- Persisted truth (PERSIST-001): no persisted truth is added.
|
||||
- Behavioral state (STATE-001): no new state/status family is added.
|
||||
- UI semantics (UI-SEM-001): chip/scope state is direct domain-to-UI mapping.
|
||||
- Shared pattern first (XCUT-001): extend Spec 314/315 registry, resolver, chip, and existing filter helper behavior before adding local patterns.
|
||||
- Provider boundary (PROV-001): provider external tenant IDs cannot become platform filter truth.
|
||||
- V1 explicitness / few layers: direct hard cutover, bounded helper, page-local data refresh where needed.
|
||||
- Spec discipline / bloat check: follow-up specs 317/318 remain separate.
|
||||
- Filament-native UI (UI-FIL-001): no ad-hoc styling; existing Filament/Blade primitives only.
|
||||
- UI/UX scope, truth, and naming: scope signals must distinguish Workspace shell from explicit Environment filter state.
|
||||
- UI naming: visible clear action uses `Clear filter`; visible filter wording uses `Environment`.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Feature/Livewire for page/table/session state, Browser for reload/history rendered state.
|
||||
- **Affected validation lanes**: fast-feedback, confidence, browser.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: Route/Livewire tests prove reset mechanics; browser smoke proves reload/history URL/chip/data alignment.
|
||||
- **Narrowest proving command(s)**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubClearFilter`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubEnvironmentFilter`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubNavigation`
|
||||
- focused browser smoke command or manual browser verification documented in close-out
|
||||
- **Fixture / helper / factory / seed / context cost risks**: Existing workspace/environment/member context and page-specific factories may be needed. Keep helper setup opt-in and avoid broad seeders.
|
||||
- **Expensive defaults or shared helper growth introduced?**: no. Any shared reset helper is runtime behavior, not a test setup default.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: Focused browser smoke only. Durable browser infrastructure is deferred to Spec 318.
|
||||
- **Surface-class relief / special coverage rule**: Native Filament/Livewire tests for page/table state; browser smoke for integrated UI state.
|
||||
- **Closing validation and reviewer handoff**: Reviewers must verify no hidden Environment state survives clear/reload, no legacy aliases become canonical, no unrelated filters are over-cleared without reason, and browser limitations are documented.
|
||||
- **Budget / baseline / trend follow-up**: none expected.
|
||||
- **Review-stop questions**: Does any hub still restore `managed_environment_id`/`tenant` from session after clear? Does clean URL show filtered rows? Does back/forward mismatch URL and chip? Does clear switch Workspace or Environment shell context?
|
||||
- **Escalation path**: Follow-up Spec 318 only for durable browser no-drift infrastructure; Spec 317 for broad legacy naming/query cleanup.
|
||||
- **Active feature PR close-out entry**: Guardrail and Smoke Coverage.
|
||||
- **Why no dedicated follow-up spec is needed**: This is the dedicated clear-filter spec. Only broad cleanup and durable browser infrastructure remain separate.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/316-workspace-hub-clear-filter-contract/
|
||||
|-- spec.md
|
||||
|-- plan.md
|
||||
|-- tasks.md
|
||||
|-- checklists/
|
||||
| `-- requirements.md
|
||||
`-- artifacts/
|
||||
`-- screenshots/ # created during implementation/browser verification if useful
|
||||
```
|
||||
|
||||
No `research.md`, `data-model.md`, `quickstart.md`, or `contracts/` artifact is required for preparation because this feature introduces no data model, external API contract, or new workflow API.
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
Likely runtime files to inspect or update during implementation:
|
||||
|
||||
```text
|
||||
apps/platform/app/Support/Navigation/WorkspaceHubRegistry.php
|
||||
apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php
|
||||
apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php
|
||||
apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php # if new helper is required
|
||||
apps/platform/app/Filament/Pages/Monitoring/Operations.php
|
||||
apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php
|
||||
apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php
|
||||
apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php
|
||||
apps/platform/app/Filament/Pages/Governance/DecisionRegister.php
|
||||
apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php
|
||||
apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php
|
||||
apps/platform/app/Filament/Resources/ProviderConnectionResource.php
|
||||
apps/platform/resources/views/filament/partials/workspace-hub-environment-filter-chip.blade.php
|
||||
apps/platform/tests/Feature/
|
||||
apps/platform/tests/Browser/
|
||||
```
|
||||
|
||||
Potential classification-only inspection areas:
|
||||
|
||||
```text
|
||||
apps/platform/app/Filament/Pages/Monitoring/AuditLog.php
|
||||
apps/platform/app/Filament/Resources/Alert*
|
||||
apps/platform/app/Filament/Resources/StoredReport*
|
||||
apps/platform/app/Filament/Resources/SupportRequest*
|
||||
```
|
||||
|
||||
**Structure Decision**: Laravel/Filament platform app under `apps/platform`. New runtime source, if needed, stays under existing `app/Support` boundaries. Tests stay in existing Pest feature/browser directories.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|---|---|---|
|
||||
| Shared reset service/helper if existing helper cannot own the contract | Clear state spans repeated current hubs and multiple state layers | Page-local clear handlers already caused inconsistent reload/session behavior |
|
||||
| Possible page integration helper/trait | Repeated resolver/chip/reset wiring may otherwise drift | Eight local implementations would recreate the defect; a broad context framework is still rejected |
|
||||
|
||||
## Phase 0: Discovery Completed During Preparation
|
||||
|
||||
Relevant repository facts discovered before authoring this plan:
|
||||
|
||||
- Current branch before creation was `platform-dev`, clean, at `eced9ad5 Spec 315: implement environment CTA explicit filter contract (#370)`.
|
||||
- `specs/316-workspace-hub-clear-filter-contract` did not exist before this preparation.
|
||||
- Related Specs 313, 314, and 315 contain completed/checklist/close-out signals and are historical context only.
|
||||
- `WorkspaceHubRegistry` already centralizes hub paths, forbidden query keys, Environment-like keys, `cleanUrl()`, and clean workspace hub entry detection.
|
||||
- `WorkspaceHubEnvironmentFilter` already resolves canonical `environment_id` inside the current Workspace and exposes display/query/clear helpers.
|
||||
- `workspace-hub-environment-filter-chip.blade.php` already renders `Environment filter:` and `Clear filter`.
|
||||
- `CanonicalAdminTenantFilterState` already handles some persisted filter cleanup for Environment-like keys on clean workspace hub entry.
|
||||
- Operations, Finding Exceptions Queue, Evidence Overview, Review Register, Customer Review Workspace, Decision Register, Governance Inbox, and Provider Connections all contain Environment-like filter/page state that may need reset integration.
|
||||
- Laravel Boost docs confirmed Filament v5 supports `persistFiltersInSession()` and deferred filters, and Livewire v4 URL-bound properties read/write query string state on page load.
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Formalize the shared reset mechanism
|
||||
|
||||
Preferred implementation:
|
||||
|
||||
```text
|
||||
apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php
|
||||
```
|
||||
|
||||
or a bounded extension/delegation of:
|
||||
|
||||
```text
|
||||
apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- identify the current workspace hub through `WorkspaceHubRegistry`
|
||||
- clear Environment-like query/session/table/deferred keys
|
||||
- clear legacy alias keys where present
|
||||
- remove nested Environment-like Filament table filter entries
|
||||
- support per-hub session key mapping where needed
|
||||
- preserve unrelated user filters when safe
|
||||
- never clear selected Workspace, auth/session, or unrelated table preferences
|
||||
|
||||
### 2. Keep the canonical filter source narrow
|
||||
|
||||
Only `WorkspaceHubEnvironmentFilter::fromRequest()` may create filtered state for workspace hubs.
|
||||
|
||||
Invalid sources:
|
||||
|
||||
```text
|
||||
tenant
|
||||
tenant_id
|
||||
managed_environment_id
|
||||
environment
|
||||
tenant_scope
|
||||
tableFilters
|
||||
remembered Environment
|
||||
Filament tenant
|
||||
session last environment
|
||||
lastTenantId / lastEnvironmentId
|
||||
provider external tenant id
|
||||
```
|
||||
|
||||
### 3. Standardize clear targets
|
||||
|
||||
The final clear URL should be the clean hub URL with no query string by default:
|
||||
|
||||
```text
|
||||
/admin/workspaces/{workspace}/operations
|
||||
/admin/governance/inbox
|
||||
/admin/governance/decisions
|
||||
/admin/finding-exceptions/queue
|
||||
/admin/provider-connections
|
||||
/admin/evidence/overview
|
||||
/admin/reviews
|
||||
/admin/reviews/workspace
|
||||
```
|
||||
|
||||
If preserving unrelated query parameters is implemented, tests must prove stale Environment-like state cannot survive.
|
||||
|
||||
### 4. Integrate required hubs
|
||||
|
||||
Each required hub must:
|
||||
|
||||
- resolve valid `environment_id` through `WorkspaceHubEnvironmentFilter`
|
||||
- call shared reset behavior when `environment_id` is absent or clear entry is used
|
||||
- clear local Livewire properties or table arrays that represent Environment filters
|
||||
- clear persisted session filter keys for Environment-like entries
|
||||
- refresh derived rows/counts/header state to workspace-wide
|
||||
- render chip only when a valid filter is active
|
||||
- keep shell context workspace-first
|
||||
|
||||
### 5. Preserve Spec 314 and Spec 315 behavior
|
||||
|
||||
Regression requirements:
|
||||
|
||||
- sidebar/global workspace hub entries remain clean and workspace-wide
|
||||
- Environment-owned CTAs still use `environment_id`
|
||||
- legacy params remain noncanonical
|
||||
- cross-workspace `environment_id` remains rejected
|
||||
- Decision Register clean URL remains valid
|
||||
|
||||
## Data Model
|
||||
|
||||
No data model changes.
|
||||
|
||||
No migrations, seeders, backfills, stored filter migrations, stored URL migrations, compatibility transforms, retention changes, queues, scheduler, or storage changes.
|
||||
|
||||
## Security and RBAC
|
||||
|
||||
- Clear filter never grants access.
|
||||
- Clear filter widens only to the current user's authorized workspace-wide data.
|
||||
- Existing page/resource policies and workspace membership checks remain authoritative.
|
||||
- Non-member workspace/environment access remains deny-as-not-found.
|
||||
- Member-without-capability behavior remains existing 403 where applicable.
|
||||
- Clear filter must not reveal cross-workspace Environment existence.
|
||||
- No provider external tenant ID fallback may become an authorization or filter boundary.
|
||||
|
||||
## Deployment / Operations
|
||||
|
||||
- No new environment variables.
|
||||
- No migrations.
|
||||
- No queues or scheduler changes.
|
||||
- No storage persistence or volume changes.
|
||||
- No package changes.
|
||||
- No Dokploy-specific deployment changes expected.
|
||||
- If implementation registers Filament assets unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`; no registered assets are expected.
|
||||
|
||||
## Browser Verification Plan
|
||||
|
||||
Use the in-app browser or existing Pest Browser tooling after runtime changes.
|
||||
|
||||
Required hubs:
|
||||
|
||||
- Operations
|
||||
- Governance Inbox
|
||||
- Decision Register
|
||||
- Finding Exceptions Queue
|
||||
- Provider Connections
|
||||
- Evidence
|
||||
- Reviews
|
||||
- Customer Reviews
|
||||
|
||||
Flows:
|
||||
|
||||
1. Filter -> Clear -> Reload for every required hub.
|
||||
2. Persisted Filter -> Sidebar Entry where a persisted filter can be applied.
|
||||
3. Browser Back/Forward for Provider Connections, Finding Exceptions Queue, Customer Reviews, and Evidence.
|
||||
|
||||
Screenshots may be saved under:
|
||||
|
||||
```text
|
||||
specs/316-workspace-hub-clear-filter-contract/artifacts/screenshots/
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Use direct hard cuts.
|
||||
- Do not add compatibility middleware, alias readers, or redirects.
|
||||
- Do not broaden optional hubs.
|
||||
- Do not update tests by broad rebaseline.
|
||||
- Prefer removing Environment-like persisted state over preserving it.
|
||||
- Keep any new helper narrow and repo-local.
|
||||
- Document intentional exclusions and browser limitations in the final implementation report.
|
||||
480
specs/316-workspace-hub-clear-filter-contract/spec.md
Normal file
480
specs/316-workspace-hub-clear-filter-contract/spec.md
Normal file
@ -0,0 +1,480 @@
|
||||
# Feature Specification: Workspace Hub Clear Filter Contract
|
||||
|
||||
**Feature Branch**: `316-workspace-hub-clear-filter-contract`
|
||||
**Created**: 2026-05-16
|
||||
**Status**: Draft
|
||||
**Input**: User supplied Spec 316 draft for completing the Workspace Hub Environment filter lifecycle after Specs 314 and 315.
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: Workspace hubs can now be entered cleanly through sidebar/global navigation and explicitly filtered through `environment_id`, but clearing that filter can leave stale URL, Livewire, Filament table, deferred, session, or derived page state behind.
|
||||
- **Today's failure**: A user can click `Clear filter` and see a clean URL or no chip while rows, header wording, persisted filters, or reload state remain scoped to the old Environment.
|
||||
- **User-visible improvement**: Clearing an Environment filter returns the hub to the same state as clean sidebar/global entry: Workspace hub, no Environment chip, workspace-wide data, reload safe.
|
||||
- **Smallest enterprise-capable version**: Formalize one shared clear/reset contract for in-scope workspace hubs, wire it into existing page/table state where needed, and add focused feature/browser coverage for reload and back/forward safety.
|
||||
- **Explicit non-goals**: No new Environment CTA filtering, no broad legacy naming cleanup, no durable browser no-drift infrastructure, no migrations, no seeders, no package/env/queue/scheduler/storage changes, and no compatibility aliases.
|
||||
- **Permanent complexity imported**: One shared reset service/helper or formalized existing helper path, optional small page integration trait/helper if the existing page pattern cannot stay consistent, focused Pest and browser-smoke coverage. No persisted entity, enum/status family, queue, external API, or new source of truth is introduced.
|
||||
- **Why now**: Spec 314 made clean workspace hub entry safe and Spec 315 made Environment CTA entry explicit. The remaining lifecycle gap is clear-filter exit; leaving it open keeps user-visible scope truth unreliable.
|
||||
- **Why not local**: The same stale-state risk exists across Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Reviews, and Customer Reviews. Page-local clear code already caused drift.
|
||||
- **Approval class**: Core Enterprise.
|
||||
- **Red flags triggered**: Cross-cutting filter state reset and a possible shared helper abstraction. Defense: the helper is bounded to clearing Environment-like filter state for current workspace hubs and is required by more than two current surfaces.
|
||||
- **Score**: Value: 2 | Urgency: 2 | Scope: 2 | Complexity: 1 | Product proximity: 2 | Reuse: 2 | **Total: 11/12**
|
||||
- **Decision**: approve.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: canonical-view.
|
||||
- **Primary Routes**: In-scope workspace hub routes for Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence Overview, Review Register, and Customer Review Workspace. Optional Audit Log, Alerts, Reports/Stored Reports, Risk Exceptions, and Support Requests are in scope only if existing runtime already made them Environment-filterable through `environment_id`.
|
||||
- **Data Ownership**: Workspace hubs remain workspace-owned/canonical-view pages. Existing Environment-owned records remain owned by `workspace_id` plus `managed_environment_id` or their current repo-real relationship. This spec changes filter-state handling only.
|
||||
- **RBAC**: Existing workspace membership, environment entitlement, page/resource policies, and capability checks remain authoritative. Clear filter must not grant access, switch Workspace, activate Environment shell context, or reveal cross-workspace Environment existence.
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: Clean workspace hub entry has no Environment filter, regardless of remembered Environment, Filament tenant state, session state, old table filters, or legacy query params.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: A hub is Environment-filtered only when `WorkspaceHubEnvironmentFilter` resolves a valid `environment_id` inside the selected Workspace for the current user. No legacy source may create a valid filter.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes.
|
||||
- **Interaction class(es)**: workspace hub scope signals, visible Environment filter chips, clear-filter links/actions, Filament table filters, page-level derived state, persisted/session filter state, browser reload/back-forward state, and navigation entry contracts.
|
||||
- **Systems touched**: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `CanonicalAdminTenantFilterState`, existing workspace hub Filament pages/resources, shared chip partial, page table state, session filter keys, and related Pest/browser tests.
|
||||
- **Existing pattern(s) to extend**: Spec 314 clean hub registry and URL cleaning, Spec 315 canonical `environment_id` resolver and visible chip, current `CanonicalAdminTenantFilterState` persisted filter cleanup behavior, and each page's existing table/query state pattern.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: Prefer formalizing a shared `WorkspaceHubFilterStateResetter` behavior around existing reset logic. Reuse `WorkspaceHubRegistry::environmentLikeFilterKeys()`, `WorkspaceHubRegistry::cleanUrl()`, `WorkspaceHubEnvironmentFilter`, and `workspace-hub-environment-filter-chip`.
|
||||
- **Why the existing shared path is sufficient or insufficient**: The repo already has the correct entry-point pieces, but the current reset surface is not yet guaranteed across all relevant layers and pages. Spec 316 makes the reset behavior explicit, complete, and covered.
|
||||
- **Allowed deviation and why**: Page-specific data refresh/query mechanics may remain local when pages use different data sources, but Environment-like state identification, URL cleanup, chip clear targets, and persisted table/session cleanup must delegate to shared behavior.
|
||||
- **Consistency impact**: URL state, Livewire state, Filament table/deferred filters, persisted session filters, visible chip, header/scope wording, data scope, reload, and back/forward behavior must agree.
|
||||
- **Review focus**: Verify no page-local clear implementation preserves stale Environment state, no legacy key becomes canonical, and no unrelated user filters are cleared unnecessarily unless the implementation cannot safely preserve them.
|
||||
|
||||
## OperationRun UX Impact *(mandatory)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no.
|
||||
- **Shared OperationRun UX contract/layer reused**: N/A.
|
||||
- **Delegated start/completion UX behaviors**: N/A.
|
||||
- **Local surface-owned behavior that remains**: Existing Operations hub and OperationRun inspect/detail behavior remain unchanged.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: N/A.
|
||||
- **Exception required?**: none.
|
||||
|
||||
## Provider Boundary / Platform Core Check *(mandatory)*
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes.
|
||||
- **Boundary classification**: platform-core filter contract with provider-adjacent surfaces.
|
||||
- **Seams affected**: Workspace hub query keys, provider connection Environment filters, legacy Tenant/Environment aliases, persisted table filter state, and scope wording.
|
||||
- **Neutral platform terms preserved or introduced**: `Workspace`, `Environment`, `Workspace hub`, `Environment filter`, `Clear filter`, `environment_id`.
|
||||
- **Provider-specific semantics retained and why**: Existing Provider Connection model data and external provider tenant identifiers remain unchanged, but they cannot be filter-state fallbacks for workspace hubs.
|
||||
- **Why this does not deepen provider coupling accidentally**: The only valid filter source remains platform `environment_id` resolved through `ManagedEnvironment` ownership inside a Workspace.
|
||||
- **Follow-up path**: Spec 317 removes/quarantines broader legacy Tenant / Environment context names. Spec 318 adds durable browser no-drift infrastructure.
|
||||
|
||||
## UI / Surface Guardrail Impact *(mandatory)*
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Shared Environment filter chip clear behavior | yes | Existing shared Blade partial / Filament page region | scope signals, header utility action | URL, page, table/session, visible state | no | Wording stays `Clear filter` |
|
||||
| Operations | yes | Existing Filament page/table | monitoring-state page | URL, Livewire properties, table filters, deferred filters, persisted filters, data query | no | Reset must equal clean Operations entry |
|
||||
| Governance Inbox | yes | Existing Filament page/list | governance queue | URL, page property, derived state, visible chip | no | Existing `tenantId` internals may remain only as noncanonical implementation detail |
|
||||
| Decision Register | yes | Existing Filament page/list | decision register | URL, page property, derived state, visible chip | no | Clean URL must remain authorized/open |
|
||||
| Finding Exceptions Queue | yes | Existing Filament page/table | exception queue | URL, table filters, deferred filters, persisted filters, selected state | no | Previously high-risk reload restoration surface |
|
||||
| Provider Connections | yes | Existing Filament resource/table | provider connection registry | URL, query state, table/session filters | no | No provider external tenant fallback |
|
||||
| Evidence Overview | yes | Existing Filament page/table or in-memory list | evidence viewer | URL, in-memory row filter, table/search filters, persisted state | no | Reset must refresh rows to workspace-wide |
|
||||
| Review Register | yes | Existing Filament page/table | review register | URL, table/deferred filters, persisted filters | no | Workspace-wide review list after clear |
|
||||
| Customer Review Workspace | yes | Existing Filament page/table | customer-safe review workspace | URL, table/deferred filters, persisted filters, page state | no | Previously high-risk reload restoration surface |
|
||||
| Optional hubs | maybe | Existing page/resource | audit/alerts/reports/support | Only where already Environment-filterable | no | Do not add new filter support here |
|
||||
|
||||
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Cleared workspace hub | Secondary Context | Decide from workspace-wide data after removing an Environment filter | Workspace context, no Environment chip, workspace-wide header/scope wording, workspace-wide rows | Existing row/detail diagnostics | The hub remains the decision/workspace surface; this spec restores truthful scope | Completes sidebar -> CTA -> clear lifecycle | Removes hidden stale state after clear/reload |
|
||||
| Browser back/forward across filtered and cleared states | Secondary Context | Avoid acting on a page whose URL, chip, and data disagree | URL, chip presence/absence, data scope | Existing page details | Not a new workflow, but critical state truth | Keeps browser navigation trustworthy | Prevents repeated manual scope reconstruction |
|
||||
|
||||
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Workspace hub filter state | operator-MSP, support-platform, customer-read-only where existing Customer Review Workspace applies | Either visible `Environment filter: {display name}` with `Clear filter`, or no chip and workspace-wide wording | Existing page/table diagnostics | Existing support/raw surfaces | Clear filter when filtered; inspect rows otherwise | Query/table/session internals | Chip, URL, header, and data scope must say the same thing |
|
||||
|
||||
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Operations | List / Table / Bulk | Monitoring/state page | Inspect operation or clear filter | Existing operations inspect pattern | existing | Existing page/table actions | Existing placement; no destructive changes | `/admin/workspaces/{workspace}/operations` | existing | Workspace-wide or Environment chip | Operations / Operation | Whether data is workspace-wide | none |
|
||||
| Governance Inbox | List / Table / Bulk | Queue | Inspect governance item or clear filter | Existing inbox inspect pattern | existing | Existing page actions | Existing placement; no destructive changes | `/admin/governance/inbox` | existing | Workspace-wide or Environment chip | Governance Inbox | Whether data is workspace-wide | none |
|
||||
| Decision Register | List / Table / Bulk | Register | Inspect decision or clear filter | Existing decision inspect pattern | existing | Existing page actions | Existing placement; no destructive changes | `/admin/governance/decisions` | existing | Workspace-wide or Environment chip | Decision Register | Whether data is workspace-wide | none |
|
||||
| Finding Exceptions Queue | List / Table / Bulk | Queue | Inspect exception or clear filter | Existing exception inspect pattern | existing | Existing page/table actions | Existing placement; no destructive changes | `/admin/finding-exceptions/queue` | existing | Workspace-wide or Environment chip | Finding Exceptions | Whether data is workspace-wide | none |
|
||||
| Provider Connections | List / Table / Bulk | Registry | Inspect provider connection or clear filter | Existing provider connection inspect pattern | existing | Existing resource actions | Existing placement; no destructive changes | `/admin/provider-connections` | existing | Workspace-wide or Environment chip | Provider Connections | Whether data is workspace-wide | none |
|
||||
| Evidence Overview | List / Table / Bulk | Evidence viewer | Inspect evidence or clear filter | Existing evidence inspect pattern | existing | Existing page actions | Existing placement; no destructive changes | `/admin/evidence/overview` | existing | Workspace-wide or Environment chip | Evidence | Whether data is workspace-wide | none |
|
||||
| Reviews / Customer Reviews | List / Table / Bulk | Review workspace | Inspect review or clear filter | Existing review inspect pattern | existing | Existing page/table actions | Existing placement; no destructive changes | `/admin/reviews`, `/admin/reviews/workspace` | existing | Workspace-wide or Environment chip | Reviews | Whether data is workspace-wide | none |
|
||||
|
||||
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Workspace hub after filter clear | TenantPilot operator | Continue from workspace-wide data after clearing a visible Environment filter | Existing list/table/page | Am I seeing all entitled workspace data again? | Clean URL, no Environment chip, workspace-wide rows/header/scope | Existing page diagnostics | Existing page-specific dimensions only | TenantPilot navigation/filter state only | Inspect rows, apply normal filters | None added or changed |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no.
|
||||
- **New persisted entity/table/artifact?**: no.
|
||||
- **New abstraction?**: yes, if existing reset behavior cannot be formalized directly; expected as a narrow `WorkspaceHubFilterStateResetter` service/helper or equivalent shared reset contract.
|
||||
- **New enum/state/reason family?**: no.
|
||||
- **New cross-domain UI framework/taxonomy?**: no.
|
||||
- **Current operator problem**: Clear-filter can leave hidden Environment state active after the visible filter is gone, causing operators to act on the wrong scope.
|
||||
- **Existing structure is insufficient because**: Spec 315 gives a resolver and chip, and `CanonicalAdminTenantFilterState` has some persisted filter cleanup, but there is not yet one guaranteed cross-hub contract that clears URL, Livewire, Filament table, deferred, persisted/session, and derived page state together.
|
||||
- **Narrowest correct implementation**: Formalize shared reset behavior for Environment-like keys and call it from in-scope hubs on clean entry/clear. Leave page-specific query builders local.
|
||||
- **Ownership cost**: One shared reset owner, per-hub integration, feature/browser regression coverage, and reviewer attention to session/table key maps.
|
||||
- **Alternative intentionally rejected**: Eight independent clear handlers or compatibility aliases would preserve drift. A broad legacy cleanup belongs to Spec 317.
|
||||
- **Release truth**: Current-release truth. This completes the active 314/315/316 filter lifecycle before broader cleanup.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment.
|
||||
|
||||
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope. Canonical replacement is required. Legacy Environment-like keys may be removed or neutralized; they must not be preserved as valid filter sources.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature/Livewire for clear-state contracts and page behavior; Browser for reload and high-risk back/forward rendered-state safety.
|
||||
- **Validation lane(s)**: fast-feedback for focused Pest tests, confidence for Filament/Livewire state pages/resources, browser for integrated reload/back-forward smoke.
|
||||
- **Why this classification and these lanes are sufficient**: The risk spans server-resolved query/session state and rendered browser truth. Feature tests prove reset/data contracts; browser smoke proves URL/chip/data alignment after reload and history navigation.
|
||||
- **New or expanded test families**: Spec 316 workspace hub clear contract tests, persisted table filter reset tests, legacy state reset tests, clean-entry equivalence tests, and focused browser smoke for high-risk hubs.
|
||||
- **Fixture / helper cost impact**: Existing Workspace, ManagedEnvironment, membership/capability, OperationRun, FindingException, ProviderConnection, EvidenceSnapshot, Review, and Customer Review factories/helpers may be used. Any helper widening must stay opt-in.
|
||||
- **Heavy-family visibility / justification**: Browser coverage is explicit and limited to critical/higher-risk flows because reload/history state cannot be fully proven by route tests alone.
|
||||
- **Special surface test profile**: global-context-shell, monitoring-state-page, standard-native-filament.
|
||||
- **Standard-native relief or required special coverage**: Native Filament tests cover page/table behavior; browser smoke covers integrated rendered state and history.
|
||||
- **Reviewer handoff**: Reviewers must confirm tests prove URL, chip, Livewire/table/session state, data scope, reload safety, and Spec 314/315 regressions without broad suite rebaseline.
|
||||
- **Budget / baseline / trend impact**: No material long-term lane shift expected. Any durable browser suite expansion belongs to Spec 318.
|
||||
- **Escalation needed**: none unless implementation discovers structural no-drift browser infrastructure is required; that becomes Spec 318 work.
|
||||
- **Active feature PR close-out entry**: Guardrail and Smoke Coverage.
|
||||
- **Planned validation commands**: Focused Pest filters for Spec 316 feature tests, existing Spec 314/315 regression tests, `git diff --check`, Pint for touched PHP files, and focused browser smoke/manual verification.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Clear returns a filtered workspace hub to workspace-wide state (Priority: P1)
|
||||
|
||||
An operator opens a workspace hub with `environment_id`, sees the Environment chip, clicks `Clear filter`, and lands on the clean hub URL with workspace-wide data and no stale chip or scope wording.
|
||||
|
||||
**Why this priority**: This is the core contract and the visible operator problem.
|
||||
|
||||
**Independent Test**: For each required hub, start filtered by `environment_id`, assert chip and filtered data, clear the filter, and assert clean URL, no legacy params, no chip, workspace-wide header/scope wording, and workspace-wide seeded rows.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** Operations is filtered by Environment A, **When** the operator clicks `Clear filter`, **Then** the final URL has no query string, the chip disappears, table filters no longer apply Environment A, and operations from other entitled environments are visible.
|
||||
2. **Given** Governance Inbox is filtered by Environment A, **When** the operator clears the filter, **Then** the page-level Environment property is null and the inbox returns to workspace-wide rows.
|
||||
3. **Given** Decision Register is filtered by Environment A, **When** the operator clears the filter, **Then** the clean register URL opens for the authorized workspace user and no 403 is caused by missing filter state.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Clear removes persisted and deferred Environment-like state (Priority: P1)
|
||||
|
||||
An operator has stale Filament/session filter state from an Environment filter. Clearing must remove the Environment-like persisted state so reload does not silently restore it.
|
||||
|
||||
**Why this priority**: Previously observed defects included chip removal while table/session filters continued to apply.
|
||||
|
||||
**Independent Test**: Persist Environment-like table filters for Finding Exceptions Queue, Customer Reviews, Evidence, and Provider Connections; visit filtered state; clear; reload; assert persisted keys are gone and workspace-wide rows are visible.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** Finding Exceptions Queue has a persisted `managed_environment_id` table filter, **When** the operator clears the visible `environment_id` filter, **Then** the persisted table filter is removed and reload remains workspace-wide.
|
||||
2. **Given** Customer Review Workspace has stale `tenant` or `managed_environment_id` persisted filter state, **When** the operator clears the filter, **Then** no stale review Environment filter is reapplied after reload.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Legacy Environment aliases cannot recreate filtered state (Priority: P1)
|
||||
|
||||
An operator or stale browser/session state contains legacy keys. Clean hub entry and clear results must neutralize those aliases instead of treating them as canonical filter state.
|
||||
|
||||
**Why this priority**: Hard cutover is a product and constitution requirement in this pre-production repo.
|
||||
|
||||
**Independent Test**: Seed stale query/session/table keys for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and nested `tableFilters`; assert clear neutralizes them and reload does not reapply hidden filtering.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** Provider Connections session state contains `tableFilters.tenant.value`, **When** the operator clears the Environment filter, **Then** the legacy key is removed or ignored and provider rows are workspace-wide.
|
||||
2. **Given** Evidence Overview URL contains no `environment_id` but session state contains old `tenant_scope`, **When** the page renders through clean hub entry, **Then** no Environment chip appears and rows are not silently Environment-scoped.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Clear result equals clean sidebar/global entry (Priority: P1)
|
||||
|
||||
An operator clearing a filter and an operator entering through sidebar/global navigation should see the same hub state.
|
||||
|
||||
**Why this priority**: This connects Specs 314, 315, and 316 into one lifecycle.
|
||||
|
||||
**Independent Test**: Capture clean hub entry state, visit filtered URL, clear, and assert the final URL, chip/scope indicators, and data set match clean entry.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a user enters Evidence Overview through the sidebar, **When** the same user later opens Evidence with `environment_id` and clears it, **Then** the final state matches the sidebar entry state.
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 - Reload and browser history keep URL, chip, and data aligned (Priority: P2)
|
||||
|
||||
An operator clears a filter, reloads, and uses browser back/forward. The browser must not show a clean URL with filtered data or a filtered URL with missing chip.
|
||||
|
||||
**Why this priority**: The stale state defect is most visible after reload/history navigation, but durable no-drift infrastructure remains Spec 318.
|
||||
|
||||
**Independent Test**: Browser-smoke Provider Connections, Finding Exceptions Queue, Customer Reviews, Evidence, and Operations for filter -> clear -> reload and high-risk back/forward alignment.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** Customer Review Workspace was filtered and then cleared, **When** the browser reloads the clean URL, **Then** no Environment chip returns and workspace-wide reviews remain visible.
|
||||
2. **Given** Provider Connections filtered URL is in browser history, **When** the operator goes back and forward, **Then** each visible state matches the current URL: filtered URL has chip and filtered data; clean URL has no chip and workspace-wide data.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: A workspace hub is Environment-filtered only when `environment_id` is present, valid, workspace-owned, user-accessible, and resolved by `WorkspaceHubEnvironmentFilter`.
|
||||
- **FR-002**: `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, remembered Environment, Filament tenant state, session last environment, and `tableFilters` MUST NOT create canonical Environment-filtered hub state.
|
||||
- **FR-003**: Clear filter final URLs MUST remove `environment_id`, `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
|
||||
- **FR-004**: The default final clear target SHOULD be the clean hub URL with no query string unless preserving unrelated query params is proven safe and intentional.
|
||||
- **FR-005**: A shared reset mechanism MUST clear Environment-like persisted/session filter state for in-scope workspace hubs.
|
||||
- **FR-006**: Environment-like keys for reset MUST include `environment_id`, `environment`, `tenant`, `tenant_id`, `managed_environment_id`, and `tenant_scope`.
|
||||
- **FR-007**: Nested Filament table filters representing Environment selection MUST be removed or neutralized, including `tableFilters.environment.value`, `tableFilters.environment_id.value`, `tableFilters.tenant.value`, `tableFilters.tenant_id.value`, `tableFilters.managed_environment_id.value`, and `tableFilters.tenant_scope.value` where present.
|
||||
- **FR-008**: Clear MUST reset Environment-related Livewire public properties or derived page state for the affected hub.
|
||||
- **FR-009**: Clear MUST reset Environment-related Filament `tableFilters` state where the hub uses tables.
|
||||
- **FR-010**: Clear MUST reset Environment-related `tableDeferredFilters` state where the hub uses deferred filters.
|
||||
- **FR-011**: Clear MUST remove Environment-like persisted table/session filters without clearing unrelated safe user filters such as search/status/date unless the page implementation cannot safely preserve them.
|
||||
- **FR-012**: Visible Environment filter chips MUST disappear after clear.
|
||||
- **FR-013**: Header, scope, empty-state, and shell wording MUST return to workspace-wide/all-environments wording after clear where applicable.
|
||||
- **FR-014**: Data MUST return to workspace-wide scope after clear.
|
||||
- **FR-015**: Reload after clear MUST NOT restore Environment-filtered state.
|
||||
- **FR-016**: Sidebar/global revisit after clear MUST remain clean and workspace-wide.
|
||||
- **FR-017**: Browser back/forward MUST keep URL, chip, shell, and data scope aligned where feasible; any tooling limitation must be documented.
|
||||
- **FR-018**: Clear MUST keep the selected Workspace active and MUST NOT activate or infer Environment shell context.
|
||||
- **FR-019**: Clear MUST NOT weaken existing page authorization or use Environment filters as an authorization substitute.
|
||||
- **FR-020**: Operations MUST clear `environment_id`, Operations-specific Environment/table state, deferred filters, persisted Environment-like table state, chip, and misleading filtered wording.
|
||||
- **FR-021**: Governance Inbox MUST clear `environment_id`, page Environment property state, query-derived filter state, and chip.
|
||||
- **FR-022**: Decision Register MUST clear `environment_id`, Environment-derived page state, chip, and any persisted/deferred state if applicable; clean URL remains authorized.
|
||||
- **FR-023**: Finding Exceptions Queue MUST clear `environment_id`, Filament Environment table filter, deferred/persisted table filter state, old `tenant` persisted state, and chip.
|
||||
- **FR-024**: Provider Connections MUST clear `environment_id`, provider/environment filter state, hidden query-level Environment filters, persisted table filters if present, and chip.
|
||||
- **FR-025**: Evidence Overview MUST clear `environment_id`, in-memory Environment row filtering, table/search Environment filters, persisted state, and chip.
|
||||
- **FR-026**: Review Register MUST clear `environment_id`, review-list Environment filters, persisted state, old `tenant` state if present, and chip.
|
||||
- **FR-027**: Customer Review Workspace MUST clear `environment_id`, Customer Review Environment filters, page properties, persisted/session table state, old aliases if present, and chip.
|
||||
- **FR-028**: Optional hubs MUST NOT receive new Environment filter support in this spec. They are included only if Spec 315 already made them Environment-filterable via `environment_id`.
|
||||
- **FR-029**: Page-local clear implementations MUST be removed or delegated to shared reset behavior where they can drift.
|
||||
- **FR-030**: No backwards compatibility layer, legacy alias preservation, migration, seeder, package, env var, queue, scheduler, or storage change may be introduced.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- **NFR-001**: The implementation MUST preserve workspace isolation and deny-as-not-found behavior for out-of-scope workspace/environment access.
|
||||
- **NFR-002**: The implementation MUST target Filament v5.2.1 and Livewire v4.1.4 patterns; no Livewire v3 or Filament v3/v4 APIs are allowed.
|
||||
- **NFR-003**: The shared reset mechanism MUST stay narrow and must not become a broad context/session framework.
|
||||
- **NFR-004**: Tests MUST prove reload safety and persisted-state clearing for high-risk hubs.
|
||||
- **NFR-005**: Browser smoke MUST stay focused; durable no-drift infrastructure is deferred to Spec 318.
|
||||
|
||||
## Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Workspace**: Primary SaaS and operating context. It remains selected after clear.
|
||||
- **ManagedEnvironment**: Secondary operational context inside a Workspace. It is valid as a hub filter only through canonical `environment_id`.
|
||||
- **WorkspaceHubRegistry**: Existing hub registry and URL/query cleanup helper. It supplies Environment-like key lists and clean hub URL behavior.
|
||||
- **WorkspaceHubEnvironmentFilter**: Existing canonical resolver for valid `environment_id`.
|
||||
- **WorkspaceHubFilterStateResetter**: Proposed/formalized shared reset behavior for Environment-like URL, table, deferred, persisted, and page state. It is not persisted and is not a new source of truth.
|
||||
- **CanonicalAdminTenantFilterState**: Existing persisted filter helper that may be reused, narrowed, or delegated to by the new shared reset behavior.
|
||||
|
||||
## In Scope
|
||||
|
||||
- Clear-filter behavior for required Spec 315 `environment_id` hubs.
|
||||
- Shared reset mechanism for Environment-like query/table/session/page state.
|
||||
- Existing visible chip clear target behavior.
|
||||
- Reset of Livewire public properties and derived state where used for Environment filtering.
|
||||
- Reset of Filament `tableFilters`, `tableDeferredFilters`, and persisted filter session entries where used for Environment filtering.
|
||||
- Reload and focused back/forward safety.
|
||||
- Spec 314 and Spec 315 regressions.
|
||||
- Focused browser verification and screenshot artifacts where useful.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- New Environment CTA filtering.
|
||||
- Retrofitting optional hubs that were not already Environment-filterable through `environment_id`.
|
||||
- Broad legacy Tenant/Environment naming cleanup.
|
||||
- Removing every `tenant` occurrence in the codebase.
|
||||
- Durable browser regression/no-drift infrastructure beyond focused Spec 316 smoke.
|
||||
- RBAC redesign.
|
||||
- Data model, migration, seeder, backfill, package, env var, queue, scheduler, or storage changes.
|
||||
- Compatibility redirects or old URL preservation.
|
||||
|
||||
## Page-Specific Requirements
|
||||
|
||||
### Operations
|
||||
|
||||
- Clear removes `environment_id`, Operations Environment/table filter state, deferred filters, persisted Environment-like filters, chip, and misleading filtered header/scope state.
|
||||
- After reload, Operations remains workspace-wide and no hidden Environment filter applies.
|
||||
|
||||
### Governance Inbox
|
||||
|
||||
- Clear removes `environment_id`, `$tenantId` or equivalent page Environment property, query-derived state, and chip.
|
||||
- Existing internal `tenantId` naming may remain only where renaming is unnecessary for correctness; Spec 317 owns naming cleanup.
|
||||
|
||||
### Decision Register
|
||||
|
||||
- Clear removes `environment_id`, Environment-derived page state, visible chip, and persisted/deferred filters if applicable.
|
||||
- Clean URL remains authorized and open for workspace users.
|
||||
|
||||
### Finding Exceptions Queue
|
||||
|
||||
- Clear removes `environment_id`, Filament Environment table filter, deferred filter, persisted table filter state, old `tenant` persisted state, and chip.
|
||||
- Filtered queue -> clear -> reload equals workspace-wide queue with no Environment chip.
|
||||
|
||||
### Provider Connections
|
||||
|
||||
- Clear removes `environment_id`, provider/environment filter state, hidden query-level Environment filtering, persisted table filters if present, and chip.
|
||||
- No remembered Environment inference and no provider external tenant fallback may survive clear.
|
||||
|
||||
### Evidence Overview
|
||||
|
||||
- Clear removes `environment_id`, in-memory row Environment filtering, table/search Environment filter state, persisted filter state, and chip.
|
||||
- Reload remains workspace-wide.
|
||||
|
||||
### Review Register
|
||||
|
||||
- Clear removes `environment_id`, review-list Environment filter, persisted filter state, visible chip, and old `tenant` state if present.
|
||||
- Reload remains workspace-wide.
|
||||
|
||||
### Customer Review Workspace
|
||||
|
||||
- Clear removes `environment_id`, customer-review Environment filter, page property state, persisted table/session state, visible chip, and old aliases if present.
|
||||
- Customer Reviews filtered -> clear -> reload equals clean workspace-wide customer review workspace.
|
||||
|
||||
## Required Tests
|
||||
|
||||
- **Shared Clear State Contract**: `it_workspace_hub_clear_filter_removes_environment_id_and_state_layers` covering Operations, Finding Exceptions Queue, Provider Connections, Evidence, Reviews, Customer Reviews, Governance Inbox, and Decision Register.
|
||||
- **Persisted Table Filter Reset**: `it_clear_filter_removes_persisted_environment_like_table_filters` covering Finding Exceptions Queue, Customer Reviews, Evidence, and Provider Connections.
|
||||
- **Legacy Persisted State Reset**: `it_clear_filter_removes_legacy_environment_alias_state` covering legacy keys `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and nested `tableFilters` where repo supports them.
|
||||
- **Clean Entry Equivalence**: `it_clear_filter_result_matches_clean_workspace_hub_entry` for critical hubs.
|
||||
- **Browser Reload Safety**: `it_cleared_workspace_hub_environment_filter_does_not_restore_after_reload` for Provider Connections, Finding Exceptions Queue, Customer Reviews, and Operations.
|
||||
- **Browser Back/Forward Safety**: `it_workspace_hub_clear_filter_does_not_create_mismatched_back_forward_state` for Provider Connections, Finding Exceptions Queue, Customer Reviews, and Evidence where tooling is stable.
|
||||
- **Spec 314 Regression**: `it_sidebar_workspace_hub_entry_remains_clean_after_clear_filter_contract`.
|
||||
- **Spec 315 Regression**: `it_environment_cta_filter_contract_still_uses_environment_id_after_clear_filter_contract`.
|
||||
|
||||
## Browser Verification Required
|
||||
|
||||
Required hubs:
|
||||
|
||||
- Operations
|
||||
- Governance Inbox
|
||||
- Decision Register
|
||||
- Finding Exceptions Queue
|
||||
- Provider Connections
|
||||
- Evidence
|
||||
- Reviews
|
||||
- Customer Reviews
|
||||
|
||||
Required flows:
|
||||
|
||||
1. **Filter -> Clear -> Reload**: Open with `?environment_id={id}`, assert chip, click `Clear filter`, assert clean URL/no chip/workspace-wide state, reload, assert chip does not return.
|
||||
2. **Persisted Filter -> Sidebar Entry**: Apply/persist Environment filter where possible, navigate away, enter through sidebar/global, assert clean URL/no chip/no hidden data filter.
|
||||
3. **Browser Back/Forward**: For Provider Connections, Finding Exceptions Queue, Customer Reviews, and Evidence, verify URL/chip/data scope alignment after back/forward.
|
||||
|
||||
Screenshots may be saved under:
|
||||
|
||||
```text
|
||||
specs/316-workspace-hub-clear-filter-contract/artifacts/screenshots/
|
||||
```
|
||||
|
||||
Suggested screenshot names:
|
||||
|
||||
```text
|
||||
operations--filtered.png
|
||||
operations--after-clear.png
|
||||
operations--after-clear-reload.png
|
||||
provider-connections--filtered.png
|
||||
provider-connections--after-clear.png
|
||||
provider-connections--after-clear-reload.png
|
||||
finding-exceptions-queue--filtered.png
|
||||
finding-exceptions-queue--after-clear.png
|
||||
finding-exceptions-queue--after-clear-reload.png
|
||||
customer-reviews--filtered.png
|
||||
customer-reviews--after-clear.png
|
||||
customer-reviews--after-clear-reload.png
|
||||
```
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: Clearing a valid `environment_id` filter removes all Environment-like URL/query state from the final URL.
|
||||
- **SC-002**: Clearing resets Environment-related Livewire properties, Filament table filters, deferred filters, and persisted/session filters for required hubs.
|
||||
- **SC-003**: Clearing removes the visible Environment chip and returns header/scope/empty-state wording to workspace-wide state.
|
||||
- **SC-004**: Clearing returns data to workspace-wide scope and reload does not restore Environment-filtered data.
|
||||
- **SC-005**: Sidebar/global entry remains clean and workspace-wide after clear.
|
||||
- **SC-006**: Browser back/forward does not create a misleading URL/chip/data mismatch on high-risk hubs or limitations are documented.
|
||||
- **SC-007**: Spec 314 sidebar/global clean-entry contract and Spec 315 Environment CTA `environment_id` contract still pass.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Specs 313, 314, and 315 are completed historical context and must not be rewritten.
|
||||
- `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, and `CanonicalAdminTenantFilterState` are the starting points for implementation.
|
||||
- There is no production data or production environment to preserve.
|
||||
- Existing factories and feature/browser harnesses can support the required focused coverage.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Session key discovery risk**: Filament persisted table filters may use page/resource-specific session keys. Mitigation: inspect actual `getTableFiltersSessionKey()` usage and test persisted reset per high-risk hub.
|
||||
- **Over-clearing risk**: A blunt reset could erase unrelated user filters. Mitigation: remove only Environment-like keys by default and document any unavoidable full clean URL behavior.
|
||||
- **Back/forward tool instability**: Browser history assertions can be flaky. Mitigation: keep required coverage focused and document limitations if tooling blocks deterministic proof.
|
||||
- **Abstraction drift risk**: A resetter could become a broad context framework. Mitigation: scope it to Environment-like workspace hub filter state only.
|
||||
|
||||
## Required Final Implementation Report
|
||||
|
||||
When implementation is complete, report:
|
||||
|
||||
```text
|
||||
Spec 316 completed.
|
||||
|
||||
Changed behavior:
|
||||
...
|
||||
|
||||
Shared clear mechanism:
|
||||
...
|
||||
|
||||
Hubs covered:
|
||||
...
|
||||
|
||||
Files changed:
|
||||
...
|
||||
|
||||
Tests:
|
||||
- command:
|
||||
- result:
|
||||
|
||||
Browser verification:
|
||||
...
|
||||
|
||||
Known remaining issues:
|
||||
...
|
||||
|
||||
Remaining follow-ups:
|
||||
- 317:
|
||||
- 318:
|
||||
|
||||
No migrations were created.
|
||||
No seeders were changed.
|
||||
No packages, env vars, queues, scheduler, or storage changes were made.
|
||||
No backwards compatibility layer was introduced.
|
||||
No legacy query alias preservation was added.
|
||||
```
|
||||
|
||||
Also include:
|
||||
|
||||
- list of clear-state layers covered
|
||||
- list of hubs verified
|
||||
- list of legacy state keys removed or neutralized
|
||||
- pages intentionally excluded
|
||||
- browser limitations
|
||||
- unrelated residual test failures, if any
|
||||
|
||||
## Follow-Up Specs
|
||||
|
||||
- **317 - Legacy Tenant / Environment Context Cleanup**: Remove/quarantine old aliases, stale Tenant naming, remembered Environment as data boundary, `Filament::getTenant()` workspace hub usage, and compatibility seams.
|
||||
- **318 - Browser Regression Coverage / No-Drift Guard**: Durable automated browser/regression coverage for sidebar/global entry, Environment CTA entry, clear filter, reload, back/forward, visible scope correctness, and hidden filter drift.
|
||||
|
||||
## Filament v5 Output Contract
|
||||
|
||||
1. **Livewire v4.0+ compliance**: This spec targets the current app stack: Filament v5.2.1 with Livewire v4.1.4. No Livewire v3 APIs or assumptions are allowed.
|
||||
2. **Provider registration location**: No new Filament panel provider is expected. If implementation discovers provider registration work, Laravel 12 requires panel providers in `apps/platform/bootstrap/providers.php`, not `bootstrap/app.php`.
|
||||
3. **Global search**: No resource is made globally searchable by this spec. Existing globally searchable resources must still have Edit/View pages; resources without safe View/Edit pages must keep global search disabled.
|
||||
4. **Destructive actions**: No destructive actions are added or changed. Clear filter is a navigation/filter-state action, not a destructive mutation. If any implementation path touches a destructive action, it must use `->action(...)`, `->requiresConfirmation()`, authorization, and audit logging.
|
||||
5. **Asset strategy**: No new heavy assets are expected. Reusing the existing chip partial requires no asset deployment change. If Filament assets are registered unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`.
|
||||
6. **Testing plan**: Cover Filament pages/resources as Livewire components or feature routes following Pest 4 conventions. Required browser smoke remains focused on reload/history rendered-state safety.
|
||||
111
specs/316-workspace-hub-clear-filter-contract/tasks.md
Normal file
111
specs/316-workspace-hub-clear-filter-contract/tasks.md
Normal file
@ -0,0 +1,111 @@
|
||||
# Tasks: Workspace Hub Clear Filter Contract
|
||||
|
||||
**Input**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/spec.md), [plan.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/plan.md)
|
||||
**Prerequisites**: Specs 313, 314, and 315 are completed historical baseline context. Do not rewrite them.
|
||||
|
||||
**Important**: These tasks track the Spec 316 implementation, runtime verification, and close-out evidence.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is named and is the narrowest sufficient proof for URL, Livewire, Filament, persisted/session, reload, and browser-history behavior.
|
||||
- [x] New or changed tests stay in the smallest honest family, and any browser addition is explicit.
|
||||
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
|
||||
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
|
||||
- [x] The declared surface test profile (`global-context-shell`, `monitoring-state-page`, `standard-native-filament`) is explicit.
|
||||
- [x] Any material budget, baseline, trend, browser limitation, or escalation note is recorded in the active spec or implementation close-out.
|
||||
|
||||
## Phase 1: Guardrails and Baseline
|
||||
|
||||
- [x] T001 Verify the implementation starts from branch `316-workspace-hub-clear-filter-contract` and the worktree has no unrelated user changes before runtime edits.
|
||||
- [x] T002 Re-read Specs 313, 314, and 315 to confirm the completed baseline: clean sidebar/global entry, canonical `environment_id` CTA entry, visible chip, and clean clear target.
|
||||
- [x] T003 Confirm Laravel/Filament/Livewire/Pest versions through Laravel Boost `application_info`.
|
||||
- [x] T004 Confirm no migration, seeder, package, env var, queue, scheduler, storage, or deployment asset change is required.
|
||||
- [x] T005 Inventory actual persisted/table/session state keys for Operations, Finding Exceptions Queue, Provider Connections, Evidence, Review Register, Customer Review Workspace, Governance Inbox, and Decision Register.
|
||||
- [x] T006 Classify optional hubs and document that no new Environment filter support is added for optional pages.
|
||||
|
||||
## Phase 2: Tests First - Shared Clear Contract
|
||||
|
||||
- [x] T007 Add `it_workspace_hub_clear_filter_removes_environment_id_and_state_layers` covering Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Review Register, and Customer Review Workspace.
|
||||
- [x] T008 Add assertions that each filtered hub starts with a valid `environment_id`, visible chip, clear action/link, and seeded filtered data where the page has data.
|
||||
- [x] T009 Add assertions that after clear the URL has no `environment_id`, no legacy params, no chip, workspace-wide wording, and workspace-wide seeded rows.
|
||||
- [x] T010 Add reload assertions to the shared clear contract where the test harness can revisit or rerender the page.
|
||||
- [x] T011 Add `it_clear_filter_removes_persisted_environment_like_table_filters` covering Finding Exceptions Queue, Customer Review Workspace, Evidence Overview, and Provider Connections.
|
||||
- [x] T012 Add `it_clear_filter_removes_legacy_environment_alias_state` for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and nested `tableFilters` state where repo support exists.
|
||||
- [x] T013 Add `it_clear_filter_result_matches_clean_workspace_hub_entry` for critical hubs by comparing clean entry and filtered-then-cleared state.
|
||||
- [x] T014 Add or update `it_cleared_workspace_hub_environment_filter_does_not_restore_after_reload` for Provider Connections, Finding Exceptions Queue, Customer Review Workspace, and Operations.
|
||||
- [x] T015 Add or update `it_workspace_hub_clear_filter_does_not_create_mismatched_back_forward_state` for Provider Connections, Finding Exceptions Queue, Customer Review Workspace, and Evidence, or document tooling limitations.
|
||||
|
||||
## Phase 3: Shared Reset Mechanism
|
||||
|
||||
- [x] T016 Create or formalize a shared reset mechanism such as `apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php` or a bounded extension of `CanonicalAdminTenantFilterState`.
|
||||
- [x] T017 Make the reset mechanism use `WorkspaceHubRegistry` to identify workspace hubs and Environment-like keys.
|
||||
- [x] T018 Make the reset mechanism remove or neutralize `environment_id`, `environment`, `tenant`, `tenant_id`, `managed_environment_id`, and `tenant_scope`.
|
||||
- [x] T019 Make the reset mechanism remove nested Environment-like table filter entries such as `environment.value`, `environment_id.value`, `tenant.value`, `tenant_id.value`, `managed_environment_id.value`, and `tenant_scope.value`.
|
||||
- [x] T020 Make the reset mechanism handle persisted Filament/session filter arrays and forget empty session filter keys.
|
||||
- [x] T021 Make the reset mechanism preserve unrelated safe filters such as search/status/date when possible.
|
||||
- [x] T022 Ensure the reset mechanism never clears selected Workspace, auth/session, unrelated table preferences, or unrelated resource state.
|
||||
- [x] T023 Add focused unit/feature coverage for the reset mechanism with representative nested filter arrays.
|
||||
|
||||
## Phase 4: Shared Page Integration
|
||||
|
||||
- [x] T024 Reuse or add a small shared page integration helper only if repeated reset/resolver/chip wiring cannot stay consistent locally.
|
||||
- [x] T025 Ensure clean workspace hub entry with no valid `environment_id` triggers shared Environment-like state reset before data rendering.
|
||||
- [x] T026 Ensure clear-link navigation ends on the clean hub URL and the entry pipeline clears persisted Environment-like state before rendering.
|
||||
- [x] T027 Ensure `WorkspaceHubEnvironmentFilter` remains the only valid source of Environment-filtered hub state.
|
||||
- [x] T028 Ensure legacy query params are removed/ignored and cannot hydrate Environment-related page properties.
|
||||
- [x] T029 Ensure the shared chip still renders only for valid `environment_id` and uses `Clear filter` as the action label.
|
||||
|
||||
## Phase 5: Required Hub Runtime Integration
|
||||
|
||||
- [x] T030 Update Operations to delegate clear/reset behavior, clear `environment_id`, `managed_environment_id` table/deferred/persisted state, and refresh workspace-wide data/header state.
|
||||
- [x] T031 Update Governance Inbox to clear `environment_id`, `$tenantId` or equivalent derived property, query-derived state, and chip state through shared behavior.
|
||||
- [x] T032 Update Decision Register to clear `environment_id`, Environment-derived page state, chip state, and any persisted/deferred filters while keeping clean URL authorized.
|
||||
- [x] T033 Update Finding Exceptions Queue to clear `environment_id`, Environment table filter, deferred filters, persisted table/session filters, old `tenant` state, and chip state.
|
||||
- [x] T034 Update Provider Connections to clear `environment_id`, provider/environment filter state, hidden query-level filters, persisted table filters if present, and provider external tenant fallback behavior.
|
||||
- [x] T035 Update Evidence Overview to clear `environment_id`, in-memory row Environment filtering, table/search filters, persisted filters, and chip state.
|
||||
- [x] T036 Update Review Register to clear `environment_id`, review-list Environment filters, persisted filters, old `tenant` state if present, and chip state.
|
||||
- [x] T037 Update Customer Review Workspace to clear `environment_id`, page properties, table/deferred/session filters, old aliases if present, and chip state.
|
||||
|
||||
## Phase 6: Legacy Alias Neutralization
|
||||
|
||||
- [x] T038 Search required hubs for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`, remembered Environment, `Filament::getTenant()`, `lastTenantId`, and `lastEnvironmentId` as workspace hub filter sources.
|
||||
- [x] T039 Remove or neutralize legacy alias handling where it can reapply workspace hub Environment filtering.
|
||||
- [x] T040 Preserve unrelated legacy names only when they are outside workspace hub Environment filter behavior, and document them for Spec 317.
|
||||
- [x] T041 Confirm Provider Connections does not infer filter state from provider external tenant identifiers after clear.
|
||||
- [x] T042 Confirm clean hub entry never restores Environment filter state from persisted table/session keys.
|
||||
|
||||
## Phase 7: Regression Safety
|
||||
|
||||
- [x] T043 Re-run or update Spec 314 regression coverage proving sidebar/global workspace hub entry remains clean, has no Environment params, and shows workspace-wide data.
|
||||
- [x] T044 Re-run or update Spec 315 regression coverage proving Environment-owned CTAs still use `environment_id`, visible chip still renders when filtered, legacy params remain noncanonical, and cross-workspace IDs are rejected.
|
||||
- [x] T045 Add a Decision Register clean URL regression proving authorized users can open it after clear without 403 caused by missing filter state.
|
||||
- [x] T046 Confirm clear behavior does not bypass page/resource authorization, does not reveal inaccessible Workspace/Environment existence, and preserves existing 404/403 semantics.
|
||||
- [x] T047 Confirm no globally searchable resource or destructive action behavior changed; if a resource/action is touched, confirm Edit/View/global-search status remains valid or disabled and destructive actions still use `->action(...)`, `->requiresConfirmation()`, authorization, and audit behavior.
|
||||
|
||||
## Phase 8: Browser Verification
|
||||
|
||||
- [x] T048 Start the local platform stack using Sail or the repo's platform dev command.
|
||||
- [x] T049 Run Flow A `Filter -> Clear -> Reload` for Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Review Register, and Customer Review Workspace.
|
||||
- [x] T050 Run Flow B `Persisted Filter -> Sidebar Entry` for hubs where an Environment filter can be persisted.
|
||||
- [x] T051 Run Flow C browser back/forward for Provider Connections, Finding Exceptions Queue, Customer Review Workspace, and Evidence.
|
||||
- [x] T052 Save screenshots where useful under `specs/316-workspace-hub-clear-filter-contract/artifacts/screenshots/`.
|
||||
- [x] T053 Document browser tooling limitations if back/forward or persisted-filter browser setup cannot be made deterministic.
|
||||
|
||||
## Phase 9: Final Validation
|
||||
|
||||
- [x] T054 Run focused Pest tests for the Spec 316 clear/reset contract.
|
||||
- [x] T055 Run existing related Spec 314 and Spec 315 regression tests.
|
||||
- [x] T056 Run formatting/static checks expected by the touched files, including Pint if PHP files changed.
|
||||
- [x] T057 Run `git diff --check`.
|
||||
- [x] T058 Prepare the final implementation report with changed behavior, shared clear mechanism, hubs covered, files changed, tests, browser verification, known issues, and remaining follow-ups.
|
||||
- [x] T059 Confirm final report lists clear-state layers covered, hubs verified, legacy state keys removed/neutralized, pages intentionally excluded, browser limitations, and unrelated residual test failures if any.
|
||||
- [x] T060 Confirm final report states no migrations, seeders, packages, env vars, queues, scheduler, storage changes, compatibility layer, or legacy query alias preservation were introduced.
|
||||
|
||||
## Explicit Non-Tasks
|
||||
|
||||
- [x] NT001 Do not implement new Environment CTA filtering beyond preserving Spec 315 behavior.
|
||||
- [x] NT002 Do not retrofit optional pages that were not already Environment-filterable through `environment_id`.
|
||||
- [x] NT003 Do not perform broad legacy Tenant / Environment naming cleanup; leave to Spec 317.
|
||||
- [x] NT004 Do not build durable browser no-drift infrastructure; leave to Spec 318.
|
||||
- [x] NT005 Do not add compatibility redirects, dual-param support, alias readers, or adapter layers.
|
||||
- [x] NT006 Do not create migrations, seeders, packages, env vars, queues, scheduler changes, or storage changes.
|
||||
Loading…
Reference in New Issue
Block a user