Skip to content

Commit 9fc606a

Browse files
committed
menu tree item refactor to livewire component
1 parent a5cfad6 commit 9fc606a

File tree

6 files changed

+329
-130
lines changed

6 files changed

+329
-130
lines changed

resources/views/filament/resources/menu-resource/pages/manage-menu-items.blade.php

Lines changed: 29 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,38 @@
22
<div
33
x-data="menuItemsManager({
44
maxDepth: {{ $this->getMaxDepth() }},
5-
menuId: {{ $this->record->id }},
6-
items: @js($this->getMenuItems())
5+
menuId: {{ $this->record->id }}
76
})"
87
x-init="init()"
98
class="space-y-6"
109
>
1110
<!-- Tree Container -->
1211
<x-filament::section>
1312
<div class="min-h-[400px]">
14-
<!-- Empty State -->
15-
<div x-show="items.length === 0" class="text-center py-12">
16-
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
17-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
18-
</svg>
19-
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
20-
{{ flexiblePagesTrans('menu_items.tree.empty_state') }}
21-
</h3>
22-
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
23-
{{ flexiblePagesTrans('menu_items.manage.empty_description') }}
24-
</p>
25-
</div>
26-
27-
<!-- Tree Items -->
28-
<div x-show="items.length > 0" class="space-y-2" id="menu-items-container">
29-
<template x-for="(item, index) in items" :key="item.id">
30-
<div class="menu-tree-item" x-bind:data-item-id="item.id">
31-
<div x-html="renderTreeItem(item, 0)"></div>
32-
</div>
33-
</template>
34-
</div>
13+
@if(count($this->getMenuItems()) === 0)
14+
<!-- Empty State -->
15+
<div class="text-center py-12">
16+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
17+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
18+
</svg>
19+
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
20+
{{ flexiblePagesTrans('menu_items.tree.empty_state') }}
21+
</h3>
22+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
23+
{{ flexiblePagesTrans('menu_items.manage.empty_description') }}
24+
</p>
25+
</div>
26+
@else
27+
<!-- Tree Items -->
28+
<div class="space-y-2" id="menu-items-container">
29+
@foreach($this->getMenuItems() as $item)
30+
@livewire('filament-flexible-content-block-pages::menu-tree-item', [
31+
'item' => $item,
32+
'maxDepth' => $this->getMaxDepth()
33+
], key($item['id']))
34+
@endforeach
35+
</div>
36+
@endif
3537
</div>
3638
</x-filament::section>
3739

