├── .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 | [](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 | [](https://github.styleci.io/repos/94800927)
5 | [](https://packagist.org/packages/laravel-enso/menus)
6 | [](https://packagist.org/packages/laravel-enso/menus)
7 | [](https://packagist.org/packages/laravel-enso/menus)
8 |
9 | Menu Manager dependency for [Laravel Enso](https://github.com/laravel-enso/Enso)
10 |
11 | [](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 |
--------------------------------------------------------------------------------