Skip to content

Commit bf20d04

Browse files
add moox/tree feature searching, filter and language switcher
1 parent b4a8932 commit bf20d04

21 files changed

Lines changed: 1180 additions & 31 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
description: Filament-Resources an Moox Tree anbinden (Consumer außerhalb packages/tree)
3+
globs: "**/*TreeResource.php,**/TreeList*.php,**/TreeInspector*.php"
4+
alwaysApply: false
5+
---
6+
7+
# Moox Tree – Resource-Integration
8+
9+
Wenn eine Resource einen Baum-Index statt einer Tabelle nutzt.
10+
11+
## Zentrale Nutzung (keine Duplikation)
12+
13+
**Tree-Funktionen nicht in Consumer-Packages neu bauen** — kein eigenes Livewire für Baum-CRUD, keine kopierten Move/Reorder-Skripte, keine parallelen Action-Klassen.
14+
15+
Consumer liefert nur:
16+
17+
- `treeIndex(): TreeIndexConfiguration` mit `->forwardFromResource(static::class, useFilamentTableToolbar: true)` — **1:1** wie die Liste: Filament-`EmbeddedTable` (Tabs, Filter, Suche, Language-Switcher in `TOOLBAR_SEARCH_BEFORE`) + `applyFiltersToTableQuery()` / `applySearchToTableQuery()` auf der List-Page; kein separater Lang-Switcher in der Baum-Spalte
18+
- dünne Filament-Pages (`TreeIndexListRecords`, optional Inspector mit **Domänen-Formular**)
19+
- ggf. `modifyQuery` / `applySearchUsing` / `applyLanguageUsing` als **Closures** (nur Datenfilter, keine Tree-Mechanik)
20+
21+
Braucht eine Resource ein Verhalten, das andere Bäume auch brauchen könnten → **Feature ins `packages/tree`**, dann per Config aktivieren.
22+
23+
## Eloquent-Models
24+
25+
**Models nicht um Tree-Funktionen erweitern** — keine Hilfsmethoden, Interfaces oder Traits nur für das Tree-Package (z. B. `getTreeLabel()`, `toTreeNode()`, `implements TreeNodeContract`).
26+
27+
Erlaubt am Model:
28+
29+
- Spalten/`$fillable` für `parent_id`, Sortierung, Label (oder konfigurierte Namen)
30+
- Bei Nested Set: Kalnoy **`NodeTrait`** (Pflicht für `nestedSet()`, sonst nichts Tree-spezifisches)
31+
- Domänen-Relations/Accessors, die ohnehin zur App gehören; Label-Accessor → `->labelColumnQueryable(false)` in der Resource
32+
33+
Tree-Mechanik gehört ins **Tree-Package**; die Resource **verdrahtet** nur über `treeIndex()`. Domänen-Inhalte (Tabs, Relationen, Medien) nur im **Inspector** (Filament Edit-Page), nicht als eigene Baum-Logik.
34+
35+
## Pflichten
36+
37+
1. Resource: `implements Moox\Tree\Contracts\ConfiguresTreeIndex` + `treeIndex(): TreeIndexConfiguration`.
38+
2. List-Page: `extends Moox\Tree\Filament\Pages\TreeIndexListRecords` (registriert Config in `mount()`).
39+
3. Routing: `'index' => TreeListXxx::route('/')`. Bei Moox-Parent-Resources: `...Arr::except(parent::getPages(), ['index'])` beibehalten.
40+
4. `modifyQuery` muss dieselbe Sicht wie `Resource::getEloquentQuery()` abbilden (Policies, Soft-Deletes, Mandanten).
41+
42+
## Inspector (empfohlen)
43+
44+
- Eigene Edit-Page mit `RendersAsTreeIndexInspector` + Route `tree-inspector` (Policies/URLs).
45+
- In Config: `->inspectorPage(TreeInspectorXxx::class)`.
46+
- Ohne Inspector: nur eingebautes Minimalformular (Label + Parent).
47+
48+
## Nested Set & Label
49+
50+
- Accessor-Labels (z. B. `title` aus Translation): `->labelColumnQueryable(false)`.
51+
- Nested Set: `->nestedSet()->sortColumn('_lft')->reorderable(true)` und `kalnoy/nestedset` am Model.
52+
53+
## Toolbar (Suche / Sprache)
54+
55+
- Mit `useFilamentTableToolbar: true`: Suche, Filter und `localization::lang-selector` in der **Filament-Tabellen-Toolbar** (Render-Hook im Tree-Package); keine Tabellen-Sortierung („Sort by“), keine Zeilen-/Bulk-Actions — Reihenfolge nur im Baum (`sortColumn` / Drag).
56+
- Ohne Table-Toolbar: `toolbarSearch` / `toolbarLanguageSwitcher` in der Baum-Spalte; Resource liefert ggf. `applySearchUsing` / `applyLanguageUsing` — keine zweite Toolbar-Implementierung.
57+
58+
## Trait-Shortcut
59+
60+
`Moox\Tree\Filament\Concerns\ConfiguresTreeIndex`: `getTreeIndexListPage()` + optional `getAdditionalResourcePages()` für Inspector-Routen.
61+
62+
## Checkliste neue Resource
63+
64+
Model-Spalten (ohne Tree-Methoden) → `treeIndex()` → `TreeIndexListRecords` → Pages/Routes → optional Inspector → Feature-Test für Config oder Livewire.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
description: Architektur und Konventionen für packages/tree (Moox Tree / filament-tree-index)
3+
globs: packages/tree/**
4+
alwaysApply: false
5+
---
6+
7+
# Moox Tree Package
8+
9+
Internes Filament-Resource-Index-UI für hierarchische Eloquent-Modelle (Baum links, Inspector rechts). Namespace: `Moox\Tree`. Views/Config-Tag: `filament-tree-index`.
10+
11+
## Zentrale Tree-Funktionen (Single Source of Truth)
12+
13+
**Alle Baum-Funktionen liegen in `packages/tree`** und werden von Consumer-Resources nur **konfiguriert**, nicht neu implementiert:
14+
15+
| Bereich | Zentrale Klassen/Komponenten |
16+
|--------|------------------------------|
17+
| CRUD / Verschieben | `Actions/Tree/*` |
18+
| Baumstruktur | `Support/TreeStructure` |
19+
| UI / Interaktion | `Livewire/ResourceTreeIndex`, Blade-Views, Alpine `$store.filamentTreeIndex` |
20+
| Filament-Anbindung | `TreeIndexListRecords`, `ConfiguresTreeIndex`, Inspector-Trait |
21+
| Anpassung | `TreeIndexConfiguration` (Spalten, Modus, Hooks, Labels, Toolbar) |
22+
| Locale / Toolbar-i18n | `Support/TreeLocale`, `toolbarLocalizedTranslations()` |
23+
| List-Forwarding | `forwardFromResource()`, `Support/ResourceListForwarder` |
24+
25+
**Flexibel nutzbar** heißt: gleicher Code für jedes hierarchische Model — Unterschiede nur über `TreeIndexConfiguration::make()` (Spaltennamen, `nestedSet()`, `reorderable`, `inspectorPage`, Closures). Fehlt eine generische Fähigkeit (z. B. Toolbar-Suche), **im Package erweitern** und per Config/API freischalten — nicht in `category`, `menu-builder` o. Ä. duplizieren.
26+
27+
## Architektur (nicht verletzen)
28+
29+
- **Geschäftslogik in Actions**, nicht in Livewire oder Blade: `CreateTreeNodeAction`, `UpdateTreeNodeAction`, `MoveTreeNodeAction`, `DeleteTreeNodeAction`. Bei `nestedSet()` delegieren Create/Move an `*NestedSet*`-Actions (Kalnoy `NodeTrait`).
30+
- **`ResourceTreeIndex`** orchestriert nur (Auth, Query, Delegation an Actions, Events). Keine neuen CRUD-Pfade direkt im Component.
31+
- **Baumaufbau** nur über `TreeStructure` + `TreeIndexConfiguration`. Konfiguration ist **immutable** (Fluent API mit `cloneWith`); neue Optionen brauchen Unit-Tests in `TreeIndexConfigurationTest`.
32+
- **Registry**: List-Pages registrieren Config unter dem **Resource-Klassennamen** (`TreeIndexConfigurationRegistry`). Schlüssel nicht umbenennen ohne Migration aller Aufrufer.
33+
34+
## Zwei Baum-Modi
35+
36+
| Modus | Spalten | Konfiguration |
37+
|-------|---------|---------------|
38+
| Adjacency List (Default) | `parent_id`, `sort_order`, Label-Spalte | Standard-`make()` |
39+
| Nested Set | `_lft`, `_rgt`, optional `parent_id` | `->nestedSet()->sortColumn('_lft')`, Model mit `NodeTrait` |
40+
41+
Adjacency- und Nested-Set-Logik **nicht mischen** in einer Action.
42+
43+
## UI & Frontend
44+
45+
- Blade unter `resources/views`, Prefix `filament-tree-index::`.
46+
- **Filament-Komponenten** (`x-filament::*`) und `fi-*`-Klassen; Layout/Scroll in `resources/css/tree.css` (Klassen `fi-tree-*`). Kein separates Theme-CSS außerhalb des Packages für Tree-Layout.
47+
- Alpine-Baumzustand nur über **`$store.filamentTreeIndex`** (`scripts/alpine-tree-store.blade.php`). Keinen zweiten Store oder duplizierte Expand/Collapse-Logik in Partials.
48+
- Drag & Drop nur wenn `reorderable(true)`; Verschiebe-Validierung (nicht unter sich selbst / eigenes Kind) beibehalten.
49+
50+
## Eloquent-Models (Vertrag)
51+
52+
Das Package darf **keine eigenen Model-Methoden, Interfaces oder Contracts** voraussetzen. Nutzung nur über:
53+
54+
- **Spalten/Attribute**: `parent_id`, Sort-Spalte, Label-Spalte (konfigurierbar), bei Nested Set `_lft`/`_rgt`
55+
- **Eloquent-Standard**: `getKey()`, `getAttribute()`, `update()`, `delete()`, `setAttribute()`
56+
- **Nested Set**: ausschließlich Kalnoy `NodeTrait` (`appendToNode`, `beforeNode`, …) — kein zusätzliches Tree-Trait im Moox-Package
57+
58+
Neue Features im Package müssen über **Config/Actions/Resource-Hooks** lösbar sein, nicht über `if (method_exists($model, …))` oder Model-APIs.
59+
60+
## Package-Grenzen
61+
62+
- **Keine domänenspezifischen Daten** im Package (keine `Category`-, `Localization`- oder Mandanten-Queries). Domänenfilter nur über **konfigurierbare Closures** (`modifyQuery`, `applySearchUsing`, `applyLanguageUsing`).
63+
- **Generische Tree-UI und -Abläufe** (Toolbar, Reorder, Expand, Inspector-Einbettung, Validierung) gehören ins Package — auch wenn heute nur eine Resource sie nutzt.
64+
- Abhängigkeit `moox/core` ist erlaubt; weitere Moox-Packages nur wenn unvermeidbar und zyklusfrei.
65+
66+
## Code-Stil
67+
68+
- `declare(strict_types=1);` in jeder PHP-Datei.
69+
- PHP ^8.3, Laravel ^12, Filament ^4/5 wie im Host-Projekt, Livewire ^3/4.
70+
- Typisierte Closures für Query-Hooks: `fn (Builder $query): Builder`.
71+
72+
## Tests
73+
74+
- Tests nur unter `packages/tree/tests/` (Pest). Feature = Livewire `ResourceTreeIndex`; Unit = Config, `TreeStructure`, Registry.
75+
- Auth in Tests: `config(['filament-tree-index.authorization.enabled' => false])` und Registry-Register mit Test-Key.
76+
- Nach Verhalten ändern: `php artisan test --compact packages/tree/tests`
77+
78+
## Referenz-Integration (außerhalb des Packages)
79+
80+
Consumer-Resources (`implements ConfiguresTreeIndex`) gehören **nicht** in dieses Package — siehe Regel `moox-tree-integration`. Referenz: `CategoryTreeResource`, `TreeListCategories`, `TreeInspectorCategory`.

packages/category/src/Resources/CategoryResource/Pages/TreeListCategories.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
namespace Moox\Category\Resources\CategoryResource\Pages;
66

7+
use Moox\Category\Models\Category;
78
use Moox\Category\Resources\CategoryTreeResource;
9+
use Moox\Core\Traits\Tabs\HasListPageTabs;
810
use Moox\Tree\Filament\Pages\TreeIndexListRecords;
911

1012
class TreeListCategories extends TreeIndexListRecords
1113
{
14+
use HasListPageTabs;
15+
1216
protected static string $resource = CategoryTreeResource::class;
1317

1418
public function mount(): void
@@ -18,5 +22,22 @@ public function mount(): void
1822
}
1923

2024
parent::mount();
25+
26+
$this->mountTabsInListPage();
27+
}
28+
29+
public function getTabs(): array
30+
{
31+
return $this->getDynamicTabs('category.resources.category.tabs', Category::class);
32+
}
33+
34+
public function updatedActiveTab(): void
35+
{
36+
static::getResource()::setCurrentTab($this->activeTab);
37+
$this->tableFilters = null;
38+
$this->tableSortColumn = null;
39+
$this->tableSortDirection = null;
40+
$this->resetTable();
41+
$this->refreshTreeIndexConfiguration();
2142
}
2243
}

packages/category/src/Resources/CategoryTreeResource.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Moox\Category\Resources;
66

7-
use Illuminate\Database\Eloquent\Builder;
87
use Illuminate\Support\Arr;
98
use Moox\Category\Models\Category;
109
use Moox\Category\Resources\CategoryResource as MooxCategoryResource;
@@ -25,13 +24,13 @@ public static function getTreeIndexListPage(): string
2524
public static function treeIndex(): TreeIndexConfiguration
2625
{
2726
return TreeIndexConfiguration::make(Category::class)
27+
->forwardFromResource(static::class, useFilamentTableToolbar: true)
2828
->labelColumn('title')
2929
->labelColumnQueryable(false)
3030
->nestedSet()
3131
->sortColumn('_lft')
3232
->reorderable(true)
3333
->inspectorPage(TreeInspectorCategory::class)
34-
->modifyQuery(fn (Builder $query): Builder => static::getEloquentQuery())
3534
->labels(
3635
treeHeading: 'Kategorien',
3736
treeSubheading: 'Baum',

packages/tree/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,33 @@ Ohne `inspectorPage()` zeigt das Package rechts nur ein **Minimalformular** (Lab
218218
| `reorderable(true)` | `true` | Drag & Drop (Livewire `wire:sort`) |
219219
| `inspectorPage(EditPage::class)` | `null` | Volles Filament-Formular rechts |
220220
| `modifyQuery(Closure)` || z. B. Resource-Scopes, Soft-Deletes, Mandanten |
221+
| `toolbarSearch(true)` | `false` | Blendet ein Suchfeld im Tree-Header ein |
222+
| `toolbarLanguageSwitcher(true)` | `false` | Blendet den Language-Switcher im Tree-Header ein (falls `localization::lang-selector` vorhanden) |
223+
| `applySearchUsing(Closure)` || Eigene Suchlogik: `fn (Builder $query, string $search, TreeIndexConfiguration $config): Builder` |
224+
| `applyLanguageUsing(Closure)` || Eigene Sprachlogik: `fn (Builder $query, string $lang, TreeIndexConfiguration $config): Builder` |
221225
| `authorizationAbility('update')` | `null` | Gate-Ability für das Model; ohne Wert nur `auth()->check()` |
222226
| `labels(...)` | deutsche Standardtexte | UI-Texte (siehe unten) |
223227

224228
Parameter von `labels()`: `treeHeading`, `treeSubheading`, `inspectorHeading`, `createRootLabel`, `createChildLabel`, `saveLabel`, `newRecordLabel`, `deleteConfirmMessage`.
225229

226230
`modifyQuery` sollte dieselbe Query-Logik wie `Resource::getEloquentQuery()` widerspiegeln (Policies, globale Scopes, Mandanten).
227231

232+
### Toolbar Features (optional)
233+
234+
Wenn du Suchfeld und/oder Language-Switcher wie in klassischen Filament-Listen brauchst, kannst du sie pro Tree-Resource aktivieren:
235+
236+
```php
237+
return TreeIndexConfiguration::make(Category::class)
238+
->toolbarSearch()
239+
->toolbarLanguageSwitcher()
240+
->applyLanguageUsing(
241+
fn (Builder $query, string $lang, TreeIndexConfiguration $config): Builder
242+
=> $query->whereHas('translations', fn (Builder $translationQuery): Builder => $translationQuery->where('locale', $lang))
243+
);
244+
```
245+
246+
Ohne `applyLanguageUsing()` setzt der Switcher nur den Livewire-`lang`-State und Query-String, verändert aber die Query nicht.
247+
228248
### Beispiel: Einfacher Baum (Adjacency List, verschiebbar)
229249

230250
```php
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@livewire(
2+
config('filament-tree-index.livewire.alias', 'filament-tree-index'),
3+
[
4+
'configurationKey' => $configurationKey,
5+
'lang' => $this->lang,
6+
'search' => $this->tableSearch ?? '',
7+
],
8+
key('filament-tree-index-'.$configurationKey.'-'.md5(json_encode([$this->tableFilters ?? [], $this->tableSearch ?? '', $this->activeTab ?? '', $this->lang ?? ''])))
9+
)

packages/tree/resources/views/filament/pages/tree-index.blade.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{{-- @deprecated Tree pages use ListRecords content() + EmbeddedTable; kept for reference. --}}
12
<x-filament-panels::page>
23
@livewire(
34
config('filament-tree-index.livewire.alias', 'filament-tree-index'),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div
2+
class="fi-sc fi-sc-has-gap fi-grid"
3+
style="--cols-default: repeat(1, minmax(0, 1fr));"
4+
>
5+
@if ($isToolbarSearchEnabled)
6+
<x-filament::input.wrapper
7+
prefix-icon="heroicon-m-magnifying-glass"
8+
class="w-full"
9+
>
10+
<x-filament::input
11+
type="search"
12+
wire:model.live.debounce.300ms="search"
13+
placeholder="Suchen..."
14+
/>
15+
</x-filament::input.wrapper>
16+
@endif
17+
18+
@if ($isToolbarLanguageSwitcherEnabled)
19+
<div class="w-full [&_.fi-dropdown]:!block [&_.fi-dropdown]:!w-full [&_.fi-dropdown-trigger]:!w-full [&_.fi-btn]:!w-full">
20+
@include('localization::lang-selector', ['fullWidth' => true])
21+
</div>
22+
@endif
23+
</div>

packages/tree/resources/views/livewire/resource-tree-index.blade.php

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,30 @@ class="fi-sc fi-sc-has-gap fi-grid lg:fi-grid-cols"
3434
</x-slot>
3535

3636
<div
37-
class="fi-tabs fi-vertical fi-contained fi-tree-scroll-panel"
38-
wire:key="resource-tree-{{ md5(json_encode($treeBranchIdsWithChildren)) }}"
39-
x-data
40-
x-init="$store.filamentTreeIndex.configure(@js($treeBranchIdsWithChildren), @js($treeAncestorIdsForSelection))"
37+
class="fi-sc fi-sc-has-gap fi-grid"
38+
style="--cols-default: repeat(1, minmax(0, 1fr));"
4139
>
42-
@include('filament-tree-index::livewire.resource-tree', [
43-
'items' => $tree,
44-
'parentId' => null,
45-
'selectedRecordId' => $selectedRecordId,
46-
'configuration' => $configuration,
47-
'isRoot' => true,
48-
])
40+
@if ($isToolbarSearchEnabled || $isToolbarLanguageSwitcherEnabled)
41+
@include('filament-tree-index::livewire.partials.tree-toolbar', [
42+
'isToolbarSearchEnabled' => $isToolbarSearchEnabled,
43+
'isToolbarLanguageSwitcherEnabled' => $isToolbarLanguageSwitcherEnabled,
44+
])
45+
@endif
46+
47+
<div
48+
class="fi-tabs fi-vertical fi-tree-scroll-panel"
49+
wire:key="resource-tree-{{ md5(json_encode($treeBranchIdsWithChildren)) }}"
50+
x-data
51+
x-init="$store.filamentTreeIndex.configure(@js($treeBranchIdsWithChildren), @js($treeAncestorIdsForSelection))"
52+
>
53+
@include('filament-tree-index::livewire.resource-tree', [
54+
'items' => $tree,
55+
'parentId' => null,
56+
'selectedRecordId' => $selectedRecordId,
57+
'configuration' => $configuration,
58+
'isRoot' => true,
59+
])
60+
</div>
4961
</div>
5062

5163
<x-slot name="footer">

0 commit comments

Comments
 (0)