From d27cfec5cbc20591ae8019e198836fb4566dd13f Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sun, 22 Feb 2026 15:00:23 +0100 Subject: [PATCH] =?UTF-8?q?tasks:=20workspace=20chooser=20v1=20=E2=80=94?= =?UTF-8?q?=2040=20tasks=20across=206=20user=20stories=20+=20foundation=20?= =?UTF-8?q?+=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/107-workspace-chooser/tasks.md | 287 +++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 specs/107-workspace-chooser/tasks.md diff --git a/specs/107-workspace-chooser/tasks.md b/specs/107-workspace-chooser/tasks.md new file mode 100644 index 0000000..de6f245 --- /dev/null +++ b/specs/107-workspace-chooser/tasks.md @@ -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 ``, 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 (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 `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 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 `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 (`` 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 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) + +1. Complete Phase 2: Foundational (T001–T003) +2. Complete Phase 3: US1 — Single workspace auto-resume (T004–T008) +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 (T001–T003) → Core infrastructure ready +2. US1 (T004–T008) → Single workspace auto-resume → **MVP!** +3. US2 (T009–T012) → Last-used auto-resume → Multi-workspace friction reduced +4. US3 (T013–T020) → Enterprise metadata in chooser → Better selection UX +5. US4 (T021–T025) → Stale session handling → Governance safety net +6. US5 (T026–T033) → Manual switch via user menu → Full switch flow +7. US6 (T034–T035) → Audit verification → Compliance confidence +8. 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-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)