tasks: workspace chooser v1 — 40 tasks across 6 user stories + foundation + polish
This commit is contained in:
parent
dd4fb6071f
commit
d27cfec5cb
287
specs/107-workspace-chooser/tasks.md
Normal file
287
specs/107-workspace-chooser/tasks.md
Normal 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 (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 (`<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 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)
|
||||
Loading…
Reference in New Issue
Block a user