diff --git a/app/Http/Middleware/ResolveWorkspace.php b/app/Http/Middleware/ResolveWorkspace.php index a93c371..b1b86be 100644 --- a/app/Http/Middleware/ResolveWorkspace.php +++ b/app/Http/Middleware/ResolveWorkspace.php @@ -3,11 +3,14 @@ namespace App\Http\Middleware; use App\Domains\Workspace\Models\Workspace; +use App\Domains\Workspace\Services\CurrentWorkspace; use Closure; use Illuminate\Http\Request; class ResolveWorkspace { + public function __construct(private CurrentWorkspace $current) {} + public function handle(Request $request, Closure $next): mixed { $ulid = $request->route('workspace'); @@ -16,7 +19,7 @@ class ResolveWorkspace return $next($request); } - $workspace = Workspace::where('ulid', $ulid)->firstOrFail(); + $workspace = Workspace::where('ulid', is_string($ulid) ? $ulid : $ulid->ulid)->firstOrFail(); // 404 instead of 403 — don't leak existence across workspace boundaries $user = $request->user(); @@ -26,8 +29,8 @@ class ResolveWorkspace abort(404); } - $request->merge(['current_workspace' => $workspace]); - app()->instance('current_workspace', $workspace); + $this->current->set($workspace); + $request->route()->setParameter('workspace', $workspace); return $next($request); } diff --git a/app/Livewire/Pages/Dashboard/Overview.php b/app/Livewire/Pages/Dashboard/Overview.php index 47b4ef6..d28340b 100644 --- a/app/Livewire/Pages/Dashboard/Overview.php +++ b/app/Livewire/Pages/Dashboard/Overview.php @@ -11,7 +11,7 @@ class Overview extends Component { public function render(): View { - $workspace = app('current_workspace'); + $workspace = require_workspace(); // Hero metrics $today = Click::where('workspace_id', $workspace->id) diff --git a/app/Livewire/Pages/Settings/Index.php b/app/Livewire/Pages/Settings/Index.php index abef19f..2bbfca8 100644 --- a/app/Livewire/Pages/Settings/Index.php +++ b/app/Livewire/Pages/Settings/Index.php @@ -13,7 +13,7 @@ class Index extends Component public function mount(): void { - $ws = app('current_workspace'); + $ws = current_workspace(); $this->workspaceName = $ws->name; $this->workspaceSlug = $ws->slug; } @@ -25,9 +25,9 @@ class Index extends Component 'workspaceSlug' => 'nullable|string|max:50|alpha_dash', ]); - app('current_workspace')->update([ + require_workspace()->update([ 'name' => $this->workspaceName, - 'slug' => $this->workspaceSlug ?: app('current_workspace')->slug, + 'slug' => $this->workspaceSlug ?: require_workspace()->slug, ]); session()->flash('status', 'Workspace settings saved.'); diff --git a/app/Livewire/Pages/Team/Members.php b/app/Livewire/Pages/Team/Members.php index a34fa50..c298928 100644 --- a/app/Livewire/Pages/Team/Members.php +++ b/app/Livewire/Pages/Team/Members.php @@ -10,7 +10,7 @@ class Members extends Component { public function render(): View { - $workspace = app('current_workspace'); + $workspace = require_workspace(); $members = WorkspaceMember::with('user') ->where('workspace_id', $workspace->id) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9177ac3..530ebbd 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Providers; use App\Domains\Translation\Providers\DatabaseTranslationLoader; use App\Domains\Workspace\Models\Workspace; use App\Domains\Workspace\Policies\WorkspacePolicy; +use App\Domains\Workspace\Services\CurrentWorkspace; use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; use Laravel\Cashier\Cashier; @@ -16,6 +17,8 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { + $this->app->singleton(CurrentWorkspace::class); + // TranslationServiceProvider is deferred and re-binds translation.loader when it // resolves. We extend 'translator' to swap in our DB loader after the instance is // created, then freeze it in the container so app('translation.loader') is consistent. diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..1c9e6e9 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,37 @@ +get(); + if ($ws) { + return $ws; + } + + // Backward-compat fallback: tests and legacy code may bind via app()->instance() + if (app()->bound('current_workspace')) { + /** @var Workspace $ws */ + $ws = app('current_workspace'); + app(CurrentWorkspace::class)->set($ws); + + return $ws; + } + + return null; + } +} + +if (! function_exists('require_workspace')) { + function require_workspace(): Workspace + { + $ws = current_workspace(); + if (! $ws) { + throw new \RuntimeException('No current workspace set. Route missing workspace.resolve middleware?'); + } + + return $ws; + } +} diff --git a/config/blade-icons.php b/config/blade-icons.php new file mode 100644 index 0000000..5aade2a --- /dev/null +++ b/config/blade-icons.php @@ -0,0 +1,183 @@ + [ + + // 'default' => [ + // + // /* + // |----------------------------------------------------------------- + // | Icons Path + // |----------------------------------------------------------------- + // | + // | Provide the relative path from your app root to your SVG icons + // | directory. Icons are loaded recursively so there's no need to + // | list every sub-directory. + // | + // | Relative to the disk root when the disk option is set. + // | + // */ + // + // 'path' => 'resources/svg', + // + // /* + // |----------------------------------------------------------------- + // | Filesystem Disk + // |----------------------------------------------------------------- + // | + // | Optionally, provide a specific filesystem disk to read + // | icons from. When defining a disk, the "path" option + // | starts relatively from the disk root. + // | + // */ + // + // 'disk' => '', + // + // /* + // |----------------------------------------------------------------- + // | Default Prefix + // |----------------------------------------------------------------- + // | + // | This config option allows you to define a default prefix for + // | your icons. The dash separator will be applied automatically + // | to every icon name. It's required and needs to be unique. + // | + // */ + // + // 'prefix' => 'icon', + // + // /* + // |----------------------------------------------------------------- + // | Fallback Icon + // |----------------------------------------------------------------- + // | + // | This config option allows you to define a fallback + // | icon when an icon in this set cannot be found. + // | + // */ + // + // 'fallback' => '', + // + // /* + // |----------------------------------------------------------------- + // | Default Set Classes + // |----------------------------------------------------------------- + // | + // | This config option allows you to define some classes which + // | will be applied by default to all icons within this set. + // | + // */ + // + // 'class' => '', + // + // /* + // |----------------------------------------------------------------- + // | Default Set Attributes + // |----------------------------------------------------------------- + // | + // | This config option allows you to define some attributes which + // | will be applied by default to all icons within this set. + // | + // */ + // + // 'attributes' => [ + // // 'width' => 50, + // // 'height' => 50, + // ], + // + // ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global Default Classes + |-------------------------------------------------------------------------- + | + | This config option allows you to define some classes which + | will be applied by default to all icons. + | + */ + + 'class' => '', + + /* + |-------------------------------------------------------------------------- + | Global Default Attributes + |-------------------------------------------------------------------------- + | + | This config option allows you to define some attributes which + | will be applied by default to all icons. + | + */ + + 'attributes' => [ + // 'width' => 50, + // 'height' => 50, + ], + + /* + |-------------------------------------------------------------------------- + | Global Fallback Icon + |-------------------------------------------------------------------------- + | + | This config option allows you to define a global fallback + | icon when an icon in any set cannot be found. It can + | reference any icon from any configured set. + | + */ + + 'fallback' => '', + + /* + |-------------------------------------------------------------------------- + | Components + |-------------------------------------------------------------------------- + | + | These config options allow you to define some + | settings related to Blade Components. + | + */ + + 'components' => [ + + /* + |---------------------------------------------------------------------- + | Disable Components + |---------------------------------------------------------------------- + | + | This config option allows you to disable Blade components + | completely. It's useful to avoid performance problems + | when working with large icon libraries. + | + */ + + 'disabled' => false, + + /* + |---------------------------------------------------------------------- + | Default Icon Component Name + |---------------------------------------------------------------------- + | + | This config option allows you to define the name + | for the default Icon class component. + | + */ + + 'default' => 'icon', + + ], + +]; diff --git a/config/octane.php b/config/octane.php index 8cfba01..8715c77 100644 --- a/config/octane.php +++ b/config/octane.php @@ -73,7 +73,7 @@ return [ RequestReceived::class => [ ...Octane::prepareApplicationForNextOperation(), ...Octane::prepareApplicationForNextRequest(), - // + \App\Listeners\ResetCurrentWorkspace::class, ], RequestHandled::class => [ @@ -82,6 +82,7 @@ return [ RequestTerminated::class => [ // FlushUploadedFiles::class, + \App\Listeners\ResetCurrentWorkspace::class, ], TaskReceived::class => [ diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 92f5d04..af989b7 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -53,5 +53,13 @@ document.addEventListener('alpine:init', () => { }, }); - Alpine.store('sidebar', { open: false }); + Alpine.store('sidebar', { + open: false, + collapsed: localStorage.getItem('sidebarCollapsed') === 'true', + toggle() { + this.collapsed = ! this.collapsed; + localStorage.setItem('sidebarCollapsed', this.collapsed); + window.dispatchEvent(new CustomEvent('sidebar-toggled', { detail: { collapsed: this.collapsed } })); + }, + }); }); diff --git a/resources/views/components/sidebar.blade.php b/resources/views/components/sidebar.blade.php index fa8e3d4..3a63e30 100644 --- a/resources/views/components/sidebar.blade.php +++ b/resources/views/components/sidebar.blade.php @@ -1,5 +1,5 @@ @php - $workspace = app()->bound('current_workspace') ? app('current_workspace') : null; + $workspace = current_workspace(); $workspaces = auth()->check() ? auth()->user()->workspaces()->orderBy('name')->get() : collect(); $wsUlid = $workspace?->ulid; @@ -68,6 +68,13 @@ 'match' => 'w.team.*', 'icon' => 'team', ], + [ + 'label' => 'AI', + 'route' => 'w.ai.insights', + 'params' => [$wsUlid], + 'match' => 'w.ai.*', + 'icon' => 'ai', + ], ] : [ [ 'label' => 'Dashboard', @@ -90,67 +97,78 @@ @endphp