This commit is contained in:
Mark Gregory
2026-07-24 11:02:38 +01:00
parent 3f87bea5f9
commit 9825a0ab0e
23 changed files with 103 additions and 6 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
<?php
namespace Modules\Blog\Models;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag;
final class Blog extends Model { protected $table='blog_posts'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'blog_post_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow;
final class Blog extends Model { use HasWorkflow; protected $table='blog_posts'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'blog_post_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
+2 -2
View File
@@ -1,4 +1,4 @@
<?php
namespace Modules\News\Models;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag;
final class News extends Model { protected $table='news_articles'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'news_article_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow;
final class News extends Model { use HasWorkflow; protected $table='news_articles'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'boolean','published_at'=>'datetime','expires_at'=>'datetime']; } public function author(): BelongsTo { return $this->belongsTo(Author::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class, 'news_article_tag'); } public function scopePublished(Builder $query): Builder { return $query->where('status','published')->where(fn(Builder $q)=>$q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn(Builder $q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now())); } }
+2 -1
View File
@@ -5,10 +5,11 @@ namespace Modules\Pages\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Modules\Workflow\Concerns\HasWorkflow;
final class Page extends Model
{
use HasFactory, SoftDeletes;
use HasFactory, HasWorkflow, SoftDeletes;
protected $table = 'pages';
protected $fillable = ['title', 'slug', 'summary', 'content', 'status', 'published_at', 'is_home'];
@@ -50,6 +50,9 @@ final class RolePermissionSeeder extends Seeder
'events.manage',
'careers.manage',
'locations.manage',
'workflow.view',
'workflow.manage',
'workflow.review',
];
foreach ($permissions as $permission) {
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Concerns;
use Illuminate\Database\Eloquent\Relations\MorphOne; use Modules\Workflow\Models\WorkflowInstance;
trait HasWorkflow { public function workflowInstance(): MorphOne { return $this->morphOne(WorkflowInstance::class,'subject'); } public function workflowLabel(): string { return (string) ($this->title ?? $this->name ?? class_basename($this).' #'.$this->getKey()); } }
+2
View File
@@ -0,0 +1,2 @@
<?php
return ['name' => 'Workflow', 'default_due_days' => 3];
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Contracts;
use Illuminate\Database\Eloquent\Relations\MorphOne;
interface Workflowable { public function workflowInstance(): MorphOne; public function workflowLabel(): string; }
@@ -0,0 +1,14 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('workflow_definitions', function(Blueprint $t){ $t->id(); $t->string('name'); $t->string('slug')->unique(); $t->text('description')->nullable(); $t->string('subject_type')->nullable()->index(); $t->boolean('is_active')->default(true); $t->timestamps(); });
Schema::create('workflow_stages', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); $t->string('name'); $t->string('slug'); $t->unsignedInteger('position')->default(0); $t->string('required_permission')->nullable(); $t->boolean('is_initial')->default(false); $t->boolean('is_terminal')->default(false); $t->boolean('allows_rejection')->default(true); $t->unsignedInteger('due_days')->nullable(); $t->timestamps(); $t->unique(['workflow_definition_id','slug']); });
Schema::create('workflow_transitions', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); $t->foreignId('from_stage_id')->constrained('workflow_stages')->cascadeOnDelete(); $t->foreignId('to_stage_id')->constrained('workflow_stages')->cascadeOnDelete(); $t->string('name'); $t->string('action'); $t->string('required_permission')->nullable(); $t->boolean('requires_comment')->default(false); $t->timestamps(); });
Schema::create('workflow_instances', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); $t->foreignId('workflow_stage_id')->constrained('workflow_stages'); $t->string('subject_type'); $t->unsignedBigInteger('subject_id'); $t->string('status')->default('active'); $t->foreignId('started_by')->nullable()->constrained('users')->nullOnDelete(); $t->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete(); $t->timestamp('due_at')->nullable(); $t->timestamp('completed_at')->nullable(); $t->timestamps(); $t->unique(['subject_type','subject_id']); $t->index(['status','workflow_stage_id']); });
Schema::create('workflow_history', function(Blueprint $t){ $t->id(); $t->foreignId('workflow_instance_id')->constrained()->cascadeOnDelete(); $t->foreignId('from_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete(); $t->foreignId('to_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete(); $t->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); $t->string('action'); $t->text('comment')->nullable(); $t->json('metadata')->nullable(); $t->timestamps(); });
}
public function down(): void { Schema::dropIfExists('workflow_history'); Schema::dropIfExists('workflow_instances'); Schema::dropIfExists('workflow_transitions'); Schema::dropIfExists('workflow_stages'); Schema::dropIfExists('workflow_definitions'); }
};
@@ -0,0 +1,8 @@
<?php
namespace Modules\Workflow\Http\Controllers;
use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; use Illuminate\View\View; use Modules\Workflow\Models\WorkflowDefinition; use Modules\Workflow\Models\WorkflowInstance; use Modules\Workflow\Services\WorkflowManager;
final class WorkflowController extends Controller {
public function index(): View { abort_unless(auth()->user()->can('workflow.view'),403); return view('workflow::admin.index',['definitions'=>WorkflowDefinition::with('stages')->latest()->get(),'queue'=>WorkflowInstance::with(['definition','stage','subject'])->where('status','active')->latest()->paginate(20),'completed'=>WorkflowInstance::where('status','completed')->count()]); }
public function store(Request $request): RedirectResponse { abort_unless($request->user()->can('workflow.manage'),403); $data=$request->validate(['name'=>'required|string|max:120','description'=>'nullable|string','subject_type'=>'nullable|string|max:255']); $definition=WorkflowDefinition::create($data+['slug'=>Str::slug($data['name']),'is_active'=>true]); $draft=$definition->stages()->create(['name'=>'Draft','slug'=>'draft','position'=>0,'is_initial'=>true,'due_days'=>null]); $review=$definition->stages()->create(['name'=>'Review','slug'=>'review','position'=>10,'required_permission'=>'workflow.review','due_days'=>3]); $published=$definition->stages()->create(['name'=>'Approved','slug'=>'approved','position'=>20,'is_terminal'=>true,'allows_rejection'=>false]); $definition->transitions()->createMany([['from_stage_id'=>$draft->id,'to_stage_id'=>$review->id,'name'=>'Submit for review','action'=>'submit'],['from_stage_id'=>$review->id,'to_stage_id'=>$published->id,'name'=>'Approve','action'=>'approve','required_permission'=>'workflow.review'],['from_stage_id'=>$review->id,'to_stage_id'=>$draft->id,'name'=>'Request changes','action'=>'reject','required_permission'=>'workflow.review','requires_comment'=>true]]); return back()->with('success','Workflow created.'); }
public function transition(Request $request, WorkflowInstance $instance, WorkflowManager $manager): RedirectResponse { abort_unless($request->user()->can('workflow.review'),403); $data=$request->validate(['action'=>'required|string','comment'=>'nullable|string|max:2000']); $manager->transition($instance,$data['action'],$request->user(),$data['comment']??null); return back()->with('success','Workflow updated.'); }
}
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany;
final class WorkflowDefinition extends Model { protected $fillable=['name','slug','description','subject_type','is_active']; protected function casts(): array { return ['is_active'=>'boolean']; } public function stages(): HasMany { return $this->hasMany(WorkflowStage::class)->orderBy('position'); } public function transitions(): HasMany { return $this->hasMany(WorkflowTransition::class); } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model;
final class WorkflowHistory extends Model { protected $table='workflow_history'; protected $fillable=['workflow_instance_id','from_stage_id','to_stage_id','user_id','action','comment','metadata']; protected function casts(): array { return ['metadata'=>'array']; } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo;
final class WorkflowInstance extends Model { protected $fillable=['workflow_definition_id','workflow_stage_id','subject_type','subject_id','status','started_by','assigned_to','due_at','completed_at']; protected function casts(): array { return ['due_at'=>'datetime','completed_at'=>'datetime']; } public function definition(): BelongsTo { return $this->belongsTo(WorkflowDefinition::class,'workflow_definition_id'); } public function stage(): BelongsTo { return $this->belongsTo(WorkflowStage::class,'workflow_stage_id'); } public function subject(): MorphTo { return $this->morphTo(); } public function history(): HasMany { return $this->hasMany(WorkflowHistory::class)->latest(); } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class WorkflowStage extends Model { protected $fillable=['workflow_definition_id','name','slug','position','required_permission','is_initial','is_terminal','allows_rejection','due_days']; protected function casts(): array { return ['is_initial'=>'boolean','is_terminal'=>'boolean','allows_rejection'=>'boolean']; } public function definition(): BelongsTo { return $this->belongsTo(WorkflowDefinition::class,'workflow_definition_id'); } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Models;
use Illuminate\Database\Eloquent\Model;
final class WorkflowTransition extends Model { protected $fillable=['workflow_definition_id','from_stage_id','to_stage_id','name','action','required_permission','requires_comment']; protected function casts(): array { return ['requires_comment'=>'boolean']; } }
@@ -0,0 +1,5 @@
<?php
namespace Modules\Workflow\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider { public function boot(): void { parent::boot(); Route::middleware('web')->group(module_path('Workflow', 'Routes/web.php')); } }
@@ -0,0 +1,8 @@
<?php
namespace Modules\Workflow\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Workflow\Services\WorkflowManager;
final class WorkflowServiceProvider extends ServiceProvider {
public function register(): void { $this->mergeConfigFrom(module_path('Workflow','Config/config.php'),'workflow'); $this->app->singleton(WorkflowManager::class); $this->app->register(RouteServiceProvider::class); }
public function boot(): void { $this->loadMigrationsFrom(module_path('Workflow','Database/Migrations')); $this->loadViewsFrom(module_path('Workflow','Resources/views'),'workflow'); }
}
@@ -0,0 +1 @@
<x-layouts.app :title="__('Workflows')"><main class="p-6 space-y-6"><div><flux:heading size="xl">Workflow</flux:heading><flux:text>Approval definitions, active reviews and completed work.</flux:text></div><div class="grid gap-4 md:grid-cols-3"><div class="rounded-xl border p-4"><flux:text>Definitions</flux:text><flux:heading size="xl">{{ $definitions->count() }}</flux:heading></div><div class="rounded-xl border p-4"><flux:text>Awaiting action</flux:text><flux:heading size="xl">{{ $queue->total() }}</flux:heading></div><div class="rounded-xl border p-4"><flux:text>Completed</flux:text><flux:heading size="xl">{{ $completed }}</flux:heading></div></div><div class="grid gap-6 xl:grid-cols-3"><section class="xl:col-span-2 rounded-xl border p-5"><flux:heading>Review queue</flux:heading><div class="mt-4 space-y-3">@forelse($queue as $item)<div class="rounded-lg border p-4"><div class="flex items-start justify-between gap-4"><div><div class="font-semibold">{{ method_exists($item->subject,'workflowLabel') ? $item->subject->workflowLabel() : class_basename($item->subject_type).' #'.$item->subject_id }}</div><div class="text-sm text-zinc-500">{{ $item->definition->name }} · {{ $item->stage->name }} @if($item->due_at) · due {{ $item->due_at->diffForHumans() }} @endif</div></div><div class="flex gap-2"><form method="POST" action="{{ route('administration.workflows.transition',$item) }}">@csrf<input type="hidden" name="action" value="approve"><flux:button type="submit" size="sm" variant="primary">Approve</flux:button></form><form method="POST" action="{{ route('administration.workflows.transition',$item) }}" class="flex gap-2">@csrf<input type="hidden" name="action" value="reject"><input name="comment" required placeholder="Reason" class="rounded-md border px-2 text-sm"><flux:button type="submit" size="sm">Changes</flux:button></form></div></div></div>@empty<flux:text>No items are awaiting review.</flux:text>@endforelse</div><div class="mt-4">{{ $queue->links() }}</div></section><section class="rounded-xl border p-5"><flux:heading>Create workflow</flux:heading><form method="POST" action="{{ route('administration.workflows.store') }}" class="mt-4 space-y-4">@csrf<flux:input name="name" label="Name" required/><flux:input name="subject_type" label="Subject model (optional)" placeholder="Modules\Pages\Models\Page"/><flux:textarea name="description" label="Description"/><flux:button type="submit" variant="primary">Create</flux:button></form><div class="mt-6 space-y-2">@foreach($definitions as $definition)<div class="rounded-lg border p-3"><div class="font-medium">{{ $definition->name }}</div><div class="text-xs text-zinc-500">{{ $definition->stages->pluck('name')->join(' → ') }}</div></div>@endforeach</div></section></div></main></x-layouts.app>
+3
View File
@@ -0,0 +1,3 @@
<?php
use Illuminate\Support\Facades\Route; use Modules\Workflow\Http\Controllers\WorkflowController;
Route::middleware(['auth','verified'])->prefix('administration/workflows')->name('administration.workflows.')->group(function(){ Route::get('/',[WorkflowController::class,'index'])->name('index'); Route::post('/',[WorkflowController::class,'store'])->name('store'); Route::post('/{instance}/transition',[WorkflowController::class,'transition'])->name('transition'); });
@@ -0,0 +1,7 @@
<?php
namespace Modules\Workflow\Services;
use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; use Modules\Workflow\Models\WorkflowDefinition; use Modules\Workflow\Models\WorkflowHistory; use Modules\Workflow\Models\WorkflowInstance; use Modules\Workflow\Models\WorkflowTransition;
final class WorkflowManager {
public function start(Model $subject, WorkflowDefinition $definition, ?Authenticatable $user=null): WorkflowInstance { return DB::transaction(function() use($subject,$definition,$user){ $stage=$definition->stages()->where('is_initial',true)->first() ?? $definition->stages()->firstOrFail(); $instance=WorkflowInstance::updateOrCreate(['subject_type'=>$subject->getMorphClass(),'subject_id'=>$subject->getKey()],['workflow_definition_id'=>$definition->getKey(),'workflow_stage_id'=>$stage->getKey(),'status'=>$stage->is_terminal?'completed':'active','started_by'=>$user?->getAuthIdentifier(),'due_at'=>$stage->due_days?now()->addDays($stage->due_days):null,'completed_at'=>$stage->is_terminal?now():null]); WorkflowHistory::create(['workflow_instance_id'=>$instance->id,'to_stage_id'=>$stage->id,'user_id'=>$user?->getAuthIdentifier(),'action'=>'started']); return $instance->fresh(['definition','stage']); }); }
public function transition(WorkflowInstance $instance, string $action, ?Authenticatable $user=null, ?string $comment=null): WorkflowInstance { return DB::transaction(function() use($instance,$action,$user,$comment){ $transition=WorkflowTransition::where('workflow_definition_id',$instance->workflow_definition_id)->where('from_stage_id',$instance->workflow_stage_id)->where('action',$action)->first(); if(!$transition) throw ValidationException::withMessages(['action'=>'This transition is not available.']); if($transition->requires_comment && blank($comment)) throw ValidationException::withMessages(['comment'=>'A comment is required.']); if($transition->required_permission && (!$user || !method_exists($user,'can') || !$user->can($transition->required_permission))) abort(403); $target=\Modules\Workflow\Models\WorkflowStage::findOrFail($transition->to_stage_id); $from=$instance->workflow_stage_id; $instance->update(['workflow_stage_id'=>$target->id,'status'=>$target->is_terminal?'completed':'active','due_at'=>$target->due_days?now()->addDays($target->due_days):null,'completed_at'=>$target->is_terminal?now():null]); WorkflowHistory::create(['workflow_instance_id'=>$instance->id,'from_stage_id'=>$from,'to_stage_id'=>$target->id,'user_id'=>$user?->getAuthIdentifier(),'action'=>$action,'comment'=>$comment]); return $instance->fresh(['definition','stage','subject']); }); }
}
@@ -0,0 +1,4 @@
<?php
namespace Modules\Workflow\Tests\Feature;
use Tests\TestCase;
final class ModuleBootTest extends TestCase { public function test_workflow_module_is_registered(): void { $this->assertTrue(app()->providerIsLoaded(\Modules\Workflow\Providers\WorkflowServiceProvider::class)); } }
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Workflow",
"alias": "workflow",
"description": "Reusable approval workflows, review queues, transitions and audit history.",
"keywords": ["workflow", "approval", "review"],
"priority": 35,
"providers": [
"Modules\\Workflow\\Providers\\WorkflowServiceProvider"
],
"files": []
}
+2 -1
View File
@@ -30,5 +30,6 @@
"Newsletter": true,
"Events": true,
"Careers": true,
"Locations": true
"Locations": true,
"Workflow": true
}
@@ -32,6 +32,7 @@
<flux:sidebar.item icon="chat-bubble-left-right" :href="route('administration.testimonials.index')" :current="request()->routeIs('administration.testimonials.*')" wire:navigate>{{ __('Testimonials') }}</flux:sidebar.item>
</flux:sidebar.group>
<flux:sidebar.group :heading="__('Platform')" class="grid"><flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:sidebar.item><flux:sidebar.item icon="squares-2x2" :href="route('administration.dashboard')" :current="request()->routeIs('administration.dashboard')" wire:navigate>{{ __('Administration') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Workflow')" class="grid"><flux:sidebar.item icon="arrows-right-left" :href="route('administration.workflows.index')" :current="request()->routeIs('administration.workflows.*')" wire:navigate>{{ __('Review Queue') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Management')" class="grid"><flux:sidebar.item icon="users" :href="route('administration.users.index')" :current="request()->routeIs('administration.users.*')" wire:navigate>{{ __('Users') }}</flux:sidebar.item><flux:sidebar.item icon="shield-check" :href="route('administration.roles.index')" :current="request()->routeIs('administration.roles.*')" wire:navigate>{{ __('Roles & Permissions') }}</flux:sidebar.item><flux:sidebar.item icon="photo" :href="route('administration.media.index')" :current="request()->routeIs('administration.media.*')" wire:navigate>{{ __('Media') }}</flux:sidebar.item><flux:sidebar.item icon="magnifying-glass" :href="route('administration.search.index')" :current="request()->routeIs('administration.search.*')" wire:navigate>{{ __('Search') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Configuration')" class="grid"><flux:sidebar.item icon="globe-alt" :href="route('administration.seo.index')" :current="request()->routeIs('administration.seo.*')" wire:navigate>{{ __('SEO') }}</flux:sidebar.item><flux:sidebar.item icon="cog-6-tooth" :href="route('administration.settings.index')" :current="request()->routeIs('administration.settings.*')" wire:navigate>{{ __('Settings') }}</flux:sidebar.item><flux:sidebar.item icon="cube" :href="route('administration.modules.index')" :current="request()->routeIs('administration.modules.*')" wire:navigate>{{ __('Modules') }}</flux:sidebar.item><flux:sidebar.item icon="clipboard-document-list" :href="route('administration.audit.index')" :current="request()->routeIs('administration.audit.*')" wire:navigate>{{ __('Audit Log') }}</flux:sidebar.item></flux:sidebar.group>
</flux:sidebar.nav><flux:spacer /><x-stylist-theme-common::desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" /></flux:sidebar>