Skip to content

Commit da433cf

Browse files
authored
Merge pull request #3499 from codeeu/dev
resource menu
2 parents ec8e16b + b57e325 commit da433cf

13 files changed

Lines changed: 755 additions & 27 deletions

app/Models/MenuItem.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
use Illuminate\Support\Facades\Cache;
8+
use Illuminate\Support\Facades\Route;
9+
use Spatie\EloquentSortable\Sortable;
10+
use Spatie\EloquentSortable\SortableTrait;
11+
12+
class MenuItem extends Model implements Sortable
13+
{
14+
use SortableTrait;
15+
16+
protected $table = 'menu_items';
17+
18+
protected $fillable = [
19+
'menu_section_id',
20+
'label',
21+
'label_key',
22+
'label_overrides',
23+
'url',
24+
'route_name',
25+
'route_params',
26+
'open_in_new_tab',
27+
'is_active',
28+
'sort_order',
29+
];
30+
31+
protected $casts = [
32+
'label_overrides' => 'array',
33+
'route_params' => 'array',
34+
'open_in_new_tab' => 'bool',
35+
'is_active' => 'bool',
36+
'sort_order' => 'int',
37+
];
38+
39+
public $sortable = [
40+
'order_column_name' => 'sort_order',
41+
'sort_when_creating' => true,
42+
'sort_on_has_many' => true,
43+
];
44+
45+
public function section(): BelongsTo
46+
{
47+
return $this->belongsTo(MenuSection::class, 'menu_section_id');
48+
}
49+
50+
public function resolvedLabel(?string $locale = null): string
51+
{
52+
$locale = $locale ?: app()->getLocale();
53+
54+
$override = $this->label_overrides[$locale] ?? null;
55+
if (is_string($override) && trim($override) !== '') {
56+
return $override;
57+
}
58+
59+
if (is_string($this->label_key) && $this->label_key !== '') {
60+
return __($this->label_key);
61+
}
62+
63+
return (string) ($this->label ?? '');
64+
}
65+
66+
public function resolvedHref(): ?string
67+
{
68+
if (is_string($this->route_name) && $this->route_name !== '') {
69+
$params = is_array($this->route_params) ? $this->route_params : [];
70+
71+
if (Route::has($this->route_name)) {
72+
return route($this->route_name, $params);
73+
}
74+
}
75+
76+
if (is_string($this->url) && $this->url !== '') {
77+
return $this->url;
78+
}
79+
80+
return null;
81+
}
82+
83+
protected static function booted(): void
84+
{
85+
static::saved(function (self $item) {
86+
$location = $item->section?->location;
87+
if ($location) {
88+
Cache::forget("menus.location.{$location}.v1");
89+
}
90+
});
91+
92+
static::deleted(function (self $item) {
93+
$location = $item->section?->location;
94+
if ($location) {
95+
Cache::forget("menus.location.{$location}.v1");
96+
}
97+
});
98+
}
99+
}
100+

app/Models/MenuSection.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\HasMany;
7+
use Illuminate\Support\Facades\Cache;
8+
use Spatie\EloquentSortable\Sortable;
9+
use Spatie\EloquentSortable\SortableTrait;
10+
11+
class MenuSection extends Model implements Sortable
12+
{
13+
use SortableTrait;
14+
15+
protected $table = 'menu_sections';
16+
17+
protected $fillable = [
18+
'location',
19+
'column',
20+
'title',
21+
'title_key',
22+
'sort_order',
23+
'is_active',
24+
];
25+
26+
protected $casts = [
27+
'is_active' => 'bool',
28+
'sort_order' => 'int',
29+
];
30+
31+
public $sortable = [
32+
'order_column_name' => 'sort_order',
33+
'sort_when_creating' => true,
34+
];
35+
36+
public function items(): HasMany
37+
{
38+
return $this->hasMany(MenuItem::class, 'menu_section_id');
39+
}
40+
41+
protected static function booted(): void
42+
{
43+
static::saved(function (self $section) {
44+
Cache::forget("menus.location.{$section->location}.v1");
45+
});
46+
47+
static::deleted(function (self $section) {
48+
Cache::forget("menus.location.{$section->location}.v1");
49+
});
50+
}
51+
}
52+

