├── .github └── issue_template.md ├── .styleci.yml ├── LICENSE ├── README.md ├── codesize.xml ├── composer.json ├── database ├── factories │ └── MenuFactory.php └── migrations │ ├── 2017_01_01_102000_create_menus_table.php │ └── 2017_01_01_125000_create_structure_for_menus.php ├── routes └── api.php ├── src ├── AppServiceProvider.php ├── Exceptions │ └── Menu.php ├── Forms │ ├── Builders │ │ └── Menu.php │ └── Templates │ │ └── menu.json ├── Http │ ├── Controllers │ │ ├── Create.php │ │ ├── Destroy.php │ │ ├── Edit.php │ │ ├── ExportExcel.php │ │ ├── InitTable.php │ │ ├── Organize.php │ │ ├── Store.php │ │ ├── TableData.php │ │ └── Update.php │ ├── Requests │ │ └── ValidateMenu.php │ └── Resources │ │ └── Menu.php ├── Models │ └── Menu.php ├── Services │ ├── Organizer.php │ └── TreeBuilder.php ├── State │ └── Menus.php └── Tables │ ├── Builders │ └── Menu.php │ └── Templates │ └── menus.json └── tests └── features └── MenuTest.php /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | This is a **bug | feature request**. 3 | 4 | 5 | ### Prerequisites 6 | * [ ] Are you running the latest version? 7 | * [ ] Are you reporting to the correct repository? 8 | * [ ] Did you check the documentation? 9 | * [ ] Did you perform a cursory search? 10 | 11 | ### Description 12 | 13 | 14 | ### Steps to Reproduce 15 | 20 | 21 | ### Expected behavior 22 | 23 | 24 | ### Actual behavior 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | risky: true 2 | 3 | preset: laravel 4 | 5 | enabled: 6 | - strict 7 | - unalign_double_arrow 8 | 9 | disabled: 10 | - short_array_syntax 11 | 12 | finder: 13 | exclude: 14 | - "public" 15 | - "resources" 16 | - "tests" 17 | name: 18 | - "*.php" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 laravel-enso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Menus 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d9b682700f5948d18462faed56effd41)](https://www.codacy.com/gh/laravel-enso/menus?utm_source=github.com&utm_medium=referral&utm_content=laravel-enso/menus&utm_campaign=Badge_Grade) 4 | [![StyleCI](https://github.styleci.io/repos/94800927/shield?branch=master)](https://github.styleci.io/repos/94800927) 5 | [![License](https://poser.pugx.org/laravel-enso/menus/license)](https://packagist.org/packages/laravel-enso/menus) 6 | [![Total Downloads](https://poser.pugx.org/laravel-enso/menus/downloads)](https://packagist.org/packages/laravel-enso/menus) 7 | [![Latest Stable Version](https://poser.pugx.org/laravel-enso/menus/version)](https://packagist.org/packages/laravel-enso/menus) 8 | 9 | Menu Manager dependency for [Laravel Enso](https://github.com/laravel-enso/Enso) 10 | 11 | [![Watch the demo](https://laravel-enso.github.io/menus/screenshots/bulma_012_thumb.png)](https://laravel-enso.github.io/menus/videos/bulma_menu_management.webm) 12 | 13 | click on the photo to view a short demo in compatible browsers 14 | 15 | ### Installation, Configuration & Usage 16 | 17 | Be sure to check out the full documentation for this package available at [docs.laravel-enso.com](https://docs.laravel-enso.com/backend/menus.html) 18 | 19 | ### Contributions 20 | 21 | are welcome. Pull requests are great, but issues are good too. 22 | 23 | ### License 24 | 25 | This package is released under the MIT license. 26 | -------------------------------------------------------------------------------- /codesize.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | custom rules 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-enso/menus", 3 | "description": "Menu Manager dependency for Laravel Enso", 4 | "keywords": [ 5 | "laravel-enso", 6 | "menus", 7 | "menu-manager", 8 | "breadcrumbs", 9 | "laravel-menu" 10 | ], 11 | "homepage": "https://github.com/laravel-enso/Menus", 12 | "type": "library", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Adrian Ocneanu", 17 | "email": "aocneanu@gmail.com", 18 | "homepage": "https://laravel-enso.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "laravel-enso/core": "^10.0", 24 | "laravel-enso/forms": "^4.0", 25 | "laravel-enso/migrator": "^2.0", 26 | "laravel-enso/permissions": "^5.0", 27 | "laravel-enso/roles": "^5.0", 28 | "laravel-enso/tables": "^4.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "LaravelEnso\\Menus\\": "src/", 33 | "LaravelEnso\\Menus\\Database\\Factories\\": "database/factories/" 34 | } 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "LaravelEnso\\Menus\\AppServiceProvider" 40 | ], 41 | "aliases": [] 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /database/factories/MenuFactory.php: -------------------------------------------------------------------------------- 1 | Permission::factory(), 17 | 'parent_id' => null, 18 | 'name' => $this->faker->word, 19 | 'icon' => $this->faker->word, 20 | 'has_children' => false, 21 | 'order_index' => $this->faker->randomNumber(3), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_102000_create_menus_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 13 | 14 | $table->integer('parent_id')->unsigned()->index()->nullable(); 15 | $table->foreign('parent_id')->references('id')->on('menus'); 16 | 17 | $table->integer('permission_id')->unsigned()->index()->nullable(); 18 | $table->foreign('permission_id')->references('id')->on('permissions'); 19 | 20 | $table->string('name'); 21 | $table->string('icon'); 22 | $table->integer('order_index'); 23 | 24 | $table->boolean('has_children'); 25 | 26 | $table->timestamps(); 27 | 28 | $table->unique(['parent_id', 'name']); 29 | }); 30 | } 31 | 32 | public function down() 33 | { 34 | Schema::dropIfExists('menus'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_125000_create_structure_for_menus.php: -------------------------------------------------------------------------------- 1 | 'system.menus.index', 'description' => 'Menus index', 'is_default' => false], 9 | ['name' => 'system.menus.tableData', 'description' => 'Get table data for menus', 'is_default' => false], 10 | ['name' => 'system.menus.exportExcel', 'description' => 'Export excel for menus', 'is_default' => false], 11 | ['name' => 'system.menus.initTable', 'description' => 'Init table for menus menu', 'is_default' => false], 12 | ['name' => 'system.menus.create', 'description' => 'Create menu', 'is_default' => false], 13 | ['name' => 'system.menus.edit', 'description' => 'Edit menu', 'is_default' => false], 14 | ['name' => 'system.menus.store', 'description' => 'Store newly created menu', 'is_default' => false], 15 | ['name' => 'system.menus.update', 'description' => 'Update edited menu', 'is_default' => false], 16 | ['name' => 'system.menus.destroy', 'description' => 'Delete menu', 'is_default' => false], 17 | ['name' => 'system.menus.organize', 'description' => 'Organize menus', 'is_default' => false], 18 | ]; 19 | 20 | protected array $menu = [ 21 | 'name' => 'Menus', 'icon' => 'list', 'route' => 'system.menus.index', 'order_index' => 999, 'has_children' => false, 22 | ]; 23 | 24 | protected ?string $parentMenu = 'System'; 25 | }; 26 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | prefix('api/system/menus') 16 | ->as('system.menus.') 17 | ->group(function () { 18 | Route::get('create', Create::class)->name('create'); 19 | Route::post('', Store::class)->name('store'); 20 | Route::get('{menu}/edit', Edit::class)->name('edit'); 21 | Route::patch('{menu}', Update::class)->name('update'); 22 | Route::delete('{menu}', Destroy::class)->name('destroy'); 23 | Route::put('organize', Organize::class)->name('organize'); 24 | 25 | Route::get('initTable', InitTable::class)->name('initTable'); 26 | Route::get('tableData', TableData::class)->name('tableData'); 27 | Route::get('exportExcel', ExportExcel::class)->name('exportExcel'); 28 | }); 29 | -------------------------------------------------------------------------------- /src/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | load() 12 | ->publish(); 13 | } 14 | 15 | private function load() 16 | { 17 | $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); 18 | 19 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 20 | 21 | return $this; 22 | } 23 | 24 | private function publish() 25 | { 26 | $this->publishes([ 27 | __DIR__.'/../database/factories' => database_path('factories'), 28 | ], ['menus-factory', 'enso-factories']); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exceptions/Menu.php: -------------------------------------------------------------------------------- 1 | form = (new Form($this->templatePath())) 18 | ->options('parent_id', Model::isParent()->get(['id', 'name'])) 19 | ->options('permission_id', Permission::get(['id', 'name'])); 20 | } 21 | 22 | public function create() 23 | { 24 | return $this->form->create(); 25 | } 26 | 27 | public function edit(Model $menu) 28 | { 29 | return $this->form->edit($menu); 30 | } 31 | 32 | protected function templatePath(): string 33 | { 34 | return self::TemplatePath; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Forms/Templates/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "routePrefix": "system.menus", 3 | "sections": [ 4 | { 5 | "columns": 2, 6 | "fields": [ 7 | { 8 | "label": "Name", 9 | "name": "name", 10 | "value": "", 11 | "meta": { 12 | "type": "input", 13 | "content": "text" 14 | } 15 | }, 16 | { 17 | "label": "Order", 18 | "name": "order_index", 19 | "value": 999, 20 | "meta": { 21 | "type": "input", 22 | "content": "number" 23 | } 24 | }, 25 | { 26 | "label": "Icon Class", 27 | "name": "icon", 28 | "value": "", 29 | "meta": { 30 | "type": "input", 31 | "content": "text" 32 | } 33 | }, 34 | { 35 | "label": "Route", 36 | "name": "permission_id", 37 | "value": null, 38 | "meta": { 39 | "type": "select", 40 | "options": [] 41 | } 42 | }, 43 | { 44 | "label": "Parent", 45 | "name": "parent_id", 46 | "value": null, 47 | "meta": { 48 | "type": "select", 49 | "multiple": false, 50 | "options": [] 51 | } 52 | }, 53 | { 54 | "label": "Has Children", 55 | "name": "has_children", 56 | "value": false, 57 | "meta": { 58 | "type": "input", 59 | "content": "checkbox" 60 | } 61 | } 62 | ] 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /src/Http/Controllers/Create.php: -------------------------------------------------------------------------------- 1 | $form->create()]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Http/Controllers/Destroy.php: -------------------------------------------------------------------------------- 1 | delete(); 13 | 14 | return [ 15 | 'message' => __('The menu was successfully deleted'), 16 | 'redirect' => 'system.menus.index', 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/Edit.php: -------------------------------------------------------------------------------- 1 | $form->edit($menu)]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Http/Controllers/ExportExcel.php: -------------------------------------------------------------------------------- 1 | get('menus')))->handle(); 14 | 15 | return ['message' => __('The menu order has been sucessfully updated')]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Http/Controllers/Store.php: -------------------------------------------------------------------------------- 1 | fill($request->validated())->save(); 14 | 15 | return [ 16 | 'message' => __('The menu was created!'), 17 | 'redirect' => 'system.menus.edit', 18 | 'param' => ['menu' => $menu->id], 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/TableData.php: -------------------------------------------------------------------------------- 1 | update($request->validated()); 14 | 15 | return ['message' => __('The menu was successfully updated')]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Http/Requests/ValidateMenu.php: -------------------------------------------------------------------------------- 1 | 'nullable', 19 | 'name' => [$this->nameUnique(), 'required'], 20 | 'permission_id' => 'nullable|exists:permissions,id', 21 | 'icon' => 'required', 22 | 'has_children' => 'boolean', 23 | 'order_index' => 'numeric|required', 24 | ]; 25 | } 26 | 27 | public function withValidator($validator) 28 | { 29 | $validator->after(function ($validator) { 30 | if ($this->get('has_children') && $this->filled('permission_id')) { 31 | $validator->errors()->add( 32 | 'has_children', 33 | __("The menu can't be a parent if the route isn't null") 34 | )->add( 35 | 'permission_id', 36 | __('The route has to be null if the menu is a parent') 37 | ); 38 | } 39 | 40 | if (! $this->get('has_children') && ! $this->filled('permission_id')) { 41 | $validator->errors()->add( 42 | 'has_children', 43 | __('The menu must be a parent if the route is null') 44 | )->add( 45 | 'permission_id', 46 | __("The route can't be null if the menu isn't a parent") 47 | ); 48 | } 49 | }); 50 | } 51 | 52 | protected function nameUnique() 53 | { 54 | return Rule::unique('menus', 'name') 55 | ->where(fn ($query) => $query->whereParentId($this->parent_id)) 56 | ->ignore($this->route('menu')?->id); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Http/Resources/Menu.php: -------------------------------------------------------------------------------- 1 | $this->id, 13 | 'name' => $this->name, 14 | 'icon' => $this->icon, 15 | 'hasChildren' => $this->has_children, 16 | 'children' => $this->children ? self::collection($this->children) : null, 17 | 'route' => $this->route, 18 | 'expanded' => false, 19 | 'active' => false, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Models/Menu.php: -------------------------------------------------------------------------------- 1 | belongsTo(self::class); 23 | } 24 | 25 | public function children() 26 | { 27 | return $this->hasMany(self::class, 'parent_id', 'id'); 28 | } 29 | 30 | public function permission() 31 | { 32 | return $this->belongsTo(Permission::class); 33 | } 34 | 35 | public function rolesWhereIsDefault() 36 | { 37 | return $this->hasMany(Role::class); 38 | } 39 | 40 | public function scopeIsParent(Builder $query) 41 | { 42 | return $query->whereHasChildren(true); 43 | } 44 | 45 | public function scopeIsNotParent(Builder $query) 46 | { 47 | return $query->whereHasChildren(false); 48 | } 49 | 50 | public function icon(): string|array 51 | { 52 | return Str::contains($this->icon, ' ') 53 | ? explode(' ', $this->icon) 54 | : $this->icon; 55 | } 56 | 57 | public function delete() 58 | { 59 | if ($this->children()->exists()) { 60 | throw Exception::hasChildren(); 61 | } 62 | 63 | if ($this->rolesWhereIsDefault()->exists()) { 64 | throw Exception::usedAsDefault(); 65 | } 66 | 67 | parent::delete(); 68 | } 69 | 70 | protected function casts(): array 71 | { 72 | return [ 73 | 'has_children' => 'boolean', 'parent_id' => 'integer', 74 | 'permission_id' => 'integer', 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Services/Organizer.php: -------------------------------------------------------------------------------- 1 | menus = new Obj($menus); 15 | } 16 | 17 | public function handle(): void 18 | { 19 | $this->organize($this->menus); 20 | } 21 | 22 | private function organize(Obj $menus): void 23 | { 24 | $menus->each(fn ($menu, $index) => $this->reorder($menu, $index)); 25 | } 26 | 27 | private function reorder(Obj $menu, int $index): void 28 | { 29 | Menu::find($menu->get('id')) 30 | ->update(['order_index' => ($index + 1) * 10]); 31 | 32 | if ($menu->get('has_children')) { 33 | $this->organize($menu->get('children')); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/TreeBuilder.php: -------------------------------------------------------------------------------- 1 | permissions() 18 | ->menus() 19 | ->filter() 20 | ->map() 21 | ->build(); 22 | } 23 | 24 | private function build(?int $parentId = null): Collection 25 | { 26 | return $this->menus 27 | ->filter(fn ($menu) => $menu->parent_id === $parentId) 28 | ->reduce(fn ($tree, $menu) => $tree 29 | ->push($this->withChildren($menu)), new Collection()); 30 | } 31 | 32 | private function withChildren(Menu $menu): Menu 33 | { 34 | $menu->children = $menu->has_children 35 | ? $this->build($menu->id) 36 | : null; 37 | 38 | $menu->route = $menu->permission?->name; 39 | 40 | unset($menu->permission); 41 | 42 | return $menu; 43 | } 44 | 45 | private function permissions(): self 46 | { 47 | $this->permissions = Auth::user()->role 48 | ->permissions() 49 | ->has('menu') 50 | ->get(['id', 'name']); 51 | 52 | return $this; 53 | } 54 | 55 | private function menus(): self 56 | { 57 | $this->menus = Menu::with('permission:id,name') 58 | ->orderBy('order_index') 59 | ->get(['id', 'parent_id', 'permission_id', 'name', 'icon', 'has_children']); 60 | 61 | return $this; 62 | } 63 | 64 | private function filter(): self 65 | { 66 | $this->menus = $this->menus->filter(fn ($menu) => $this->allowed($menu)); 67 | 68 | return $this; 69 | } 70 | 71 | private function map(): self 72 | { 73 | $this->menus = $this->menus->map(fn ($menu) => $this->computeIcon($menu)); 74 | 75 | return $this; 76 | } 77 | 78 | private function computeIcon(Menu $menu): Menu 79 | { 80 | if (Str::contains($menu->icon, ' ')) { 81 | $menu->icon = explode(' ', $menu->icon); 82 | } 83 | 84 | return $menu; 85 | } 86 | 87 | private function allowed($menu): bool 88 | { 89 | return $this->permissions->pluck('id')->contains($menu->permission_id) 90 | || $menu->has_children && $this->someChildrenAllowed($menu); 91 | } 92 | 93 | private function someChildrenAllowed($parent): bool 94 | { 95 | return $this->menus->some( 96 | fn ($child) => $child->parent_id === $parent->id && $this->allowed($child) 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/State/Menus.php: -------------------------------------------------------------------------------- 1 | handle()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Tables/Builders/Menu.php: -------------------------------------------------------------------------------- 1 | with('parent:id,name', 'permission:id,name') 22 | ->select($select); 23 | } 24 | 25 | public function templatePath(): string 26 | { 27 | return self::TemplatePath; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tables/Templates/menus.json: -------------------------------------------------------------------------------- 1 | { 2 | "routePrefix": "system.menus", 3 | "crtNo": true, 4 | "buttons": [ 5 | "excel", 6 | "create", 7 | "edit", 8 | "destroy" 9 | ], 10 | "columns": [ 11 | { 12 | "label": "Name", 13 | "name": "name", 14 | "data": "name", 15 | "meta": [ 16 | "searchable", 17 | "sortable" 18 | ] 19 | }, 20 | { 21 | "label": "Icon", 22 | "name": "icon", 23 | "data": "icon", 24 | "meta": [ 25 | "icon", 26 | "method", 27 | "notExportable" 28 | ] 29 | }, 30 | { 31 | "label": "Parent", 32 | "name": "parent.name", 33 | "data": "parent.name", 34 | "meta": [ 35 | "searchable" 36 | ] 37 | }, 38 | { 39 | "label": "Route", 40 | "name": "permission.name", 41 | "data": "permission.name", 42 | "meta": [ 43 | "searchable" 44 | ] 45 | }, 46 | { 47 | "label": "Has Children", 48 | "name": "has_children", 49 | "data": "has_children", 50 | "meta": [ 51 | "boolean" 52 | ] 53 | }, 54 | { 55 | "label": "Order", 56 | "name": "order_index", 57 | "data": "order_index", 58 | "meta": [ 59 | "sortable" 60 | ] 61 | }, 62 | { 63 | "label": "Created At", 64 | "name": "created_at", 65 | "data": "created_at", 66 | "meta": [ 67 | "sortable", 68 | "date" 69 | ] 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /tests/features/MenuTest.php: -------------------------------------------------------------------------------- 1 | seed() 25 | ->actingAs(User::first()); 26 | 27 | $this->testModel = Menu::factory() 28 | ->make(); 29 | } 30 | 31 | /** @test */ 32 | public function can_store_menu() 33 | { 34 | $response = $this->post( 35 | route('system.menus.store'), 36 | $this->testModel->toArray() 37 | ); 38 | 39 | $menu = Menu::whereName($this->testModel->name) 40 | ->first(); 41 | 42 | $response->assertStatus(200) 43 | ->assertJsonFragment([ 44 | 'redirect' => 'system.menus.edit', 45 | 'param' => ['menu' => $menu->id], 46 | ])->assertJsonStructure(['message']); 47 | } 48 | 49 | /** @test */ 50 | public function can_update_menu() 51 | { 52 | $this->testModel->save(); 53 | 54 | $this->testModel->name = 'edited'; 55 | 56 | $this->patch( 57 | route('system.menus.update', $this->testModel->id, false), 58 | $this->testModel->toArray() 59 | )->assertStatus(200)->assertJsonStructure(['message']); 60 | 61 | $this->assertEquals('edited', $this->testModel->fresh()->name); 62 | } 63 | 64 | /** @test */ 65 | public function cant_destroy_if_is_parent() 66 | { 67 | $parentMenu = Menu::factory()->create([ 68 | 'permission_id' => null, 69 | 'has_children' => true, 70 | ]); 71 | 72 | $this->testModel->parent_id = $parentMenu->id; 73 | $this->testModel->save(); 74 | $this->delete(route('system.menus.destroy', $parentMenu->id, false)) 75 | ->assertStatus(409); 76 | 77 | $this->assertNotNull($parentMenu->fresh()); 78 | } 79 | } 80 | --------------------------------------------------------------------------------