fix: route workspace switch to chooser (#147)
## Summary - route the context-bar `Switch workspace` link to the canonical chooser flow instead of workspace management - add focused regression coverage for topbar switching, management separation, and chooser redirect semantics - add Spec 121 artifacts (`spec`, `plan`, `research`, `data-model`, `contracts`, `quickstart`, `tasks`, checklist) ## Validation - `vendor/bin/sail artisan test --compact tests/Feature/Monitoring/HeaderContextBarTest.php tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php tests/Feature/Workspaces/ChooseWorkspacePageTest.php tests/Feature/Workspaces/WorkspaceNavigationHubTest.php tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseTenantTest.php` - `vendor/bin/sail bin pint --dirty --format agent` ## Notes - base branch: `dev` - branch: `121-workspace-switch-fix` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #147
This commit is contained in:
parent
cd811cff4f
commit
891f177311
4
.github/agents/copilot-instructions.md
vendored
4
.github/agents/copilot-instructions.md
vendored
@ -42,6 +42,8 @@ ## Active Technologies
|
|||||||
- PHP 8.4 + Laravel 12, Filament v5, Livewire v4 (116-baseline-drift-engine)
|
- PHP 8.4 + Laravel 12, Filament v5, Livewire v4 (116-baseline-drift-engine)
|
||||||
- PHP 8.4, Laravel 12, Filament v5, Livewire v4 + Laravel framework, Filament admin panels, Livewire, PostgreSQL JSONB persistence, Laravel Sail (120-secret-redaction-integrity)
|
- PHP 8.4, Laravel 12, Filament v5, Livewire v4 + Laravel framework, Filament admin panels, Livewire, PostgreSQL JSONB persistence, Laravel Sail (120-secret-redaction-integrity)
|
||||||
- PostgreSQL (`policy_versions`, `operation_runs`, `audit_logs`, related evidence tables) (120-secret-redaction-integrity)
|
- PostgreSQL (`policy_versions`, `operation_runs`, `audit_logs`, related evidence tables) (120-secret-redaction-integrity)
|
||||||
|
- PHP 8.4.15 / Laravel 12 + Filament v5 + Livewire v4.0+ + Tailwind CSS v4 (121-workspace-switch-fix)
|
||||||
|
- PostgreSQL + session-backed workspace context; no schema changes (121-workspace-switch-fix)
|
||||||
|
|
||||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -61,8 +63,8 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 121-workspace-switch-fix: Added PHP 8.4.15 / Laravel 12 + Filament v5 + Livewire v4.0+ + Tailwind CSS v4
|
||||||
- 120-secret-redaction-integrity: Added PHP 8.4, Laravel 12, Filament v5, Livewire v4 + Laravel framework, Filament admin panels, Livewire, PostgreSQL JSONB persistence, Laravel Sail
|
- 120-secret-redaction-integrity: Added PHP 8.4, Laravel 12, Filament v5, Livewire v4 + Laravel framework, Filament admin panels, Livewire, PostgreSQL JSONB persistence, Laravel Sail
|
||||||
- 116-baseline-drift-engine-session-1772451227: Added PHP 8.4 + Laravel 12, Filament v5, Livewire v4
|
- 116-baseline-drift-engine-session-1772451227: Added PHP 8.4 + Laravel 12, Filament v5, Livewire v4
|
||||||
- 116-baseline-drift-engine: Added PHP 8.4 + Laravel 12, Filament v5, Livewire v4
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
<!-- MANUAL ADDITIONS END -->
|
<!-- MANUAL ADDITIONS END -->
|
||||||
|
|||||||
@ -26,7 +26,6 @@
|
|||||||
use App\Services\Auth\WorkspaceRoleCapabilityMap;
|
use App\Services\Auth\WorkspaceRoleCapabilityMap;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
@ -135,25 +134,6 @@ public function panel(Panel $panel): Panel
|
|||||||
->group('Monitoring')
|
->group('Monitoring')
|
||||||
->sort(30),
|
->sort(30),
|
||||||
])
|
])
|
||||||
->userMenuItems([
|
|
||||||
Action::make('switch-workspace')
|
|
||||||
->label('Switch workspace')
|
|
||||||
->url(fn (): string => ChooseWorkspace::getUrl(panel: 'admin').'?choose=1')
|
|
||||||
->icon('heroicon-o-arrows-right-left')
|
|
||||||
->visible(function (): bool {
|
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
if (! $user instanceof User) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return WorkspaceMembership::query()
|
|
||||||
->where('user_id', (int) $user->getKey())
|
|
||||||
->join('workspaces', 'workspace_memberships.workspace_id', '=', 'workspaces.id')
|
|
||||||
->whereNull('workspaces.archived_at')
|
|
||||||
->count() > 1;
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
->renderHook(
|
->renderHook(
|
||||||
PanelsRenderHook::HEAD_END,
|
PanelsRenderHook::HEAD_END,
|
||||||
fn () => view('filament.partials.livewire-intercept-shim')->render()
|
fn () => view('filament.partials.livewire-intercept-shim')->render()
|
||||||
|
|||||||
@ -85,7 +85,7 @@
|
|||||||
|
|
||||||
<x-filament::dropdown.list>
|
<x-filament::dropdown.list>
|
||||||
<a
|
<a
|
||||||
href="{{ route('filament.admin.resources.workspaces.index') }}"
|
href="{{ ChooseWorkspace::getUrl(panel: 'admin').'?choose=1' }}"
|
||||||
class="block px-3 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="block px-3 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
>
|
>
|
||||||
Switch workspace
|
Switch workspace
|
||||||
|
|||||||
36
specs/121-workspace-switch-fix/checklists/requirements.md
Normal file
36
specs/121-workspace-switch-fix/checklists/requirements.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Specification Quality Checklist: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-03-07
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Validation passed on the first review.
|
||||||
|
- The specification keeps the focus on user intent, navigation semantics, and separation of context switching from workspace administration.
|
||||||
|
- No clarification round is required before `/speckit.plan`.
|
||||||
53
specs/121-workspace-switch-fix/contracts/routes.md
Normal file
53
specs/121-workspace-switch-fix/contracts/routes.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Route Contract: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Feature**: 121-workspace-switch-fix | **Date**: 2026-03-07
|
||||||
|
|
||||||
|
## Intent-level contract
|
||||||
|
|
||||||
|
### Switch workspace
|
||||||
|
|
||||||
|
- **Surface**: admin and tenant-panel context bar
|
||||||
|
- **Label**: `Switch workspace`
|
||||||
|
- **Canonical target**: `ChooseWorkspace::getUrl(panel: 'admin').'?choose=1'`
|
||||||
|
- **Meaning**: enter the explicit workspace chooser flow
|
||||||
|
- **User-menu duplication**: not allowed
|
||||||
|
|
||||||
|
### Manage workspaces
|
||||||
|
|
||||||
|
- **Surface**: existing administrative navigation / chooser management affordance
|
||||||
|
- **Canonical target**: workspace management resource index
|
||||||
|
- **Meaning**: create, view, and edit workspace records
|
||||||
|
|
||||||
|
## Before / After
|
||||||
|
|
||||||
|
### Before
|
||||||
|
|
||||||
|
```text
|
||||||
|
Context bar "Switch workspace"
|
||||||
|
-> /admin/workspaces
|
||||||
|
-> lands on workspace management CRUD index
|
||||||
|
```
|
||||||
|
|
||||||
|
### After
|
||||||
|
|
||||||
|
```text
|
||||||
|
Context bar "Switch workspace"
|
||||||
|
-> /admin/choose-workspace?choose=1
|
||||||
|
-> lands on chooser flow
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
|
||||||
|
1. Workspace chooser remains the canonical manual-switch surface.
|
||||||
|
2. `?choose=1` remains the explicit forced-chooser signal for intentional switching.
|
||||||
|
3. The top-right user menu must not expose a duplicate workspace-switch shortcut.
|
||||||
|
4. Workspace management remains separately reachable and semantically distinct.
|
||||||
|
5. Choosing a workspace continues to use existing `WorkspaceIntendedUrl` / `WorkspaceRedirectResolver` behavior.
|
||||||
|
6. No new routes, middleware rules, or authorization rules are introduced.
|
||||||
|
|
||||||
|
## Verification expectations
|
||||||
|
|
||||||
|
- Response rendering for a real admin page containing the context bar must show the chooser target.
|
||||||
|
- Filament user-menu evaluation must not register a `switch-workspace` item.
|
||||||
|
- Existing chooser behavior tests remain green.
|
||||||
|
- Workspace management navigation remains reachable through its existing destination.
|
||||||
85
specs/121-workspace-switch-fix/data-model.md
Normal file
85
specs/121-workspace-switch-fix/data-model.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Data Model: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Feature**: 121-workspace-switch-fix | **Date**: 2026-03-07
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This feature does not add or modify database tables. It corrects the navigation contract between three existing workspace-related concepts:
|
||||||
|
|
||||||
|
1. the topbar `Switch workspace` entry,
|
||||||
|
2. the canonical workspace chooser,
|
||||||
|
3. the separate workspace management destination.
|
||||||
|
|
||||||
|
## Existing Entities / Concepts
|
||||||
|
|
||||||
|
### Workspace switch entry (UI affordance)
|
||||||
|
|
||||||
|
| Attribute | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Surface | Admin context bar / topbar partial |
|
||||||
|
| Current problem | Routes to workspace management instead of chooser |
|
||||||
|
| New contract | Routes to the chooser using the existing forced-switch convention |
|
||||||
|
| Mutation | None |
|
||||||
|
|
||||||
|
### Workspace chooser
|
||||||
|
|
||||||
|
| Attribute | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Backing page | `App\Filament\Pages\ChooseWorkspace` |
|
||||||
|
| Route slug | `/admin/choose-workspace` |
|
||||||
|
| Purpose | Explicit manual workspace selection |
|
||||||
|
| Existing behavior | After selection, consumes intended URL or resolves redirect via `WorkspaceRedirectResolver` |
|
||||||
|
|
||||||
|
### Workspace management destination
|
||||||
|
|
||||||
|
| Attribute | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Backing resource | `App\Filament\Resources\Workspaces\WorkspaceResource` |
|
||||||
|
| Purpose | Workspace CRUD / administrative management |
|
||||||
|
| Access model | Existing capability-aware workspace management rules |
|
||||||
|
| Change in this feature | None |
|
||||||
|
|
||||||
|
## Relevant Existing State
|
||||||
|
|
||||||
|
### Session-backed workspace context
|
||||||
|
|
||||||
|
| Key | Type | Notes |
|
||||||
|
|-----|------|-------|
|
||||||
|
| `current_workspace_id` | int | Active workspace context used across the admin plane |
|
||||||
|
| intended workspace URL | string/null | Existing post-selection return path managed by `WorkspaceIntendedUrl` |
|
||||||
|
|
||||||
|
### Existing redirect semantics
|
||||||
|
|
||||||
|
```text
|
||||||
|
Switch intent
|
||||||
|
-> ChooseWorkspace (`?choose=1` forces chooser)
|
||||||
|
-> User selects workspace
|
||||||
|
-> WorkspaceIntendedUrl::consume() if present
|
||||||
|
-> else WorkspaceRedirectResolver::resolve()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation / Rules
|
||||||
|
|
||||||
|
| Rule | Result |
|
||||||
|
|------|--------|
|
||||||
|
| Intentional workspace switching must target chooser | Required |
|
||||||
|
| Intentional switching must not target workspace CRUD | Required |
|
||||||
|
| Workspace management remains separately reachable | Required |
|
||||||
|
| Existing membership/capability enforcement remains unchanged | Required |
|
||||||
|
| Existing post-selection redirect behavior remains unchanged | Required |
|
||||||
|
|
||||||
|
## State Transition
|
||||||
|
|
||||||
|
```text
|
||||||
|
[User on admin page]
|
||||||
|
|
|
||||||
|
| click "Switch workspace"
|
||||||
|
v
|
||||||
|
[Chooser page displayed]
|
||||||
|
|
|
||||||
|
| select workspace
|
||||||
|
v
|
||||||
|
[Existing intended URL or resolved workspace landing]
|
||||||
|
```
|
||||||
|
|
||||||
|
No new persistence state is introduced.
|
||||||
145
specs/121-workspace-switch-fix/plan.md
Normal file
145
specs/121-workspace-switch-fix/plan.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# Implementation Plan: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Branch**: `121-workspace-switch-fix` | **Date**: 2026-03-07 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/121-workspace-switch-fix/spec.md`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Correct the context-bar `Switch workspace` affordance so it uses the canonical chooser flow and remains the only workspace-switching affordance. The implementation will keep the topbar partial targeting `ChooseWorkspace::getUrl(panel: 'admin').'?choose=1'`, remove the duplicate admin user-menu shortcut, preserve the current chooser redirect behavior (`WorkspaceIntendedUrl` / `WorkspaceRedirectResolver`), and leave workspace management reachable only through its separate administrative navigation path. Validation stays focused on response-level topbar rendering plus regression coverage that management navigation and chooser semantics remain distinct.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.15 / Laravel 12
|
||||||
|
**Primary Dependencies**: Filament v5 + Livewire v4.0+ + Tailwind CSS v4
|
||||||
|
**Storage**: PostgreSQL + session-backed workspace context; no schema changes
|
||||||
|
**Testing**: Pest v4 feature tests (HTTP response assertions and existing workspace flow regression tests)
|
||||||
|
**Target Platform**: Web admin and tenant panels (`/admin`, `/admin/t/...`) running in Laravel Sail / container deployment
|
||||||
|
**Project Type**: Laravel monolith / Filament web application
|
||||||
|
**Performance Goals**: No additional remote calls or new query paths; topbar render remains effectively unchanged except for link destination
|
||||||
|
**Constraints**: Preserve existing RBAC and workspace isolation rules; preserve chooser post-selection behavior; avoid hardcoded management URLs for switch intent; keep workspace switching in context surfaces rather than account surfaces; no CRUD behavior changes
|
||||||
|
**Scale/Scope**: One context-bar Blade partial, one admin panel provider cleanup, plus targeted workspace/navigation tests; no data migration, no new services, no new routes
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
- Inventory-first: PASS (feature only changes navigation semantics; no inventory or backup semantics affected).
|
||||||
|
- Read/write separation: PASS (no new writes or mutations introduced; chooser selection behavior remains existing behavior).
|
||||||
|
- Graph contract path: PASS (no Microsoft Graph calls; all affected surfaces are DB/session-only).
|
||||||
|
- Deterministic capabilities: PASS (no new capabilities; existing workspace membership and management capability rules remain canonical).
|
||||||
|
- RBAC-UX: PASS (feature stays in `/admin`; non-member and capability behavior are unchanged because only the destination link changes).
|
||||||
|
- Workspace isolation: PASS (`?choose=1` intentionally forces the chooser while preserving existing workspace membership enforcement and post-selection resolution).
|
||||||
|
- Destructive confirmation: PASS / N/A (no destructive actions are added or changed).
|
||||||
|
- Global search: PASS / N/A (no global search behavior changes).
|
||||||
|
- Tenant isolation: PASS (chooser and management separation remains explicit; no tenant data exposure changes).
|
||||||
|
- Run observability: PASS / N/A (no long-running or operational work; no `OperationRun` usage).
|
||||||
|
- Ops-UX 3-surface feedback: PASS / N/A (no operation lifecycle involved).
|
||||||
|
- Automation: PASS / N/A (no scheduled or queued work).
|
||||||
|
- Data minimization: PASS (no new persisted data or logging payloads).
|
||||||
|
- Badge semantics (BADGE-001): PASS / N/A (no new status-like badges).
|
||||||
|
- Filament UI Action Surface Contract: PASS with exemption note. The affected surface is a topbar/context-bar navigation link, not a CRUD Resource/RelationManager/Page action surface.
|
||||||
|
- Filament UI UX-001: PASS with exemption note. The feature preserves the existing chooser page and does not alter Create/Edit/View layouts.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/121-workspace-switch-fix/
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── routes.md
|
||||||
|
└── tasks.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/
|
||||||
|
├── Filament/
|
||||||
|
│ └── Pages/
|
||||||
|
│ └── ChooseWorkspace.php # reference-only canonical chooser
|
||||||
|
├── Http/
|
||||||
|
│ └── Middleware/
|
||||||
|
│ └── EnsureWorkspaceSelected.php # reference-only forced chooser semantics
|
||||||
|
├── Providers/
|
||||||
|
│ └── Filament/
|
||||||
|
│ └── AdminPanelProvider.php # MODIFY — remove duplicate user-menu switch shortcut
|
||||||
|
└── Support/
|
||||||
|
└── Workspaces/
|
||||||
|
├── WorkspaceIntendedUrl.php # reference-only existing post-selection flow
|
||||||
|
└── WorkspaceRedirectResolver.php # reference-only existing post-selection flow
|
||||||
|
|
||||||
|
resources/
|
||||||
|
└── views/
|
||||||
|
└── filament/
|
||||||
|
└── partials/
|
||||||
|
└── context-bar.blade.php # MODIFY — switch link target
|
||||||
|
|
||||||
|
tests/
|
||||||
|
└── Feature/
|
||||||
|
├── Monitoring/
|
||||||
|
│ └── HeaderContextBarTest.php # MODIFY — assert chooser target from context bar
|
||||||
|
└── Workspaces/
|
||||||
|
├── WorkspaceSwitchUserMenuTest.php # MODIFY — assert user menu no longer exposes workspace switching
|
||||||
|
├── ChooseWorkspacePageTest.php # reference or extend — management remains reachable
|
||||||
|
└── WorkspaceNavigationHubTest.php # reference-only IA separation guard
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Keep the change inside the existing Laravel/Filament monolith. Implementation is a narrowly scoped view-level routing correction backed by targeted Pest feature tests and existing workspace chooser/navigation helpers.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
> No Constitution Check violations. No justifications needed.
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| — | — | — |
|
||||||
|
|
||||||
|
## Phase 0 — Research (output: `research.md`)
|
||||||
|
|
||||||
|
See: [research.md](./research.md)
|
||||||
|
|
||||||
|
Research goals:
|
||||||
|
- Confirm the canonical workspace-switch destination and whether forced chooser semantics require `?choose=1`.
|
||||||
|
- Confirm whether chooser post-selection redirect behavior should remain unchanged.
|
||||||
|
- Confirm the smallest reliable test surface for a context-bar-only switch rule plus management-navigation regression coverage.
|
||||||
|
|
||||||
|
## Phase 1 — Design & Contracts (outputs: `data-model.md`, `contracts/`, `quickstart.md`)
|
||||||
|
|
||||||
|
See:
|
||||||
|
- [data-model.md](./data-model.md)
|
||||||
|
- [contracts/routes.md](./contracts/routes.md)
|
||||||
|
- [quickstart.md](./quickstart.md)
|
||||||
|
|
||||||
|
Design focus:
|
||||||
|
- Treat the context-bar link as a pure navigation contract update, not a behavior rewrite.
|
||||||
|
- Reuse the existing chooser page and `?choose=1` forced-chooser convention to preserve manual switching semantics.
|
||||||
|
- Remove the duplicate user-menu shortcut so workspace switching stays anchored to the context bar.
|
||||||
|
- Preserve existing management navigation, chooser redirect resolution, and capability boundaries unchanged.
|
||||||
|
- Validate the semantic split with response-level topbar assertions plus regression checks around management reachability.
|
||||||
|
|
||||||
|
## Phase 2 — Implementation Outline (tasks created in `/speckit.tasks`)
|
||||||
|
|
||||||
|
### UI routing correction
|
||||||
|
- Replace the context-bar `Switch workspace` link target in `context-bar.blade.php`.
|
||||||
|
- Use the canonical chooser helper (`ChooseWorkspace::getUrl(panel: 'admin')`) with the forced chooser query parameter.
|
||||||
|
- Keep the link as navigation-only; no action/mutation flow is added.
|
||||||
|
- Remove the duplicate Filament user-menu switch action from the admin panel provider.
|
||||||
|
|
||||||
|
### Regression protection
|
||||||
|
- Extend the real rendered topbar test to assert the context-bar switch link points at the chooser URL rather than workspace CRUD.
|
||||||
|
- Update user-menu coverage to assert no workspace-switch shortcut is registered there.
|
||||||
|
- Reuse or mirror existing workspace chooser tests to prove management navigation remains separate and accessible through its dedicated path.
|
||||||
|
- Keep existing middleware/chooser tests as the source of truth for post-selection branching.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Run focused Pest coverage for context bar + workspace navigation semantics.
|
||||||
|
- Run Pint on dirty files through Sail.
|
||||||
|
|
||||||
|
## Constitution Check (Post-Design)
|
||||||
|
|
||||||
|
Re-check result: PASS. The design keeps workspace switching and workspace management semantically separate, reuses the existing chooser and middleware contract, introduces no new authorization rules or operational flows, and limits the implementation to a navigation target correction plus regression tests.
|
||||||
46
specs/121-workspace-switch-fix/quickstart.md
Normal file
46
specs/121-workspace-switch-fix/quickstart.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Quickstart: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Feature**: 121-workspace-switch-fix | **Date**: 2026-03-07
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
A small navigation-semantic correction:
|
||||||
|
- update the context-bar `Switch workspace` link,
|
||||||
|
- remove the duplicate user-menu `Switch workspace` shortcut,
|
||||||
|
- keep chooser behavior unchanged,
|
||||||
|
- keep workspace management navigation unchanged,
|
||||||
|
- add focused regression coverage.
|
||||||
|
|
||||||
|
## Implementation order
|
||||||
|
|
||||||
|
1. Update [resources/views/filament/partials/context-bar.blade.php](../../../resources/views/filament/partials/context-bar.blade.php) so `Switch workspace` targets the chooser helper with `?choose=1`.
|
||||||
|
2. Remove the admin user-menu `Switch workspace` shortcut from [app/Providers/Filament/AdminPanelProvider.php](../../../app/Providers/Filament/AdminPanelProvider.php).
|
||||||
|
3. Extend [tests/Feature/Monitoring/HeaderContextBarTest.php](../../../tests/Feature/Monitoring/HeaderContextBarTest.php) to assert the rendered context-bar link points to the chooser target.
|
||||||
|
4. Extend [tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php](../../../tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php) to assert no user-menu shortcut is registered.
|
||||||
|
5. Add or extend one workspace regression test to confirm management navigation remains separately reachable.
|
||||||
|
6. Run focused Sail-based tests.
|
||||||
|
7. Run Pint on dirty files.
|
||||||
|
|
||||||
|
## Reference files
|
||||||
|
|
||||||
|
- [app/Filament/Pages/ChooseWorkspace.php](../../../app/Filament/Pages/ChooseWorkspace.php)
|
||||||
|
- [app/Providers/Filament/AdminPanelProvider.php](../../../app/Providers/Filament/AdminPanelProvider.php)
|
||||||
|
- [app/Http/Middleware/EnsureWorkspaceSelected.php](../../../app/Http/Middleware/EnsureWorkspaceSelected.php)
|
||||||
|
- [tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php](../../../tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php)
|
||||||
|
- [tests/Feature/Workspaces/ChooseWorkspacePageTest.php](../../../tests/Feature/Workspaces/ChooseWorkspacePageTest.php)
|
||||||
|
|
||||||
|
## Validation commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vendor/bin/sail artisan test --compact tests/Feature/Monitoring/HeaderContextBarTest.php
|
||||||
|
vendor/bin/sail artisan test --compact tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php
|
||||||
|
vendor/bin/sail artisan test --compact tests/Feature/Workspaces/ChooseWorkspacePageTest.php
|
||||||
|
vendor/bin/sail bin pint --dirty --format agent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected outcome
|
||||||
|
|
||||||
|
- Context-bar switching always opens the chooser.
|
||||||
|
- The top-right user menu does not offer a duplicate workspace switch entry.
|
||||||
|
- Workspace management remains a separate administrative path.
|
||||||
|
- Existing chooser branching behavior and middleware semantics remain unchanged.
|
||||||
53
specs/121-workspace-switch-fix/research.md
Normal file
53
specs/121-workspace-switch-fix/research.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Research: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Feature**: 121-workspace-switch-fix | **Date**: 2026-03-07
|
||||||
|
|
||||||
|
## R1: Canonical destination for intentional workspace switching
|
||||||
|
|
||||||
|
**Decision**: Use `ChooseWorkspace::getUrl(panel: 'admin').'?choose=1'` as the context-bar target for `Switch workspace`.
|
||||||
|
|
||||||
|
**Rationale**: The codebase already treats the chooser page as the canonical manual-switch experience. The `?choose=1` parameter is the established convention for bypassing workspace auto-resume and forcing the chooser when the user explicitly intends to switch. Anchoring switching in the context bar avoids mixing workspace context controls into the top-right account/user menu.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Link to `route('filament.admin.resources.workspaces.index')`: rejected because that is the management surface (`Manage workspaces`), not the switch flow.
|
||||||
|
- Link to `/admin/choose-workspace` without `?choose=1`: rejected because the codebase already documents `?choose=1` as the explicit forced-chooser contract for intentional switching.
|
||||||
|
|
||||||
|
## R2: Post-selection redirect behavior after entering the chooser
|
||||||
|
|
||||||
|
**Decision**: Preserve the chooser’s existing redirect behavior after a workspace is selected.
|
||||||
|
|
||||||
|
**Rationale**: `ChooseWorkspace::selectWorkspace()` already resolves post-selection flow through `WorkspaceIntendedUrl::consume()` and `WorkspaceRedirectResolver::resolve()`. That behavior is covered by existing tests and is outside this spec’s scope. The fix is semantic routing into the chooser, not a redesign of what happens after a user selects a workspace.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Always redirect back to the current page after selection: rejected because it would change established chooser behavior and broaden scope beyond a low-risk semantic correction.
|
||||||
|
- Always redirect to one fixed landing page: rejected because it would bypass the existing tenant-count branching contract.
|
||||||
|
|
||||||
|
## R3: Separation between switching and management
|
||||||
|
|
||||||
|
**Decision**: Keep workspace management reachable only through its existing dedicated administrative destinations, while the context bar becomes chooser-only.
|
||||||
|
|
||||||
|
**Rationale**: The chooser page already exposes a capability-aware `Manage workspaces` link for eligible roles, and the workspace resource itself is explicitly labeled `Manage workspaces`. This preserves the intended split between operational context switching and workspace CRUD administration.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add both switch and management links to the same context-bar action surface: rejected because it would continue the semantic conflation the spec is correcting.
|
||||||
|
- Hide workspace management entirely from chooser-adjacent flows: rejected because the current dedicated management affordances are valid and already capability-aware.
|
||||||
|
|
||||||
|
## R5: Keep switching out of the top-right user menu
|
||||||
|
|
||||||
|
**Decision**: Remove the admin panel `Switch workspace` user-menu shortcut and keep workspace switching as a context-bar-only affordance.
|
||||||
|
|
||||||
|
**Rationale**: Workspace selection changes application context, not user-account state. Keeping the switcher in the context bar makes it visible in both admin and tenant panels through the shared render hook, while the user menu stays focused on profile, theme, and session actions.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Keep the user-menu shortcut as a secondary path: rejected because it duplicates the context action and creates cross-panel inconsistency unless every panel adopts it.
|
||||||
|
- Add the shortcut to the tenant panel user menu too: rejected because it still places a context switch under an account-oriented surface.
|
||||||
|
|
||||||
|
## R4: Smallest reliable test strategy
|
||||||
|
|
||||||
|
**Decision**: Extend response-level topbar rendering coverage in `HeaderContextBarTest`, and rely on existing workspace chooser/navigation tests for regression protection around management and forced chooser behavior.
|
||||||
|
|
||||||
|
**Rationale**: The bug lives in a rendered Blade partial in the admin topbar, so the most direct regression test is an HTTP response assertion on a real admin page that includes the context bar. Existing tests already cover chooser routing semantics, forced chooser behavior, chooser management-link visibility, and navigation hub separation.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Test only the chooser Livewire page: rejected because it would not assert the actual broken context-bar link.
|
||||||
|
- Add browser-only coverage: rejected because existing response-based feature tests already cover the required semantic contract at lower cost.
|
||||||
117
specs/121-workspace-switch-fix/spec.md
Normal file
117
specs/121-workspace-switch-fix/spec.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Feature Specification: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Feature Branch**: `121-workspace-switch-fix`
|
||||||
|
**Created**: 2026-03-07
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Separate workspace switching from workspace management in the context bar so the Switch workspace action always routes to the canonical workspace chooser instead of the workspace CRUD index. Keep workspace switching as a context-bar-only affordance rather than a user-menu shortcut."
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: workspace
|
||||||
|
- **Primary Routes**: authenticated admin and tenant-panel context bar workspace switch entry; workspace chooser flow at `/admin/choose-workspace`; workspace management page at `/admin/workspaces`
|
||||||
|
- **Data Ownership**: workspace-owned context selection only; no new workspace records or workspace management data changes
|
||||||
|
- **RBAC**: authenticated users continue to switch only among workspaces they already belong to; workspace management remains behind its existing administrative capabilities with no visibility or permission expansion
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Direct workspace switching (Priority: P1)
|
||||||
|
|
||||||
|
As a user with access to multiple workspaces, I can choose “Switch workspace” from the context bar and land on the workspace chooser instead of a management table.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core product expectation and the highest-visibility confusion point for multi-workspace usage.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by opening the context bar, activating “Switch workspace,” and confirming the chooser experience appears without exposing CRUD-oriented management UI.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a signed-in user with access to multiple workspaces, **When** the user selects “Switch workspace” from the context bar, **Then** the user is taken directly to the workspace chooser flow.
|
||||||
|
2. **Given** a signed-in user who opens the chooser from the context bar, **When** the chooser loads, **Then** the user sees the existing workspace selection experience rather than a workspace management list.
|
||||||
|
3. **Given** a signed-in user with access to multiple workspaces, **When** the user opens the top-right user menu, **Then** no separate `Switch workspace` shortcut is shown there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Preserve administrative separation (Priority: P2)
|
||||||
|
|
||||||
|
As an administrator, I can still reach workspace management through its dedicated administrative destination without mixing it into the context-switching action.
|
||||||
|
|
||||||
|
**Why this priority**: The product must preserve the distinction between operational context switching and administrative CRUD management.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by confirming the context bar action changes destination while the existing workspace management entry remains reachable and unchanged.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an administrator with workspace management access, **When** the administrator uses the dedicated management navigation path at `/admin/workspaces`, **Then** the administrator still reaches the workspace management experience.
|
||||||
|
2. **Given** an administrator using the context bar, **When** the administrator selects “Switch workspace,” **Then** the action does not open workspace management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Maintain navigational coherence (Priority: P3)
|
||||||
|
|
||||||
|
As a user, I experience consistent navigation after switching intent is corrected, with no loops, broken breadcrumbs, or dead-end routes.
|
||||||
|
|
||||||
|
**Why this priority**: The change is small but high-visibility, so surrounding navigation must remain coherent to avoid replacing one confusion point with another.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by following the switch path and the management path separately and confirming both destinations remain stable and understandable.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a user who arrives at the chooser from the context bar, **When** the user navigates within the chooser flow, **Then** page title, breadcrumbs, and back-navigation remain consistent with workspace selection intent.
|
||||||
|
2. **Given** a user who previously used workspace management, **When** the user accesses it through the existing administrative path, **Then** the management route continues to behave as before.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when a user has access to only one workspace but the switch entry is still shown in the context bar? The action should still resolve to the chooser flow rather than rerouting to management.
|
||||||
|
- How does the system handle a user whose active workspace becomes unavailable between rendering the context bar and clicking the switch action? The user should still be routed to the chooser, where existing entitlement handling determines the next safe state.
|
||||||
|
- What happens if a user lacks workspace management capability? The context-switching entry must still work without revealing or implying management access.
|
||||||
|
- How does the system handle stale bookmarks to workspace management? Existing management links remain valid and separate from switching intent.
|
||||||
|
- What happens if a user looks for workspace switching in the top-right user menu? The product should not duplicate the affordance there; the context bar remains the single canonical switch surface.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Constitution Alignment Notes
|
||||||
|
|
||||||
|
- This feature introduces no Microsoft Graph calls, no queued or scheduled work, and no write-oriented operational workflows, so contract registry, audit-run orchestration, and `OperationRun` requirements are unchanged.
|
||||||
|
- This feature does not introduce new authorization rules. It preserves the current workspace membership and capability model while correcting navigation semantics.
|
||||||
|
- No status badge semantics change.
|
||||||
|
- The Filament action surface change is a navigation-only context bar action. The Action Surface Contract is satisfied because the action is non-destructive, capability-aware behavior is preserved, and no mutation path is added.
|
||||||
|
- UX-001 remains satisfied for the affected surface because workspace switching continues to use a dedicated chooser experience, while workspace management remains a distinct administrative destination.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: The system MUST route the context-bar “Switch workspace” action to the canonical workspace chooser URL (`/admin/choose-workspace?choose=1`).
|
||||||
|
- **FR-002**: The system MUST preserve the workspace chooser as the primary and explicit experience for changing active workspace context, and MUST NOT route switching intent to workspace CRUD/management surfaces.
|
||||||
|
- **FR-003**: The system MUST keep workspace management available through its existing dedicated administrative destination at `/admin/workspaces`.
|
||||||
|
- **FR-004**: The system MUST preserve existing visibility and permission rules for workspace management.
|
||||||
|
- **FR-005**: The system MUST keep navigation and breadcrumb behavior coherent after the destination change.
|
||||||
|
- **FR-006**: The system MUST preserve current capability-aware handling for users who can switch workspaces but cannot manage workspaces.
|
||||||
|
- **FR-007**: The system MUST expose workspace switching through the context bar only, and MUST NOT register a duplicate `Switch workspace` shortcut in the Filament user menu.
|
||||||
|
|
||||||
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||||
|
|
||||||
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Context bar workspace switcher | Global admin and tenant-panel context bar | None | Not applicable | `Switch workspace` (navigation only) | None | None | Not applicable | Not applicable | No | Navigation-only action. Destination changes to the chooser flow. No destructive behavior, no mutation, and no authorization expansion. |
|
||||||
|
| User menu workspace shortcut | Top-right user menu | None | Not applicable | None | None | None | Not applicable | Not applicable | No | Explicitly absent by design. Workspace switching remains anchored in the context bar to preserve context-vs-account semantics. |
|
||||||
|
| Workspace management navigation | Existing admin navigation destination | Existing actions unchanged | Existing management affordances unchanged | Existing actions unchanged | Existing actions unchanged | Existing empty-state behavior unchanged | Existing actions unchanged | Existing save/cancel behavior unchanged | Unchanged | Included for separation-of-concerns verification only; this feature does not change management behavior. |
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Active Workspace Context**: The current workspace the user is operating in, which determines the semantic purpose of the switch action.
|
||||||
|
- **Workspace Chooser Experience**: The dedicated selection flow users rely on to change active workspace context safely and intentionally.
|
||||||
|
- **Workspace Management Experience**: The separate administrative surface for creating, editing, and reviewing workspaces.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- The existing workspace chooser remains the canonical destination for context switching.
|
||||||
|
- The existing workspace management destination remains available at `/admin/workspaces` and does not need redesign as part of this feature.
|
||||||
|
- Current membership and capability checks already distinguish between switching accessible workspaces and managing workspace records.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: In validation, 100% of context-bar “Switch workspace” activations land on the workspace chooser rather than a management table.
|
||||||
|
- **SC-002**: A multi-workspace user can begin a workspace switch in one click from the context bar without encountering CRUD management controls.
|
||||||
|
- **SC-003**: Administrative users retain an unchanged path to workspace management during regression testing.
|
||||||
|
- **SC-004**: Manual QA confirms no broken links, redirect loops, or breadcrumb confusion across the switch and management paths.
|
||||||
|
- **SC-005**: Validation confirms the top-right user menu does not expose a separate `Switch workspace` shortcut.
|
||||||
191
specs/121-workspace-switch-fix/tasks.md
Normal file
191
specs/121-workspace-switch-fix/tasks.md
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
# Tasks: Workspace Switch Semantic Fix
|
||||||
|
|
||||||
|
**Input**: Design documents from `/specs/121-workspace-switch-fix/`
|
||||||
|
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/routes.md, quickstart.md
|
||||||
|
|
||||||
|
**Tests**: Tests are REQUIRED for this runtime behavior change. Use Pest feature tests via Sail.
|
||||||
|
**Operations**: No `OperationRun` work is required for this feature.
|
||||||
|
**RBAC**: No authorization model changes are planned; tasks must preserve existing workspace membership and capability behavior.
|
||||||
|
**Filament UI Action Surfaces**: The affected surface is a topbar navigation link; no new CRUD action surface is introduced, and the top-right user menu must not duplicate workspace switching.
|
||||||
|
**Filament UI UX-001**: No Create/Edit/View layout changes are required; preserve existing chooser and management screens.
|
||||||
|
|
||||||
|
**Organization**: Tasks are grouped by user story so each story remains independently testable.
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Context)
|
||||||
|
|
||||||
|
**Purpose**: Align implementation with the existing chooser contract before touching code.
|
||||||
|
|
||||||
|
- [X] T001 [P] Review the canonical switch-route contract in specs/121-workspace-switch-fix/contracts/routes.md and specs/121-workspace-switch-fix/research.md
|
||||||
|
- [X] T002 [P] Inspect the current broken target in resources/views/filament/partials/context-bar.blade.php and the existing canonical switch pattern in app/Providers/Filament/AdminPanelProvider.php
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Confirm shared workspace-switch semantics that all stories depend on.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
|
||||||
|
|
||||||
|
- [X] T003 Confirm forced-chooser and post-selection redirect semantics in app/Http/Middleware/EnsureWorkspaceSelected.php, app/Filament/Pages/ChooseWorkspace.php, app/Support/Workspaces/WorkspaceIntendedUrl.php, and app/Support/Workspaces/WorkspaceRedirectResolver.php
|
||||||
|
|
||||||
|
**Checkpoint**: Canonical chooser semantics are confirmed and user-story implementation can begin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Direct workspace switching (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Make the context-bar `Switch workspace` action always open the canonical chooser instead of workspace management.
|
||||||
|
|
||||||
|
**Independent Test**: Open a real admin page with the context bar and verify that `Switch workspace` points to the chooser URL with forced-chooser semantics, not to `/admin/workspaces`.
|
||||||
|
|
||||||
|
### Tests for User Story 1 ⚠️
|
||||||
|
|
||||||
|
> **NOTE**: Add or update these tests first and ensure they fail before implementation.
|
||||||
|
|
||||||
|
- [X] T004 [P] [US1] Extend tests/Feature/Monitoring/HeaderContextBarTest.php to assert the rendered `Switch workspace` link targets `/admin/choose-workspace?choose=1` and does not target `/admin/workspaces`
|
||||||
|
- [X] T005 [P] [US1] Update tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php to assert the top-right user menu does not expose a `Switch workspace` shortcut, even when multiple workspaces exist
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [X] T006 [US1] Update resources/views/filament/partials/context-bar.blade.php to route `Switch workspace` to `ChooseWorkspace::getUrl(panel: 'admin').'?choose=1'`
|
||||||
|
- [X] T016 [US1] Remove the duplicate `Switch workspace` user-menu action from app/Providers/Filament/AdminPanelProvider.php so the context bar is the only switch surface
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 1 should now be independently functional and verifiable from the rendered admin topbar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Preserve administrative separation (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Keep workspace management reachable through its dedicated admin path without mixing it into the context-switching action.
|
||||||
|
|
||||||
|
**Independent Test**: Verify the chooser and admin navigation still expose `Manage workspaces` through `/admin/workspaces` while `Switch workspace` remains chooser-only.
|
||||||
|
|
||||||
|
### Tests for User Story 2 ⚠️
|
||||||
|
|
||||||
|
- [X] T007 [P] [US2] Extend tests/Feature/Workspaces/ChooseWorkspacePageTest.php to confirm authorized users still see `Manage workspaces`, unauthorized roles still do not, and the chooser management affordance continues to resolve to `/admin/workspaces`
|
||||||
|
- [X] T008 [P] [US2] Extend tests/Feature/Workspaces/WorkspaceNavigationHubTest.php to confirm navigation separation remains `Switch workspace` in the topbar only while `Manage workspaces` remains the dedicated admin navigation path to `/admin/workspaces`
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [X] T009 [US2] If T007 or T008 expose drift, update resources/views/filament/pages/choose-workspace.blade.php and/or app/Filament/Resources/Workspaces/WorkspaceResource.php so every `Manage workspaces` affordance still resolves to `/admin/workspaces` and remains separate from chooser-only switching
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 2 should now prove that switching and management remain semantically distinct.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Maintain navigational coherence (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Preserve existing chooser and redirect behavior so the semantic fix does not introduce loops or breadcrumb/navigation regressions.
|
||||||
|
|
||||||
|
**Independent Test**: Enter the chooser through the context-switch path and confirm existing forced-chooser and post-selection routing behavior remains intact.
|
||||||
|
|
||||||
|
### Tests for User Story 3 ⚠️
|
||||||
|
|
||||||
|
- [X] T010 [P] [US3] Extend tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php to keep `?choose=1` forced-chooser behavior covered for intentional workspace switching
|
||||||
|
- [X] T011 [P] [US3] Extend tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseTenantTest.php to confirm chooser post-selection routing remains unchanged after the context-bar fix
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [X] T012 [US3] If T010 or T011 expose a regression, update app/Filament/Pages/ChooseWorkspace.php and/or app/Support/Workspaces/WorkspaceRedirectResolver.php so forced-chooser semantics and post-selection branching remain unchanged
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 3 should confirm the fix changes only entry semantics, not downstream chooser behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Final verification and formatting for the complete change.
|
||||||
|
|
||||||
|
- [X] T013 Perform manual QA for `/admin/choose-workspace?choose=1` and `/admin/workspaces`, confirming page title, breadcrumbs, back-navigation, link targets, and absence of a user-menu switch shortcut remain coherent across switching and management flows
|
||||||
|
- [X] T014 Run focused Pest coverage with `vendor/bin/sail artisan test --compact` for tests/Feature/Monitoring/HeaderContextBarTest.php, tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php, tests/Feature/Workspaces/ChooseWorkspacePageTest.php, tests/Feature/Workspaces/WorkspaceNavigationHubTest.php, tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php, and tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseTenantTest.php
|
||||||
|
- [X] T015 Run `vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Setup (Phase 1)**: No dependencies; can start immediately.
|
||||||
|
- **Foundational (Phase 2)**: Depends on Setup completion; blocks all user stories.
|
||||||
|
- **User Story 1 (Phase 3)**: Depends on Foundational completion; this is the MVP.
|
||||||
|
- **User Story 2 (Phase 4)**: Depends on Foundational completion; can run after or in parallel with User Story 3 once User Story 1 direction is clear.
|
||||||
|
- **User Story 3 (Phase 5)**: Depends on Foundational completion; can run after or in parallel with User Story 2.
|
||||||
|
- **Polish (Phase 6)**: Depends on completion of the desired user stories.
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1**: No dependency on other stories; delivers the primary fix.
|
||||||
|
- **US2**: Depends only on the shared chooser/management contract, not on US3.
|
||||||
|
- **US3**: Depends only on the shared chooser contract, not on US2.
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Tests should be updated first and observed failing before implementation.
|
||||||
|
- Implementation follows only after the story-specific regression tests are in place.
|
||||||
|
- Each story should remain independently testable when complete.
|
||||||
|
|
||||||
|
### Parallel Opportunities
|
||||||
|
|
||||||
|
- T001 and T002 can run in parallel.
|
||||||
|
- T004 and T005 can run in parallel.
|
||||||
|
- T007 and T008 can run in parallel.
|
||||||
|
- T010 and T011 can run in parallel.
|
||||||
|
- After Phase 2, US2 and US3 can proceed in parallel if staffed separately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Example: User Story 1
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch both regression updates for the switching contract together:
|
||||||
|
Task: "Extend tests/Feature/Monitoring/HeaderContextBarTest.php to assert the rendered Switch workspace link targets /admin/choose-workspace?choose=1 and does not target /admin/workspaces"
|
||||||
|
Task: "Update tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php to assert the top-right user menu does not expose a Switch workspace shortcut"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Example: User Story 2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch management-separation regression coverage together:
|
||||||
|
Task: "Extend tests/Feature/Workspaces/ChooseWorkspacePageTest.php to confirm authorized users still see Manage workspaces on the chooser and unauthorized roles still do not"
|
||||||
|
Task: "Extend tests/Feature/Workspaces/WorkspaceNavigationHubTest.php to confirm navigation separation remains Switch workspace in the topbar only and Manage workspaces in admin navigation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (User Story 1 Only)
|
||||||
|
|
||||||
|
1. Complete Phase 1: Setup
|
||||||
|
2. Complete Phase 2: Foundational
|
||||||
|
3. Complete Phase 3: User Story 1
|
||||||
|
4. Validate the rendered topbar switch target
|
||||||
|
5. Stop and review before broader regression work if needed
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Ship the chooser-target fix in US1
|
||||||
|
2. Add management-separation regression protection in US2
|
||||||
|
3. Add chooser-flow coherence regression protection in US3
|
||||||
|
4. Finish with focused tests and formatting
|
||||||
|
|
||||||
|
### Parallel Team Strategy
|
||||||
|
|
||||||
|
With multiple contributors:
|
||||||
|
|
||||||
|
1. One contributor updates the context-bar and topbar regression tests (US1)
|
||||||
|
2. One contributor hardens management-separation regressions (US2)
|
||||||
|
3. One contributor hardens chooser-flow regressions (US3)
|
||||||
|
4. Recombine for focused Sail test execution and Pint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- [P] tasks touch different files and can be executed in parallel.
|
||||||
|
- User story labels map each task to the corresponding spec story.
|
||||||
|
- No migrations, no new routes, and no new dependencies are expected.
|
||||||
|
- Preserve existing chooser redirect behavior unless a failing regression test proves otherwise.
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\ChooseWorkspace;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
@ -15,7 +16,7 @@
|
|||||||
|
|
||||||
Filament::setTenant(null, true);
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
||||||
@ -31,6 +32,15 @@
|
|||||||
->assertSee('Clear tenant context')
|
->assertSee('Clear tenant context')
|
||||||
->assertSee($tenant->getFilamentName());
|
->assertSee($tenant->getFilamentName());
|
||||||
|
|
||||||
|
$content = $response->getContent();
|
||||||
|
|
||||||
|
expect($content)->not->toBeFalse();
|
||||||
|
expect(
|
||||||
|
preg_match('/<a[^>]+href="([^"]+)"[^>]*>\s*Switch workspace\s*<\/a>/i', (string) $content, $matches)
|
||||||
|
)->toBe(1);
|
||||||
|
expect($matches[1] ?? null)->toBe(ChooseWorkspace::getUrl(panel: 'admin').'?choose=1');
|
||||||
|
expect($matches[1] ?? null)->not->toContain('/admin/workspaces');
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
|
|||||||
@ -163,7 +163,8 @@
|
|||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get(route('filament.admin.pages.choose-workspace'))
|
->get(route('filament.admin.pages.choose-workspace'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Manage workspaces');
|
->assertSee('Manage workspaces')
|
||||||
|
->assertSee(route('filament.admin.resources.workspaces.index', absolute: false), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides manage link for non-owner roles', function (): void {
|
it('hides manage link for non-owner roles', function (): void {
|
||||||
@ -181,7 +182,8 @@
|
|||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get(route('filament.admin.pages.choose-workspace'))
|
->get(route('filament.admin.pages.choose-workspace'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('Manage workspaces');
|
->assertDontSee('Manage workspaces')
|
||||||
|
->assertDontSee(route('filament.admin.resources.workspaces.index', absolute: false), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- T020: it_has_no_n_plus_1_queries_in_chooser ---
|
// --- T020: it_has_no_n_plus_1_queries_in_chooser ---
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -91,3 +92,21 @@
|
|||||||
->call('selectWorkspace', $workspace->getKey())
|
->call('selectWorkspace', $workspace->getKey())
|
||||||
->assertRedirect('/admin/choose-tenant');
|
->assertRedirect('/admin/choose-tenant');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers the stored intended url after selecting a workspace', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$workspace = Workspace::factory()->create();
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => $workspace->getKey(),
|
||||||
|
'user_id' => $user->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
|
||||||
|
session()->put(WorkspaceContext::INTENDED_URL_SESSION_KEY, '/admin/operations');
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(ChooseWorkspace::class)
|
||||||
|
->call('selectWorkspace', $workspace->getKey())
|
||||||
|
->assertRedirect('/admin/operations');
|
||||||
|
});
|
||||||
|
|||||||
@ -220,6 +220,30 @@
|
|||||||
$response->assertRedirect('/admin/choose-workspace');
|
$response->assertRedirect('/admin/choose-workspace');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redirects to the chooser when switching is explicitly requested with choose=1', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspaceA = Workspace::factory()->create();
|
||||||
|
$workspaceB = Workspace::factory()->create();
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => $workspaceA->getKey(),
|
||||||
|
'user_id' => $user->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => $workspaceB->getKey(),
|
||||||
|
'user_id' => $user->getKey(),
|
||||||
|
'role' => 'operator',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspaceA->getKey()])
|
||||||
|
->get('/admin/_test/workspace-context?choose=1');
|
||||||
|
|
||||||
|
$response->assertRedirect('/admin/choose-workspace');
|
||||||
|
});
|
||||||
|
|
||||||
// --- T023: it_clears_session_when_active_workspace_membership_revoked ---
|
// --- T023: it_clears_session_when_active_workspace_membership_revoked ---
|
||||||
|
|
||||||
it('clears session when active workspace membership revoked', function (): void {
|
it('clears session when active workspace membership revoked', function (): void {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\ChooseWorkspace;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
@ -11,30 +12,46 @@
|
|||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
it('does not show "Switch workspace" in sidebar navigation (topbar-only)', function (): void {
|
it('keeps workspace switching out of sidebar navigation while management stays on its dedicated admin path', function (): void {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$workspace = Workspace::factory()->create();
|
$workspace = Workspace::factory()->create();
|
||||||
|
$secondaryWorkspace = Workspace::factory()->create();
|
||||||
|
|
||||||
WorkspaceMembership::factory()->create([
|
WorkspaceMembership::factory()->create([
|
||||||
'workspace_id' => $workspace->getKey(),
|
'workspace_id' => $workspace->getKey(),
|
||||||
'user_id' => $user->getKey(),
|
'user_id' => $user->getKey(),
|
||||||
'role' => 'owner',
|
'role' => 'owner',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => $secondaryWorkspace->getKey(),
|
||||||
|
'user_id' => $user->getKey(),
|
||||||
|
'role' => 'operator',
|
||||||
|
]);
|
||||||
|
|
||||||
Filament::setTenant(null, true);
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin/operations')
|
->get('/admin/operations')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
|
$response->assertSee('Switch workspace')
|
||||||
|
->assertSee(ChooseWorkspace::getUrl(panel: 'admin').'?choose=1', false);
|
||||||
|
|
||||||
$panel = Filament::getCurrentOrDefaultPanel();
|
$panel = Filament::getCurrentOrDefaultPanel();
|
||||||
|
|
||||||
$labels = collect($panel->getNavigationItems())
|
$navigationItems = collect($panel->getNavigationItems());
|
||||||
|
$labels = $navigationItems
|
||||||
->map(static fn ($item): string => $item->getLabel())
|
->map(static fn ($item): string => $item->getLabel())
|
||||||
->all();
|
->all();
|
||||||
|
$manageWorkspaces = $navigationItems
|
||||||
|
->first(static fn ($item): bool => $item->getLabel() === 'Manage workspaces');
|
||||||
|
|
||||||
expect($labels)->not->toContain('Switch workspace');
|
expect($labels)->not->toContain('Switch workspace');
|
||||||
expect($labels)->toContain('Manage workspaces');
|
expect($labels)->toContain('Manage workspaces');
|
||||||
expect($labels)->not->toContain('Workspaces');
|
expect($labels)->not->toContain('Workspaces');
|
||||||
|
expect($manageWorkspaces)->not->toBeNull();
|
||||||
|
expect($manageWorkspaces->getUrl())->toBe(route('filament.admin.resources.workspaces.index'));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
@ -15,9 +16,9 @@
|
|||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- T032: it_shows_switch_workspace_menu_when_multiple_workspaces ---
|
// --- T032: it_keeps_workspace_switch_out_of_user_menu_when_multiple_workspaces ---
|
||||||
|
|
||||||
it('shows switch workspace in user menu when multiple workspaces', function (): void {
|
it('keeps switch workspace out of the user menu when multiple workspaces exist', function (): void {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$workspaceA = Workspace::factory()->create(['name' => 'Workspace Alpha']);
|
$workspaceA = Workspace::factory()->create(['name' => 'Workspace Alpha']);
|
||||||
@ -44,12 +45,12 @@
|
|||||||
->get('/admin/workspaces');
|
->get('/admin/workspaces');
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('choose-workspace?choose=1', false);
|
expect(Filament::getUserMenuItems())->not->toHaveKey('switch-workspace');
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- T032: it_hides_switch_workspace_menu_when_single_workspace ---
|
// --- T032: it_keeps_workspace_switch_out_of_user_menu_when_single_workspace ---
|
||||||
|
|
||||||
it('hides switch workspace in user menu when single workspace', function (): void {
|
it('keeps switch workspace out of the user menu when single workspace exists', function (): void {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$workspace = Workspace::factory()->create(['name' => 'Solo Workspace']);
|
$workspace = Workspace::factory()->create(['name' => 'Solo Workspace']);
|
||||||
@ -69,5 +70,6 @@
|
|||||||
->get('/admin/workspaces');
|
->get('/admin/workspaces');
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertDontSee('choose-workspace?choose=1', false);
|
|
||||||
|
expect(Filament::getUserMenuItems())->not->toHaveKey('switch-workspace');
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user