app/Nova/MenuItem.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace App\Nova;
4+
5+
use App\Models\MenuItem as MenuItemModel;
6+
use Illuminate\Http\Request;
7+
use Laravel\Nova\Fields\BelongsTo;
8+
use Laravel\Nova\Fields\Boolean;
9+
use Laravel\Nova\Fields\Code;
10+
use Laravel\Nova\Fields\ID;
11+
use Laravel\Nova\Fields\KeyValue;
12+
use Laravel\Nova\Fields\Text;
13+
use Laravel\Nova\Http\Requests\NovaRequest;
14+
use Outl1ne\NovaSortable\Traits\HasSortableRows;
15+
16+
class MenuItem extends Resource
17+
{
18+
use HasSortableRows;
19+
20+
public static $model = MenuItemModel::class;
21+
22+
public static $title = 'id';
23+
24+
public static $search = [
25+
'id',
26+
'label',
27+
'label_key',
28+
'url',
29+
'route_name',
30+
];
31+
32+
public static $displayInNavigation = false;
33+
34+
public function title(): string
35+
{
36+
$label = $this->resource?->resolvedLabel() ?: "Item #{$this->id}";
37+
$href = $this->resource?->resolvedHref();
38+
$hrefPart = $href ? " · {$href}" : '';
39+
40+
return $label.$hrefPart;
41+
}
42+
43+
public function fields(Request $request): array
44+
{
45+
return [
46+
ID::make()->sortable(),
47+
48+
BelongsTo::make('Section', 'section', MenuSection::class)
49+
->sortable()
50+
->rules('required'),
51+
52+
Text::make('Label Key', 'label_key')
53+
->nullable()
54+
->rules('nullable', 'max:255')
55+
->help('Preferred. Example: menu.webinars'),
56+
57+
Text::make('Label (literal)', 'label')
58+
->nullable()
59+
->rules('nullable', 'max:255')
60+
->help('Used only if Label Key is empty (and no locale override exists).'),
61+
62+
KeyValue::make('Label Overrides', 'label_overrides')
63+
->keyLabel('Locale')
64+
->valueLabel('Label')
65+
->nullable()
66+
->rules('nullable')
67+
->help('Optional per-locale labels. Example key: en, fr, de'),
68+
69+
Text::make('Route Name', 'route_name')
70+
->nullable()
71+
->rules('nullable', 'max:255')
72+
->help('Preferred for internal links, e.g. educational-resources'),
73+
74+
Code::make('Route Params', 'route_params')
75+
->json()
76+
->nullable()
77+
->rules('nullable')
78+
->help('Optional JSON object, e.g. {"slug":"xyz"}'),
79+
80+
Text::make('URL', 'url')
81+
->nullable()
82+
->rules('nullable', 'max:2048')
83+
->help('Use for external links or hardcoded internal paths like /podcasts'),
84+
85+
Boolean::make('Open in new tab', 'open_in_new_tab')->default(false),
86+
Boolean::make('Active', 'is_active')->default(true),
87+
88+
Text::make('Preview', function () {
89+
$label = $this->resource?->resolvedLabel() ?: '';
90+
$href = $this->resource?->resolvedHref() ?: '';
91+
return trim($label.' '.$href);
92+
})->onlyOnDetail(),
93+
];
94+
}
95+
96+
public static function indexQuery(NovaRequest $request, $query)
97+
{
98+
return $query->orderBy('sort_order')->orderBy('id');
99+
}
100+
}
101+

