قبل كل شيء، أود شكر صديقي معاذ السوادي الذي علّمني هذه المعلومة بعد أن حاول فيها مرارًا حتى ضُبطت بهذا الشكل..

في البداية، من أجمل أدوات تسريع عمل لوحة التحكم في لارافل هي مكتبة filament php، فهي تشبه Nova ولكنها مبنية على TALL stack ومجانية، وإذا لم تكن قد جربتها فأدعوك لتجربتها بشدة..

ولكن هناك ميزة لتحسينها أكثر، وجعل التنقل بين عناصر التنقل navigation items دون إعادة تحميل كاملة للصفحة، عن طريق مكتبة hotwire، بحيث تبدو الانتقالات بين عناصر التنقل سلسلة وأشبه ما تكون بتطبيقات الصفحة الواحدة single page applications.

والخطوات كالتالي:

1- أضف @hotwired/turbo لاعتماديات package.json عن طريق الأمر

npm i -D @hotwired/turbo أو yarn add -D @hotwired/turbo إذا كنت تستخدم yarn.

2- إضافة الملف بهذا المسار resources/filament/filament-turbo.js ونكتب بداخله هذا السطر

import '../js/libs/turbo'

3- إضافة الملف بهذا المسار resources/js/libs/turbo.js ونكتب محتواه

import * as Turbo from '@hotwired/turbo'

//start Livewire turbolinks, source https://github.com/livewire/turbolinks/blob/master/src/index.js v0.1.4
//removes the need for a cdn link in app.blade.php
if (typeof window.Livewire === 'undefined') {
  throw 'Livewire Turbolinks Plugin: window.Livewire is undefined. Make sure @livewireScripts is placed above this script include'
}

let firstTime = true

function wireTurboAfterFirstVisit() {
  // We only want this handler to run AFTER the first load.
  if (firstTime) {
    firstTime = false

    return
  }

  window.Livewire.restart()

  window.Alpine &&
    window.Alpine.flushAndStopDeferringMutations &&
    window.Alpine.flushAndStopDeferringMutations()
}

function wireTurboBeforeCache() {
  document.querySelectorAll('[wire\\:id]').forEach(function (el) {
    const component = el.__livewire
    const dataObject = {
      fingerprint: component.fingerprint,
      serverMemo: component.serverMemo,
      effects: component.effects,
    }
    el.setAttribute('wire:initial-data', JSON.stringify(dataObject))
  })

  window.Alpine &&
    window.Alpine.deferMutations &&
    window.Alpine.deferMutations()
}

document.addEventListener('turbo:load', wireTurboAfterFirstVisit)
document.addEventListener('turbo:before-cache', wireTurboBeforeCache)

document.addEventListener('turbolinks:load', wireTurboAfterFirstVisit)
document.addEventListener('turbolinks:before-cache', wireTurboBeforeCache)

Livewire.hook('beforePushState', (state) => {
  if (!state.turbolinks) state.turbolinks = {}
})

Livewire.hook('beforeReplaceState', (state) => {
  if (!state.turbolinks) state.turbolinks = {}
})
//end Livewire turbolinks

//start turbo-laravel, source https://github.com/tonysm/turbo-laravel/blob/main/stubs/resources/js/libs/alpine.js v1.1.0
function initAlpineTurboPermanentFix() {
  document.addEventListener('turbo:before-render', () => {
    let permanents = document.querySelectorAll('[data-turbo-permanent]')
    let undos = Array.from(permanents).map((el) => {
      el._x_ignore = true
      return () => {
        delete el._x_ignore
      }
    })

    document.addEventListener('turbo:render', function handler() {
      while (undos.length) undos.shift()()
      document.removeEventListener('turbo:render', handler)
    })
  })
}

if (window.Alpine !== undefined) {
  initAlpineTurboPermanentFix()
}
//end turbo-laravel

export default Turbo

4- وفي ملف vite.config.js نضيف resources/filament/filament-turbo.js فتصير كالتالي:

import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'

export default defineConfig({
  plugins: [
    laravel({
      input: [
        'resources/css/app.css',
        'resources/js/app.js',
        'resources/filament/filament-turbo.js', // add this line
      ],
      refresh: true,
    }),
  ],
})

5- نضيف هذا الملف الذي سيجعل filament تقرأ منه وليس من الملف الافتراضي بحيث تتجاوزه override، والملف في هذا المسار resources/views/vendor/filament/components/layouts

ومحتواه كالتالي

@props([
    'title' => null,
])

