107 lines
9.1 KiB
Markdown
107 lines
9.1 KiB
Markdown
# Research: Workspace Chooser v1
|
|
|
|
**Feature**: 107-workspace-chooser | **Date**: 2026-02-22
|
|
|
|
## R1: Middleware Refactor Strategy
|
|
|
|
**Question**: Should we create a new `EnsureActiveWorkspace` middleware or refactor the existing `EnsureWorkspaceSelected`?
|
|
|
|
- **Decision**: Refactor the existing `EnsureWorkspaceSelected` middleware in-place.
|
|
- **Rationale**: The existing middleware is already registered in both `AdminPanelProvider` and `TenantPanelProvider` middleware stacks as `'ensure-workspace-selected'`, and referenced by alias in `bootstrap/app.php`. Creating a new class would require changing all registration points and updating existing tests. The current class already handles the same responsibilities — it just doesn't implement them according to the spec.
|
|
- **Alternatives considered**:
|
|
- New `EnsureActiveWorkspace` class: rejected because it would require renaming the middleware alias everywhere, with no functional benefit beyond a name change. The alias can remain `ensure-workspace-selected` for backward compatibility.
|
|
|
|
## R2: Audit Event Integration Pattern
|
|
|
|
**Question**: How should workspace selection audit events be emitted?
|
|
|
|
- **Decision**: Call `WorkspaceAuditLogger::log()` directly from the middleware (for auto-selections) and from `ChooseWorkspace::selectWorkspace()` (for manual selections). No events/listeners needed.
|
|
- **Rationale**: `WorkspaceAuditLogger` is a simple synchronous service — no queue, no listener. The codebase pattern (used in workspace membership, settings, alert destinations, baselines, etc.) is direct `$logger->log(...)` calls at the mutation point. Workspace selection audit is similarly < 1ms DB insert.
|
|
- **Alternatives considered**:
|
|
- Laravel Events + Listeners: rejected — overkill for a synchronous log write. No other systems need to react to workspace selection events.
|
|
- Observer on `User` model (`last_workspace_id` change): rejected — would miss cases where only the session changes (auto-resume from session), and would conflate preference persistence with audit semantics.
|
|
|
|
## R3: `AuditActionId` Enum Values
|
|
|
|
**Question**: What enum values and string representations to use?
|
|
|
|
- **Decision**: Add two cases:
|
|
- `WorkspaceAutoSelected = 'workspace.auto_selected'` — for auto-resume (single membership or last-used).
|
|
- `WorkspaceSelected = 'workspace.selected'` — for manual selection from chooser.
|
|
- **Rationale**: Follows the existing naming pattern (`case CamelName = 'snake.dotted_value'`). The `method` (auto/manual) and `reason` (single_membership/last_used/chooser) are stored in the audit log's `metadata` JSONB, not in separate enum values.
|
|
- **Alternatives considered**:
|
|
- Three separate enum values (one per reason): rejected — metadata provides sufficient granularity; enum values should represent the action type, not the trigger.
|
|
|
|
## R4: Redirect After Selection (Tenant-Count Branching)
|
|
|
|
**Question**: Where does redirect logic live? Should it be deduplicated?
|
|
|
|
- **Decision**: Extract the tenant-count branching logic into a shared helper method on `WorkspaceContext` or a dedicated `WorkspaceRedirectResolver` to avoid duplicating it across:
|
|
1. `EnsureWorkspaceSelected` middleware (auto-resume redirects)
|
|
2. `ChooseWorkspace::selectWorkspace()` (manual selection redirect)
|
|
3. `SwitchWorkspaceController::__invoke()` (POST switch redirect)
|
|
4. `routes/web.php` `/admin` route handler
|
|
|
|
Currently, the same branching logic (0 tenants → managed-tenants, 1 → tenant dashboard, >1 → choose-tenant) is copy-pasted in all four locations.
|
|
- **Rationale**: DRY — the branching is identical in all cases and is the single authority for "where to go after workspace is set." A single method eliminates the risk of divergence as new conditions are added.
|
|
- **Alternatives considered**:
|
|
- Leave duplicated: rejected — 4 copies of the same logic is a maintenance hazard.
|
|
- Put on `ChooseWorkspace` page: rejected — the middleware and controller both need it but don't have access to the page class.
|
|
|
|
## R5: `?choose=1` Handling Location
|
|
|
|
**Question**: Should the `?choose=1` forced-chooser parameter be handled in the middleware or in the page?
|
|
|
|
- **Decision**: Handle in the middleware — step 2 of the algorithm. If `choose=1` is present, redirect to `/admin/choose-workspace?choose=1` and skip auto-resume logic.
|
|
- **Rationale**: The middleware is the single entry point for all `/admin/*` requests. Handling it there prevents auto-resume from overriding the explicit user intent to see the chooser.
|
|
- **Alternatives considered**:
|
|
- Handle in `ChooseWorkspace` page: rejected — the middleware would auto-resume BEFORE the page loads, so the user would never see the chooser.
|
|
|
|
## R6: User Menu Integration
|
|
|
|
**Question**: How to add "Switch workspace" to the Filament user menu?
|
|
|
|
- **Decision**: Register via `->userMenuItems()` in `AdminPanelProvider::panel()`. Use `Filament\Navigation\MenuItem::make()` with `->url('/admin/choose-workspace?choose=1')` and a `->visible()` callback that checks workspace membership count > 1.
|
|
- **Rationale**: This is the documented Filament v5 pattern from the constitution + blueprint. The menu item is a navigation-only action (URL link), not a destructive action, so no confirmation needed.
|
|
- **Alternatives considered**:
|
|
- Context bar link only: rejected — specification explicitly requires a user menu entry (FR-008).
|
|
- Adding to both user menu and context bar: the context bar already has "Switch workspace" — the user menu entry provides an additional discovery point per spec.
|
|
|
|
## R7: Badge Rendering for Workspace Role
|
|
|
|
**Question**: Should workspace membership role badges use `BadgeCatalog`/`BadgeDomain`?
|
|
|
|
- **Decision**: No. Workspace membership role is a **tag/category** (owner, admin, member), not a status-like value. Per constitution (BADGE-001), tag/category chips are not governed by `BadgeCatalog`. Use a simple Filament `<x-filament::badge>` with a color mapping (e.g., owner → primary, admin → warning, member → gray).
|
|
- **Rationale**: The role is static metadata, not a state transition. No existing `BadgeDomain` for workspace roles. Adding one would be over-engineering for 3 static values.
|
|
- **Alternatives considered**:
|
|
- Create a `WorkspaceRoleBadgeDomain`: rejected — violates the "tag, not status" exemption in BADGE-001.
|
|
|
|
## R8: Blade Template vs. Livewire Component for Chooser
|
|
|
|
**Question**: Should the chooser cards remain as a Blade template or be converted to a Livewire component?
|
|
|
|
- **Decision**: Keep as Blade template rendered by the existing Livewire-backed `ChooseWorkspace` Filament Page. The page class already extends `Filament\Pages\Page` (which is Livewire). The `wire:click` for "Open" calls the existing `selectWorkspace()` method. No separate Livewire component needed.
|
|
- **Rationale**: The existing pattern works. The page is already a Livewire component (all Filament Pages are). Converting to a separate Livewire component adds complexity with no benefit — the chooser has no real-time reactive needs.
|
|
- **Alternatives considered**:
|
|
- Separate `WorkspaceChooserCard` Livewire component: rejected — unnecessary abstraction for a simple card grid.
|
|
|
|
## R9: Existing `SwitchWorkspaceController` Coexistence
|
|
|
|
**Question**: The chooser currently uses POST to `SwitchWorkspaceController`. Should we switch to Livewire `wire:click` or keep the POST?
|
|
|
|
- **Decision**: Migrate the chooser page to use Livewire `wire:click` calling `selectWorkspace($workspaceId)` (which already exists). The `SwitchWorkspaceController` is still needed for the `workspace-switcher.blade.php` partial (context-bar dropdown) which uses a form POST. Both paths converge on `WorkspaceContext::setCurrentWorkspace()`.
|
|
- **Rationale**: The Livewire path already exists in `ChooseWorkspace::selectWorkspace()` — the blade template just needs to call it via `wire:click` instead of a form POST. This simplifies the chooser page and makes audit integration easier (audit logging happens in the PHP method, not in a controller).
|
|
- **Alternatives considered**:
|
|
- Keep form POST from chooser: rejected — the `selectWorkspace()` method is where we add audit logging. Using `wire:click` means a single code path.
|
|
- Remove `SwitchWorkspaceController` entirely: deferred — the context-bar dropdown still uses it. Can be unified in a future PR.
|
|
|
|
## R10: Flash Warning Implementation
|
|
|
|
**Question**: How to show "Your access to {workspace_name} was removed" when stale membership detected?
|
|
|
|
- **Decision**: Use Filament database notifications or session flash + `Filament\Notifications\Notification::make()->danger()`. Since the middleware redirects to the chooser, and the chooser is a Filament page, Filament's notification system renders flash/database notifications automatically.
|
|
- **Rationale**: The chooser is a Filament page — Filament's notification toast system is already wired. Session-based `Notification::make()` works for redirect→page scenarios.
|
|
- **Alternatives considered**:
|
|
- Custom Blade banner: rejected — Filament notifications already solve this and are consistent with the rest of the app.
|
|
- Session flash only (no Filament notification): rejected — the Filament notification system provides better UX (auto-dismiss, consistent styling).
|