@@ -54,7 +56,6 @@ class="space-y-6"
5456
<script>
5557
function menuItemsManager(config) {
5658
return {
57-
items: config.items || [],
5859
maxDepth: config.maxDepth || 2,
5960
menuId: config.menuId,
6061
loading: false,
@@ -73,8 +74,8 @@ function menuItemsManager(config) {
7374
7475
refreshMenuItems() {
7576
this.loading = true;
76-
this.$wire.call('getMenuItems').then((items) => {
77-
this.items = items;
77+
// Refresh the page component - Livewire will handle the re-render
78+
this.$wire.$refresh().finally(() => {
7879
this.loading = false;
7980
// Re-initialize sortable after items are updated
8081
this.$nextTick(() => {
@@ -93,104 +94,14 @@ function menuItemsManager(config) {
9394
handle: '.drag-handle',
9495
onEnd: (evt) => {
9596
if (evt.oldIndex !== evt.newIndex) {
96-
this.reorderItems(evt.oldIndex, evt.newIndex);
97+
// TODO: Implement reordering with proper nested set handling
98+
console.log('Reorder from', evt.oldIndex, 'to', evt.newIndex);
9799
}
98100
}
99101
});
100102
}
101103
}
102104
},
103-
104-
renderTreeItem(item, depth) {
105-
const canHaveChildren = depth < this.maxDepth;
106-
const hasChildren = item.children && item.children.length > 0;
107-
const indentClass = depth > 0 ? `ml-${depth * 8}` : '';
108-
109-
return `
110-
<div class="flex items-center p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 group hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${indentClass}">
111-
<!-- Drag Handle -->
112-
<div class="drag-handle cursor-move text-gray-400 hover:text-gray-600 mr-4 opacity-0 group-hover:opacity-100 transition-opacity">
113-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"></path>
115-
</svg>
116-
</div>
117-
118-
<!-- Item Content -->
119-
<div class="flex-1 min-w-0">
120-
<div class="flex items-center justify-between">
121-
<div class="flex items-center space-x-3">
122-
${item.icon ? `<span class="text-gray-500 text-lg">${item.icon}</span>` : ''}
123-
<div>
124-
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
125-
${this.getItemDisplayLabel(item)}
126-
</p>
127-
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">
128-
${this.getItemTypeLabel(item)}
129-
</p>
130-
</div>
131-
${!item.is_visible ? '<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">{{ flexiblePagesTrans('menu_items.status.hidden') }}</span>' : ''}
132-
</div>
133-
134-
<!-- Actions -->
135-
<div class="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
136-
${canHaveChildren ? `
137-
<button onclick="$wire.mountAction('addMenuItem', { parent_id: ${item.id} })"
138-
class="inline-flex items-center px-2 py-1 border border-gray-300 rounded text-xs font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
139-
{{ flexiblePagesTrans('menu_items.tree.add_child') }}
140-
</button>
141-
` : ''}
142-
<button onclick="$wire.mountAction('editMenuItem', { itemId: ${item.id} })"
143-
class="inline-flex items-center px-2 py-1 border border-primary-300 rounded text-xs font-medium text-primary-700 bg-primary-50 hover:bg-primary-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
144-
{{ flexiblePagesTrans('menu_items.tree.edit') }}
145-
</button>
146-
<button onclick="$wire.mountAction('deleteMenuItem', { itemId: ${item.id} })"
147-
class="inline-flex items-center px-2 py-1 border border-red-300 rounded text-xs font-medium text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
148-
{{ flexiblePagesTrans('menu_items.tree.delete') }}
149-
</button>
150-
</div>
151-
</div>
152-
</div>
153-
</div>
154-
155-
${hasChildren ? `
156-
<div class="space-y-2 mt-2">
157-
${item.children.map(child => this.renderTreeItem(child, depth + 1)).join('')}
158-
</div>
159-
` : ''}
160-
`;
161-
},
162-
163-
getItemDisplayLabel(item) {
164-
if (item.use_model_title && item.linkable && item.linkable.title) {
165-
return item.linkable.title;
166-
}
167-
return item.label || '{{ flexiblePagesTrans('menu_items.status.no_label') }}';
168-
},
169-
170-
getItemTypeLabel(item) {
171-
if (item.linkable_type && item.linkable) {
172-
return `{{ flexiblePagesTrans('menu_items.tree.linked_to') }} ${item.linkable_type}`;
173-
}
174-
if (item.url) {
175-
return `{{ flexiblePagesTrans('menu_items.tree.external_url') }}: ${item.url}`;
176-
}
177-
return '{{ flexiblePagesTrans('menu_items.tree.no_link') }}';
178-
},
179-
180-
reorderItems(oldIndex, newIndex) {
181-
// Move item in array
182-
const item = this.items.splice(oldIndex, 1)[0];
183-
this.items.splice(newIndex, 0, item);
184-
185-
// Send new order to server
186-
const orderedIds = this.items.map(item => item.id);
187-
this.loading = true;
188-
this.$wire.call('reorderMenuItems', orderedIds).then(() => {
189-
this.loading = false;
190-
// Refresh items to get updated structure
191-
location.reload();
192-
});
193-
}
194105
}
195106
}
196107
</script>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<div class="menu-tree-item" data-item-id="{{ $item['id'] }}">
2+
<div class="flex items-center p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 group hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors {{ $depth > 0 ? 'ml-' . ($depth * 8) : '' }}">
3+
4+
@if($showActions)
5+
<!-- Drag Handle -->
6+
<div class="drag-handle cursor-move text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 mr-4 opacity-0 group-hover:opacity-100 transition-opacity">
7+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
8+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"></path>
9+
</svg>
10+
</div>
11+
@endif
12+
13+
<!-- Expand/Collapse Button for items with children -->
14+
@if($this->hasChildren())
15+
<button
16+
type="button"
17+
wire:click="toggleExpanded"
18+
class="mr-2 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
19+
>
20+
<svg class="w-4 h-4 transform transition-transform {{ $isExpanded ? 'rotate-90' : '' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
21+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
22+
</svg>
23+
</button>
24+
@else
25+
<div class="w-6 mr-2"></div>
26+
@endif
27+
28+
<!-- Item Content -->
29+
<div class="flex-1 min-w-0">
30+
<div class="flex items-center justify-between">
31+
<div class="flex items-center space-x-3">
32+
@if(!empty($item['icon']))
33+
<span class="text-gray-500 text-lg">{!! $item['icon'] !!}</span>
34+
@endif
35+
36+
<div>
37+
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
38+
{{ $this->getItemDisplayLabel() }}
39+
</p>
40+
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">
41+
{{ $this->getItemTypeLabel() }}
42+
</p>
43+
</div>
44+
45+
@if(!($item['is_visible'] ?? true))
46+
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
47+
{{ flexiblePagesTrans('menu_items.status.hidden') }}
48+
</span>
49+
@endif
50+
</div>
51+
52+
@if($showActions)
53+
<!-- Actions -->
54+
<div class="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
55+
@if($this->canHaveChildren())
56+
{{ ($this->addChildAction)(['color' => 'gray']) }}
57+
@endif
58+
59+
{{ ($this->editAction)(['color' => 'primary']) }}
60+
{{ ($this->deleteAction)(['color' => 'danger']) }}
61+
</div>
62+
@endif
63+
</div>
64+
</div>
65+
</div>
66+
67+
<!-- Children -->
68+
@if($this->hasChildren() && $isExpanded)
69+
<div class="space-y-2 mt-2" wire:key="children-{{ $item['id'] }}">
70+
@foreach($item['children'] as $child)
71+
@livewire('filament-flexible-content-block-pages::menu-tree-item', [
72+
'item' => $child,
73+
'depth' => $depth + 1,
74+
'maxDepth' => $maxDepth,
75+
'showActions' => $showActions
76+
], key($child['id']))
77+
@endforeach
78+
</div>
79+
@endif
80+
</div>

src/Filament/Form/Forms/MenuItemForm.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,20 @@ protected static function getLabelField(): TextInput
8585
{
8686
return TextInput::make(static::FIELD_LABEL)
8787
->label(flexiblePagesTrans('menu_items.form.label_lbl'))
88-
->required(fn (Get $get): bool => !$get(static::FIELD_USE_MODEL_TITLE))
89-
->visible(fn (Get $get): bool => !$get(static::FIELD_USE_MODEL_TITLE))
88+
->required(fn (Get $get): bool => ! $get(static::FIELD_USE_MODEL_TITLE))
89+
->visible(fn (Get $get): bool => ! $get(static::FIELD_USE_MODEL_TITLE))
9090
->maxLength(255)
91-
->helperText(flexiblePagesTrans('menu_items.form.label_help'));
91+
->helperText(flexiblePagesTrans('menu_items.form.label_help'))
92+
->live();
9293
}
9394

9495
protected static function getUseModelTitleField(): Toggle
9596
{
9697
return Toggle::make(static::FIELD_USE_MODEL_TITLE)
9798
->label(flexiblePagesTrans('menu_items.form.use_model_title_lbl'))
9899
->helperText(flexiblePagesTrans('menu_items.form.use_model_title_help'))
99-
->visible(fn (Get $get): bool => static::isModelType($get(static::FIELD_LINK_TYPE)));
100+
->visible(fn (Get $get): bool => static::isModelType($get(static::FIELD_LINK_TYPE)))
101+
->live();
100102
}
101103

102104
protected static function getLinkableField(): Select

src/FilamentFlexibleContentBlockPagesServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
namespace Statikbe\FilamentFlexibleContentBlockPages;
44

55
use Illuminate\Database\Eloquent\Relations\Relation;
6+
use Livewire\Livewire;
67
use Spatie\LaravelPackageTools\Package;
78
use Spatie\LaravelPackageTools\PackageServiceProvider;
89
use Statikbe\FilamentFlexibleContentBlockPages\Commands\SeedDefaultsCommand;
910
use Statikbe\FilamentFlexibleContentBlockPages\Components\BaseLayout;
1011
use Statikbe\FilamentFlexibleContentBlockPages\Components\LanguageSwitch;
1112
use Statikbe\FilamentFlexibleContentBlockPages\Components\Menu;
1213
use Statikbe\FilamentFlexibleContentBlockPages\Components\MenuItem;
14+
use Statikbe\FilamentFlexibleContentBlockPages\Livewire\MenuTreeItem;
1315

1416
class FilamentFlexibleContentBlockPagesServiceProvider extends PackageServiceProvider
1517
{
@@ -46,5 +48,8 @@ public function packageBooted()
4648
{
4749
// add morph map
4850
Relation::morphMap(\Statikbe\FilamentFlexibleContentBlockPages\Facades\FilamentFlexibleContentBlockPages::config()->getMorphMap());
51+
52+
// Register Livewire components
53+
Livewire::component('filament-flexible-content-block-pages::menu-tree-item', MenuTreeItem::class);
4954
}
5055
}

0 commit comments

Comments
 (0)