19 KiB
Tasks: Workspace Chooser v1 (Enterprise) + In-App Switch Entry Point
Input: Design documents from /specs/107-workspace-chooser/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/routes.md, quickstart.md
Tests: REQUIRED (Pest) — all changes involve runtime behavior. 17 test cases from spec + additional integration tests.
Operations: No OperationRun needed — workspace selection is synchronous, DB-only, < 2s. Audit entries via WorkspaceAuditLogger.
RBAC:
- Authorization plane: admin
/admin - Membership = switch-right (any workspace member may select/switch)
- Non-member selection attempt → 404 (deny-as-not-found) via
WorkspaceContext::isMember() Capabilities::WORKSPACE_MANAGEgates "Manage workspaces" link visibility (canonical registry constant)- Positive test: member selects workspace → success
- Negative test: non-member attempt → 404
Filament UI Action Surfaces: ChooseWorkspace is a custom context-selector page (not CRUD Resource). UI Action Matrix in spec — no header actions (v1), "Open" per workspace, empty state with specific title + CTA. Exemption from UX-001 documented.
Badges: Workspace membership role badge is a tag/category (owner/admin/member), exempt from BADGE-001 per R7 decision. Simple color-mapped
<x-filament::badge>, noBadgeCatalog.
Organization: Tasks grouped by user story. Stories map to quickstart phases:
- Foundation → Phase A (enum + redirect resolver)
- US1+US2 → Phase B (middleware refactor, incremental)
- US3 → Phase C (chooser page upgrade)
- US4 → Phase B enhancement (stale detection)
- US5 → Phase C+D (chooser audit + user menu)
- US6 → Verification (audit payloads)
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (US1–US6)
- Exact file paths included in all descriptions
Phase 1: Setup
Purpose: No project initialization needed — existing Laravel monolith with Filament v5.
(No tasks — project structure, dependencies, and all target directories already exist.)
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Core infrastructure that MUST be complete before ANY user story can be implemented.
⚠️ CRITICAL: No user story work can begin until this phase is complete.
- T001 [P] Add
WorkspaceAutoSelectedandWorkspaceSelectedenum cases toapp/Support/Audit/AuditActionId.php - T002 [P] Create
WorkspaceRedirectResolverservice with tenant-count branching logic (0→managed tenants, 1→tenant dashboard, >1→choose tenant) inapp/Support/Workspaces/WorkspaceRedirectResolver.php - T003 Write tests for
WorkspaceRedirectResolvercovering 0/1/>1 tenant branching intests/Feature/Workspaces/WorkspaceRedirectResolverTest.php
Checkpoint: Foundation ready — AuditActionId enum extended, tenant-count branching deduplicated into resolver. User story implementation can now begin.
Phase 3: User Story 1 — Auto-Resume: Single Workspace (Priority: P1) 🎯 MVP
Goal: A user with exactly one workspace membership is taken directly to their workspace dashboard without seeing the chooser.
Independent Test: Create a user with one workspace membership, hit /admin, verify redirect to workspace dashboard without chooser.
Implementation for User Story 1
- T004 [US1] Refactor
EnsureWorkspaceSelectedmiddleware: implement step 1 (workspace-optional bypass), step 2 (?choose=1redirect), step 3 (basic session validation — allow if valid membership), step 4 (load selectable memberships), step 5 (single membership auto-resume with audit viaWorkspaceAuditLogger), step 7 (fallback redirect to chooser) inapp/Http/Middleware/EnsureWorkspaceSelected.php - T005 [US1] Write test
it_skips_chooser_when_single_workspace_membership— verify direct redirect to workspace dashboard intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T006 [US1] Write test
it_emits_audit_event_on_auto_selection_single_membership— verifyworkspace.auto_selectedaudit log with reasonsingle_membershipintests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T007 [US1] Write test
it_redirects_via_tenant_count_branching_after_single_auto_resume— verify 0/1/>1 tenant routing after auto-resume intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T008 [US1] Write test
it_allows_request_when_session_workspace_is_valid— verify middleware passes through when session has valid membership intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
Checkpoint: Single-workspace users bypass the chooser entirely. Audit event emitted. Middleware skeleton (7 steps) in place with steps 1–5, 7 active.
Phase 4: User Story 2 — Auto-Resume: Last Used Workspace (Priority: P1)
Goal: A user with multiple workspaces who has a valid last_workspace_id is taken directly to that workspace without the chooser.
Independent Test: Create a user with 2+ workspaces and a valid last_workspace_id, hit /admin, verify direct entry.
Implementation for User Story 2
- T009 [US2] Add step 6 to
EnsureWorkspaceSelectedmiddleware:last_workspace_idauto-resume with membership validation and audit logging inapp/Http/Middleware/EnsureWorkspaceSelected.php - T010 [US2] Write test
it_auto_resumes_to_last_used_workspace_when_membership_valid— verify direct redirect via last_workspace_id intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T011 [US2] Write test
it_emits_audit_event_on_auto_selection_last_used— verifyworkspace.auto_selectedaudit log with reasonlast_usedintests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T012 [US2] Write test
it_falls_back_to_chooser_when_multiple_workspaces_and_no_last_used— verify redirect to chooser when no default intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
Checkpoint: Multi-workspace users with a valid last-used preference bypass the chooser. Both P1 auto-resume paths (single + last-used) are functional.
Phase 5: User Story 3 — Chooser Fallback: Multiple Workspaces, No Default (Priority: P1)
Goal: A user with multiple workspaces and no valid last_workspace_id sees the chooser with enterprise metadata (name, role badge, tenant count).
Independent Test: Create a user with 3 workspaces (varying roles, tenant counts), clear last_workspace_id, visit /admin, verify chooser renders with metadata.
Implementation for User Story 3
- T013 [US3] Refactor
ChooseWorkspace::getWorkspaces()to addwithCount('tenants')and load membership roles keyed by workspace_id; expose$this->workspaceRolesfor Blade inapp/Filament/Pages/ChooseWorkspace.php - T014 [US3] Remove "Create workspace" header action from
ChooseWorkspacepage (FR-006) inapp/Filament/Pages/ChooseWorkspace.php - T015 [US3] Update Blade template: add role badge (
<x-filament::badge>with color mapping for owner/admin/member), tenant count display, "Manage workspaces" link (gated byCapabilities::WORKSPACE_MANAGE), updated empty state copy per spec terminology inresources/views/filament/pages/choose-workspace.blade.php - T016 [US3] Write test
it_only_lists_workspaces_user_is_member_of— create workspaces user is and isn't a member of, verify only member workspaces shown intests/Feature/Workspaces/ChooseWorkspacePageTest.php - T017 [US3] Write test
it_shows_name_role_and_tenants_count_per_workspace— verify metadata rendered in chooser cards intests/Feature/Workspaces/ChooseWorkspacePageTest.php - T018 [US3] Write test
it_shows_empty_state_when_no_memberships— verify "You don't have access to any workspace yet." message intests/Feature/Workspaces/ChooseWorkspacePageTest.php - T019 [US3] Write test
it_hides_manage_link_without_workspace_manage_capabilityandit_shows_manage_link_with_workspace_manage_capability— positive + negative authorization intests/Feature/Workspaces/ChooseWorkspacePageTest.php - T020 [US3] Write test
it_has_no_n_plus_1_queries_in_chooser— assert query count with 5+ workspaces intests/Feature/Workspaces/ChooseWorkspacePageTest.php
Checkpoint: Chooser page displays enterprise metadata. All three P1 stories are functional — auto-resume (single + last-used) and chooser fallback with metadata.
Phase 6: User Story 4 — Stale Session / Revoked Membership (Priority: P2)
Goal: A user whose workspace membership was revoked between sessions sees a clear warning and is redirected to the chooser.
Independent Test: Set session to a workspace, delete the membership, visit /admin, verify warning + chooser.
Implementation for User Story 4
- T021 [US4] Enhance middleware step 3: detect stale session (revoked membership or archived workspace), clear session, emit Filament
Notification::make()->danger()with "Your access to {workspace_name} was removed." flash, redirect to chooser inapp/Http/Middleware/EnsureWorkspaceSelected.php - T022 [US4] Enhance middleware step 6 error path: detect stale
last_workspace_id(revoked or archived), clearlast_workspace_idon user record, emit flash warning, redirect to chooser inapp/Http/Middleware/EnsureWorkspaceSelected.php - T023 [US4] Write test
it_clears_session_when_active_workspace_membership_revoked— verify session cleared + warning notification + chooser redirect intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T024 [US4] Write test
it_redirects_to_chooser_when_last_workspace_membership_revoked_and_shows_warning— verifylast_workspace_idcleared + warning + chooser, including archived workspace scenario (edge case EC2) intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T025 [US4] Write test
it_handles_archived_workspace_in_session— verify archived workspace treated as stale intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
Checkpoint: Stale/revoked membership detection is active. Users see clear warning notifications instead of broken states.
Phase 7: User Story 5 — Manual Workspace Switch (Priority: P2)
Goal: A user can switch workspaces from within the app via the user menu, which takes them to the chooser.
Independent Test: As a logged-in user with active workspace, click "Switch workspace" in user menu, verify chooser loads with ?choose=1.
Implementation for User Story 5
- T026 [US5] Add audit logging in
ChooseWorkspace::selectWorkspace()— emitworkspace.selectedviaWorkspaceAuditLoggerwith metadata{method: "manual", reason: "chooser", prev_workspace_id}inapp/Filament/Pages/ChooseWorkspace.php - T027 [US5] Replace form POST with
wire:click="selectWorkspace({{ $workspace->id }})"in chooser Blade template inresources/views/filament/pages/choose-workspace.blade.php - T028 [US5] Use
WorkspaceRedirectResolverinChooseWorkspace::redirectAfterWorkspaceSelected()for tenant-count branching inapp/Filament/Pages/ChooseWorkspace.php - T029 [US5] Register "Switch workspace" user menu item via
->userMenuItems()withMenuItem::make()->url('/admin/choose-workspace?choose=1')->icon('heroicon-o-arrows-right-left')and->visible()callback (>1 workspace membership) inapp/Providers/Filament/AdminPanelProvider.php - T030 [US5] Write test
it_forces_chooser_with_choose_param— verify?choose=1bypasses auto-resume, including the single-workspace sub-case (edge case EC3: forced chooser shown even with 1 membership) intests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php - T031 [US5] Write test
it_persists_last_used_workspace_on_manual_selectionandit_emits_audit_event_on_manual_selection— verifylast_workspace_idupdate +workspace.selectedaudit log intests/Feature/Workspaces/ChooseWorkspacePageTest.php - T032 [US5] Write test
it_shows_switch_workspace_menu_when_multiple_workspacesandit_hides_switch_workspace_menu_when_single_workspaceintests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php - T033 [US5] Write test
it_rejects_non_member_workspace_selection_with_404— verify deny-as-not-found for non-member attempt intests/Feature/Workspaces/ChooseWorkspacePageTest.php
Checkpoint: Manual switch flow complete — user menu entry, Livewire selection, audit logging, and 404 for non-members.
Phase 8: User Story 6 — Audit Trail for Workspace Context Changes (Priority: P2)
Goal: Every workspace selection (auto or manual) produces an audit log entry with correct payloads for compliance.
Independent Test: Trigger auto-resume and manual selection, verify audit log entries with correct payloads.
Implementation for User Story 6
- T034 [US6] Write comprehensive audit payload verification test covering all four audit scenarios (auto/single_membership, auto/last_used, manual/chooser, manual/context_bar) with full metadata assertion (
method,reason,prev_workspace_id,resource_type,resource_id) intests/Feature/Workspaces/WorkspaceAuditTrailTest.php - T035 [US6] Write test
it_includes_prev_workspace_id_when_switching_from_active_workspace— verify previous workspace context is captured in audit metadata intests/Feature/Workspaces/WorkspaceAuditTrailTest.php
Checkpoint: All six user stories are implemented and tested. Audit trail is verified for compliance.
Phase 9: Polish & Cross-Cutting Concerns
Purpose: Deduplicate remaining tenant-branching copies, full suite validation, formatting.
- T036 [US6] Replace inline tenant-count branching in
SwitchWorkspaceController::__invoke()withWorkspaceRedirectResolverAND addWorkspaceAuditLogger::log()forworkspace.selected(method:manual, reason:context_bar) to satisfy FR-005 audit coverage for the context-bar switch path, inapp/Http/Controllers/SwitchWorkspaceController.php - T037 Replace inline tenant-count branching in
/adminroute handler withWorkspaceRedirectResolverinroutes/web.php - T038 Run full test suite via
vendor/bin/sail artisan test --compactand verify no regressions - T039 Run Pint formatting via
vendor/bin/sail bin pint --dirty --format agent - T040 Final commit and push to branch
107-workspace-chooser
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): N/A — project already initialized
- Foundational (Phase 2): No dependencies — can start immediately. BLOCKS all user stories
- US1 (Phase 3): Depends on Phase 2 (AuditActionId enum + WorkspaceRedirectResolver)
- US2 (Phase 4): Depends on Phase 3 (middleware skeleton from US1)
- US3 (Phase 5): Depends on Phase 2 only — can run in parallel with US1/US2
- US4 (Phase 6): Depends on Phase 3 (middleware skeleton from US1)
- US5 (Phase 7): Depends on Phase 5 (chooser page from US3) + Phase 3 (middleware
?choose=1) - US6 (Phase 8): Depends on Phases 3–7 (all audit-emitting code must exist)
- Polish (Phase 9): Depends on all user stories being complete
User Story Dependencies
- US1 (P1): After Foundational → provides middleware skeleton for US2 + US4
- US2 (P1): After US1 → extends middleware with step 6
- US3 (P1): After Foundational → independent from US1/US2 (different files)
- US4 (P2): After US1 → enhances middleware error paths
- US5 (P2): After US3 (chooser page) + US1 (middleware ?choose=1)
- US6 (P2): After US1–US5 → verifies audit payloads across all paths
Within Each User Story
- Implementation before tests (refactoring existing code — not greenfield TDD)
- Core changes before edge-case handling
- Story complete before moving to next priority
Parallel Opportunities
- Phase 2: T001 and T002 can run in parallel (different files)
- After Phase 2: US3 (chooser page) can start in parallel with US1 (middleware)
- After Phase 3: US2 and US4 can start in parallel (US4 enhances middleware error paths, US2 adds step 6)
- Tests within same file: Sequential (same file), but different test files can run in parallel
Parallel Example: After Foundational
# Developer A: Middleware track (US1 → US2 → US4)
T004 Refactor EnsureWorkspaceSelected (US1)
T009 Add step 6 to middleware (US2)
T021–T022 Enhance error paths (US4)
# Developer B: Chooser page track (US3 → US5)
T013–T015 Upgrade ChooseWorkspace page + Blade (US3)
T026–T029 Add audit + wire:click + user menu (US5)
# Both tracks converge at:
T034–T035 Audit trail verification (US6)
T036–T040 Polish & cross-cutting
Implementation Strategy
MVP First (US1 Only)
- Complete Phase 2: Foundational (T001–T003)
- Complete Phase 3: US1 — Single workspace auto-resume (T004–T008)
- STOP and VALIDATE: Single-workspace users bypass chooser, audit logged
- Deploy/demo if ready → immediate UX improvement for majority of users
Incremental Delivery
- Foundation (T001–T003) → Core infrastructure ready
- US1 (T004–T008) → Single workspace auto-resume → MVP!
- US2 (T009–T012) → Last-used auto-resume → Multi-workspace friction reduced
- US3 (T013–T020) → Enterprise metadata in chooser → Better selection UX
- US4 (T021–T025) → Stale session handling → Governance safety net
- US5 (T026–T033) → Manual switch via user menu → Full switch flow
- US6 (T034–T035) → Audit verification → Compliance confidence
- Polish (T036–T040) → DRY codebase, full suite green
Each story adds value without breaking previous stories.
Summary
| Metric | Value |
|---|---|
| Total tasks | 40 |
| Phase 2 (Foundational) | 3 tasks |
| US1 (Auto-resume single) | 5 tasks |
| US2 (Auto-resume last-used) | 4 tasks |
| US3 (Chooser metadata) | 8 tasks |
| US4 (Stale session) | 5 tasks |
| US5 (Manual switch) | 8 tasks |
| US6 (Audit verification) | 2 tasks |
| Polish | 5 tasks |
| Parallel opportunities | 2 independent tracks (middleware + chooser page) after foundation |
| MVP scope | Foundation + US1 (8 tasks) |
| New files | 6 (1 service + 5 test files) |
| Modified files | 6 (middleware, page, blade, enum, provider, controller + routes) |
Notes
- All tasks reference exact file paths from plan.md project structure
- Audit logging uses direct
WorkspaceAuditLogger::log()calls (decision R2) - Middleware is refactored in-place (decision R1) — alias
ensure-workspace-selectedunchanged - Chooser migrates to
wire:click(decision R9) —SwitchWorkspaceControllerretained for context-bar - Flash warnings use Filament
Notification::make()->danger()(decision R10) - Role badge uses simple color mapping, exempt from BadgeCatalog (decision R7)
WorkspaceRedirectResolverdeduplicates 4 copies of tenant-count branching (decision R4)