<!DOCTYPE html>
<html
    lang="{{ str_replace('_', '-', app()->getLocale()) }}"
    dir="{{ __('filament::layout.direction') ?? 'ltr' }}"
    class="antialiased bg-gray-100 filament js-focus-visible"
>
<head>
    {{ \Filament\Facades\Filament::renderHook('head.start') }}

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    @foreach (\Filament\Facades\Filament::getMeta() as $tag)
        {{ $tag }}
    @endforeach

    @if ($favicon = config('filament.favicon'))
        <link rel="icon" href="{{ $favicon }}">
    @endif

    <title>{{ $title ? "{$title} - " : null }} {{ config('filament.brand') }}</title>

    {{ \Filament\Facades\Filament::renderHook('styles.start') }}

    <style>
        [x-cloak=""], [x-cloak="x-cloak"], [x-cloak="1"] { display: none !important; }
        @media (max-width: 1023px) { [x-cloak="-lg"] { display: none !important; } }
        @media (min-width: 1024px) { [x-cloak="lg"] { display: none !important; } }
        :root { --sidebar-width: {{ config('filament.layout.sidebar.width') ?? '20rem' }}; }
    </style>

    @livewireStyles

    @if (filled($fontsUrl = config('filament.google_fonts')))
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="{{ $fontsUrl }}" rel="stylesheet" />
    @endif

    {{ \Filament\Facades\Filament::getThemeLink() }}

    @foreach (\Filament\Facades\Filament::getStyles() as $name => $path)
        @if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
            <link rel="stylesheet" href="{{ $path }}" />
        @elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
            {!! $path !!}
        @else
            <link rel="stylesheet" href="{{ route('filament.asset', [
                    'file' => "{$name}.css",
                ]) }}" />
        @endif
    @endforeach

    <style>
        .turbo-progress-bar {
            height: 3px;
            background:  rgb(245 158 11/var(--tw-bg-opacity));
        }
    </style>

    {{ \Filament\Facades\Filament::renderHook('styles.end') }}

    @if (config('filament.dark_mode'))
        <script>
            const theme = localStorage.getItem('theme')

            if ((theme === 'dark') || (! theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
                document.documentElement.classList.add('dark')
            }
        </script>
    @endif

    {{-- Filament head.end position--}}
    {{-- Filament body script start --}}

    {{ \Filament\Facades\Filament::renderHook('scripts.start') }}

    @livewireScripts
    {{-- bokaMarknad specific --}}
    @vite(['resources/css/app.css', 'resources/js/app.js', 'resources/filament/filament-turbo.js'])
    {{-- <script src="{{ mix('js/manifest.js') }}" defer></script>
    <script src="{{ mix('js/vendor-turbo.js') }}" defer></script>
    <script src="{{ mix('js/filament-turbo.js') }}" defer></script> --}}
    {{-- end bokaMarknad specific --}}

    {{-- Filament body continued --}}
    <script>
        window.filamentData = @js(\Filament\Facades\Filament::getScriptData());
    </script>

    @foreach (\Filament\Facades\Filament::getBeforeCoreScripts() as $name => $path)
        @if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
            <script defer src="{{ $path }}"></script>
        @elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
            {!! $path !!}
        @else
            <script defer src="{{ route('filament.asset', [
                    'file' => "{$name}.js",
                ]) }}"></script>
        @endif
    @endforeach

    @stack('beforeCoreScripts')

    <script defer src="{{ route('filament.asset', [
            'id' => Filament\get_asset_id('app.js'),
            'file' => 'app.js',
        ]) }}"></script>

    @foreach (\Filament\Facades\Filament::getScripts() as $name => $path)
        @if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
            <script defer src="{{ $path }}"></script>
        @elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
            {!! $path !!}
        @else
            <script defer src="{{ route('filament.asset', [
                    'file' => "{$name}.js",
                ]) }}"></script>
        @endif
    @endforeach

    @stack('scripts')

    {{ \Filament\Facades\Filament::renderHook('scripts.end') }}

    {{ \Filament\Facades\Filament::renderHook('head.end') }}
</head>

<body @class([
        'bg-gray-100 text-gray-900 filament-body',
        'dark:text-gray-100 dark:bg-gray-900' => config('filament.dark_mode'),
    ])>
{{ \Filament\Facades\Filament::renderHook('body.start') }}

{{ $slot }}

{{ \Filament\Facades\Filament::renderHook('body.end') }}
</body>
</html>

وبهذا صار التنقل أسلسل وأجمل وأشبه ما يكون بتطبيقات الصفحة الواحدة..

أرجو أن تكونوا استفدتم من هذا الدرس، وأي تصويب فهو مرحب به..