tasks: workspace chooser v1 — 40 tasks across 6 user stories + foundation + polish

This commit is contained in:
Ahmed Darrazi 2026-02-22 15:00:23 +01:00
parent dd4fb6071f
commit d27cfec5cb

View File

@ -0,0 +1,287 @@
# 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_MANAGE` gates "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>`, no `BadgeCatalog`.
**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 (US1US6)
- 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 `WorkspaceAutoSelected` and `WorkspaceSelected` enum cases to `app/Support/Audit/AuditActionId.php`
- [ ] T002 [P] Create `WorkspaceRedirectResolver` service with tenant-count branching logic (0→managed tenants, 1→tenant dashboard, >1→choose tenant) in `app/Support/Workspaces/WorkspaceRedirectResolver.php`
- [ ] T003 Write tests for `WorkspaceRedirectResolver` covering 0/1/>1 tenant branching in `tests/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 `EnsureWorkspaceSelected` middleware: implement step 1 (workspace-optional bypass), step 2 (`?choose=1` redirect), step 3 (basic session validation — allow if valid membership), step 4 (load selectable memberships), step 5 (single membership auto-resume with audit via `WorkspaceAuditLogger`), step 7 (fallback redirect to chooser) in `app/Http/Middleware/EnsureWorkspaceSelected.php`
- [ ] T005 [US1] Write test `it_skips_chooser_when_single_workspace_membership` — verify direct redirect to workspace dashboard in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [ ] T006 [US1] Write test `it_emits_audit_event_on_auto_selection_single_membership` — verify `workspace.auto_selected` audit log with reason `single_membership` in `tests/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 in `tests/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 in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
**Checkpoint**: Single-workspace users bypass the chooser entirely. Audit event emitted. Middleware skeleton (7 steps) in place with steps 15, 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 `EnsureWorkspaceSelected` middleware: `last_workspace_id` auto-resume with membership validation and audit logging in `app/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 in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [ ] T011 [US2] Write test `it_emits_audit_event_on_auto_selection_last_used` — verify `workspace.auto_selected` audit log with reason `last_used` in `tests/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 in `tests/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 add `withCount('tenants')` and load membership roles keyed by workspace_id; expose `$this->workspaceRoles` for Blade in `app/Filament/Pages/ChooseWorkspace.php`
- [ ] T014 [US3] Remove "Create workspace" header action from `ChooseWorkspace` page (FR-006) in `app/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 by `Capabilities::WORKSPACE_MANAGE`), updated empty state copy per spec terminology in `resources/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 in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [ ] T017 [US3] Write test `it_shows_name_role_and_tenants_count_per_workspace` — verify metadata rendered in chooser cards in `tests/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 in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [ ] T019 [US3] Write test `it_hides_manage_link_without_workspace_manage_capability` and `it_shows_manage_link_with_workspace_manage_capability` — positive + negative authorization in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [ ] T020 [US3] Write test `it_has_no_n_plus_1_queries_in_chooser` — assert query count with 5+ workspaces in `tests/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 in `app/Http/Middleware/EnsureWorkspaceSelected.php`
- [ ] T022 [US4] Enhance middleware step 6 error path: detect stale `last_workspace_id` (revoked or archived), clear `last_workspace_id` on user record, emit flash warning, redirect to chooser in `app/Http/Middleware/EnsureWorkspaceSelected.php`
- [ ] T023 [US4] Write test `it_clears_session_when_active_workspace_membership_revoked` — verify session cleared + warning notification + chooser redirect in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [ ] T024 [US4] Write test `it_redirects_to_chooser_when_last_workspace_membership_revoked_and_shows_warning` — verify `last_workspace_id` cleared + warning + chooser in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [ ] T025 [US4] Write test `it_handles_archived_workspace_in_session` — verify archived workspace treated as stale in `tests/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()` — emit `workspace.selected` via `WorkspaceAuditLogger` with metadata `{method: "manual", reason: "chooser", prev_workspace_id}` in `app/Filament/Pages/ChooseWorkspace.php`
- [ ] T027 [US5] Replace form POST with `wire:click="selectWorkspace({{ $workspace->id }})"` in chooser Blade template in `resources/views/filament/pages/choose-workspace.blade.php`
- [ ] T028 [US5] Use `WorkspaceRedirectResolver` in `ChooseWorkspace::redirectAfterWorkspaceSelected()` for tenant-count branching in `app/Filament/Pages/ChooseWorkspace.php`
- [ ] T029 [US5] Register "Switch workspace" user menu item via `->userMenuItems()` with `MenuItem::make()->url('/admin/choose-workspace?choose=1')->icon('heroicon-o-arrows-right-left')` and `->visible()` callback (>1 workspace membership) in `app/Providers/Filament/AdminPanelProvider.php`
- [ ] T030 [US5] Write test `it_forces_chooser_with_choose_param` — verify `?choose=1` bypasses auto-resume in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [ ] T031 [US5] Write test `it_persists_last_used_workspace_on_manual_selection` and `it_emits_audit_event_on_manual_selection` — verify `last_workspace_id` update + `workspace.selected` audit log in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [ ] T032 [US5] Write test `it_shows_switch_workspace_menu_when_multiple_workspaces` and `it_hides_switch_workspace_menu_when_single_workspace` in `tests/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 in `tests/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 three audit scenarios (auto/single_membership, auto/last_used, manual/chooser) with full metadata assertion (`method`, `reason`, `prev_workspace_id`, `resource_type`, `resource_id`) in `tests/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 in `tests/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 Replace inline tenant-count branching in `SwitchWorkspaceController::__invoke()` with `WorkspaceRedirectResolver` in `app/Http/Controllers/SwitchWorkspaceController.php`
- [ ] T037 Replace inline tenant-count branching in `/admin` route handler with `WorkspaceRedirectResolver` in `routes/web.php`
- [ ] T038 Run full test suite via `vendor/bin/sail artisan test --compact` and 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 37 (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 US1US5 → 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)
T021T022 Enhance error paths (US4)
# Developer B: Chooser page track (US3 → US5)
T013T015 Upgrade ChooseWorkspace page + Blade (US3)
T026T029 Add audit + wire:click + user menu (US5)
# Both tracks converge at:
T034T035 Audit trail verification (US6)
T036T040 Polish & cross-cutting
```
---
## Implementation Strategy
### MVP First (US1 Only)
1. Complete Phase 2: Foundational (T001T003)
2. Complete Phase 3: US1 — Single workspace auto-resume (T004T008)
3. **STOP and VALIDATE**: Single-workspace users bypass chooser, audit logged
4. Deploy/demo if ready → immediate UX improvement for majority of users
### Incremental Delivery
1. Foundation (T001T003) → Core infrastructure ready
2. US1 (T004T008) → Single workspace auto-resume → **MVP!**
3. US2 (T009T012) → Last-used auto-resume → Multi-workspace friction reduced
4. US3 (T013T020) → Enterprise metadata in chooser → Better selection UX
5. US4 (T021T025) → Stale session handling → Governance safety net
6. US5 (T026T033) → Manual switch via user menu → Full switch flow
7. US6 (T034T035) → Audit verification → Compliance confidence
8. Polish (T036T040) → 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-selected` unchanged
- Chooser migrates to `wire:click` (decision R9) — `SwitchWorkspaceController` retained for context-bar
- Flash warnings use Filament `Notification::make()->danger()` (decision R10)
- Role badge uses simple color mapping, exempt from BadgeCatalog (decision R7)
- `WorkspaceRedirectResolver` deduplicates 4 copies of tenant-count branching (decision R4)