app/Nova/MenuSection.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace App\Nova;
4+
5+
use App\Models\MenuSection as MenuSectionModel;
6+
use Illuminate\Http\Request;
7+
use Laravel\Nova\Fields\Boolean;
8+
use Laravel\Nova\Fields\HasMany;
9+
use Laravel\Nova\Fields\ID;
10+
use Laravel\Nova\Fields\Select;
11+
use Laravel\Nova\Fields\Text;
12+
use Laravel\Nova\Http\Requests\NovaRequest;
13+
use Outl1ne\NovaSortable\Traits\HasSortableRows;
14+
15+
class MenuSection extends Resource
16+
{
17+
use HasSortableRows;
18+
19+
public static $group = 'Site';
20+
21+
public static $model = MenuSectionModel::class;
22+
23+
public static $title = 'id';
24+
25+
public static $search = [
26+
'id',
27+
'location',
28+
'column',
29+
'title',
30+
'title_key',
31+
];
32+
33+
public static function label()
34+
{
35+
return 'Menu Sections';
36+
}
37+
38+
public static function singularLabel()
39+
{
40+
return 'Menu Section';
41+
}
42+
43+
public function title(): string
44+
{
45+
$location = (string) ($this->location ?? '');
46+
$column = (string) ($this->column ?? '');
47+
$title = $this->title_key ? __((string) $this->title_key) : (string) ($this->title ?? '');
48+
$title = trim($title) !== '' ? $title : "Section #{$this->id}";
49+
50+
return trim("{$location} · {$column} · {$title}");
51+
}
52+
53+
public function fields(Request $request): array
54+
{
55+
return [
56+
ID::make()->sortable(),
57+
58+
Text::make('Location', 'location')
59+
->rules('required', 'max:255')
60+
->help('Example: resources_dropdown'),
61+
62+
Select::make('Column', 'column')
63+
->options([
64+
'left' => 'Left',
65+
'right' => 'Right',
66+
])
67+
->displayUsingLabels()
68+
->rules('required'),
69+
70+
Text::make('Title Key', 'title_key')
71+
->nullable()
72+
->rules('nullable', 'max:255')
73+
->help('Preferred. Example: menu.learn_to_code'),
74+
75+
Text::make('Title (literal)', 'title')
76+
->nullable()
77+
->rules('nullable', 'max:255')
78+
->help('Used only if Title Key is empty.'),
79+
80+
Boolean::make('Active', 'is_active')->default(true),
81+
82+
HasMany::make('Items', 'items', MenuItem::class),
83+
];
84+
}
85+
86+
public static function indexQuery(NovaRequest $request, $query)
87+
{
88+
return $query->orderBy('sort_order')->orderBy('id');
89+
}
90+
}
91+

app/Providers/AppServiceProvider.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use App\Services\Support\Gmail\GmailConnector;
2121
use App\Services\Support\Gmail\GoogleGmailConnector;
2222
use App\Services\Support\Gmail\NullGmailConnector;
23+
use App\Services\Menu\MenuRepository;
2324

2425
class AppServiceProvider extends ServiceProvider
2526
{
@@ -42,6 +43,12 @@ public function boot(): void
4243
View::share('locales', config('app.locales'));
4344

4445
Carbon::setLocale('app.locale');
46+
47+
View::composer('layout.menu', function ($view) {
48+
$menuRepository = app(MenuRepository::class);
49+
$view->with('resourcesDropdownSections', $menuRepository->sectionsForLocation('resources_dropdown'));
50+
});
51+
4552
\View::composer(
4653
['event.add', 'event.search', 'event.edit'],
4754
function ($view) {

app/Providers/NovaServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use App\Nova\SupportCaseMessage as SupportCaseMessageNova;
1515
use App\Nova\SupportGmailCursor as SupportGmailCursorNova;
1616
use App\Nova\TrainingResource as TrainingResourceNova;
17+
use App\Nova\MenuSection as MenuSectionNova;
1718
use App\Nova\Metrics\UsersPerDay;
1819
use Illuminate\Support\Facades\Gate;
1920
use Laravel\Nova\Menu\MenuSection;
@@ -39,6 +40,7 @@ public function boot(): void
3940
SupportApprovalNova::class,
4041
SupportCaseMessageNova::class,
4142
SupportGmailCursorNova::class,
43+
MenuSectionNova::class,
4244
]);
4345

4446
// Ensure dashboards are registered at boot so /nova/dashboards/main is always available

0 commit comments

Comments
 (0)