diff --git a/app/Http/Controllers/Superuser/MenuController.php b/app/Http/Controllers/Superuser/MenuController.php new file mode 100644 index 0000000..30ee891 --- /dev/null +++ b/app/Http/Controllers/Superuser/MenuController.php @@ -0,0 +1,339 @@ + $route->getName(), Route::getRoutes()->getRoutes())); + + return Inertia::render('Superuser/Menu/Index')->with([ + 'permissions' => Permission::get(), + 'routes' => array_values($routes), + 'icons' => $this->icons(), + ]); + } + + /** + * @return array + */ + public function icons() + { + return array_map( + callback: fn (string $name) => mb_substr( + string: $name, + start: 0, + length: -4 + ), + array: array_map( + callback: fn (SplFileInfo $path) => $path->getFilename(), + array: array_map( + callback: fn (string $path) => new SplFileInfo( + filename: $path + ), + array: glob( + pattern: public_path( + path: '/vendors/fontawesome/svgs/**/*.svg' + ), + ), + ), + ), + ); + } + + /** + * @return \Illuminate\Http\Response + */ + public function get() + { + return Menu::with(['childs', 'permissions']) + ->whereNull('parent_id') + ->orderBy('position') + ->get(); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + $post = $request->validate([ + 'name' => 'required|string', + 'icon' => 'nullable|string', + 'route_or_url' => 'nullable|string', + 'actives.*' => 'nullable|string', + 'permissions.*' => 'nullable|integer|exists:permissions,id', + ]); + + $post['position'] = Menu::whereNull('parent_id')->count() + 1; + + if ($menu = Menu::create($post)) { + $menu->permissions()->sync($request->input('permissions', [])); + + return redirect()->back()->with('success', __( + 'menu `:name` has been created', [ + 'name' => $menu->name, + ] + )); + } + + return redirect()->back()->with('error', __( + 'can\'t create menu', + )); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\Menu $menu + * @return \Illuminate\Http\Response + */ + public function update(Request $request, Menu $menu) + { + $request->validate([ + 'name' => 'required|string', + 'icon' => 'nullable|string', + 'route_or_url' => 'nullable|string', + 'actives.*' => 'nullable|string', + 'permissions.*' => 'nullable|integer|exists:permissions,id', + ]); + + if ($menu->update($request->all())) { + $menu->permissions()->sync($request->input('permissions', [])); + + return redirect()->back()->with('success', __( + 'menu `:name` has been updated', [ + 'name' => $menu->name, + ] + )); + } + + return redirect()->back()->with('error', __( + 'can\'t update menu', + )); + } + + /** + * Remove the specified resource from storage. + * + * @param \App\Models\Menu $menu + * @return \Illuminate\Http\Response + */ + public function destroy(Menu $menu) + { + if ($menu->deleteable) { + DB::beginTransaction(); + + try { + if ($menu->delete()) { + Menu::where('parent_id', $menu->parent_id) + ->where('position', '>', $menu->position) + ->decrement('position'); + + DB::commit(); + + return redirect()->back()->with('success', __( + 'menu `:name` has been deleted', [ + 'name' => $menu->name, + ] + )); + } + } catch (Throwable $e) { + DB::rollBack(); + } + } + + return redirect()->back()->with('error', __( + 'can\'t delete menu', + )); + } + + /** + * @param \App\Models\Menu $menu + * @return \Illuminate\Http\Response + */ + public function up(Menu $menu) + { + $before = Menu::where('parent_id', $menu->parent_id) + ->where('position', $menu->position - 1) + ->first(); + + if (!$before) { + return redirect()->back()->with('error', __( + 'can\'t find menu before `:name`', [ + 'name' => $menu->name, + ] + )); + } + + DB::beginTransaction(); + + try { + Menu::where('id', $before->id)->update([ + 'position' => $menu->position, + ]); + + $menu->update([ + 'position' => $before->position, + ]); + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + + return redirect()->back()->with('error', __($e->getMessage())); + } + + return redirect()->back()->with('success', __( + 'position has been updated', + )); + } + + /** + * @param \App\Models\Menu $menu + * @return \Illuminate\Http\Response + */ + public function down(Menu $menu) + { + $after = Menu::where('parent_id', $menu->parent_id) + ->where('position', $menu->position + 1) + ->first(); + + if (!$after) { + return redirect()->back()->with('error', __( + 'can\'t find menu after `:name`', [ + 'name' => $menu->name, + ] + )); + } + + DB::beginTransaction(); + + try { + Menu::where('id', $after->id)->update([ + 'position' => $menu->position, + ]); + + $menu->update([ + 'position' => $after->position, + ]); + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + + return redirect()->back()->with('error', __($e->getMessage())); + } + + return redirect()->back()->with('success', __( + 'position has been updated', + )); + } + + /** + * @param \App\Models\Menu $menu + * @return \Illuminate\Http\Response + */ + public function right(Menu $menu) + { + $before = Menu::where('parent_id', $menu->parent_id) + ->where('position', $menu->position - 1) + ->withCount('childs') + ->first(); + + if (!$before) { + return redirect()->back()->with('error', __( + 'can\'t find menu before `:name`', [ + 'name' => $menu->name, + ] + )); + } + + DB::beginTransaction(); + + try { + Menu::where('parent_id', $menu->parent_id) + ->where('position', '>', $menu->position) + ->decrement('position'); + + $menu->update([ + 'parent_id' => $before->id, + 'position' => $before->childs_count + 1, + ]); + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + + return redirect()->back()->with('error', __($e->getMessage())); + } + + return redirect()->back()->with('success', __( + 'position has been updated', + )); + } + + /** + * @param \App\Models\Menu $menu + * @return \Illuminate\Http\Response + */ + public function left(Menu $menu) + { + $parent = $menu->parent; + + if (!$parent) { + return redirect()->back()->with('error', __( + 'can\'t find parent menu `:name`', [ + 'name' => $menu->name, + ] + )); + } + + DB::beginTransaction(); + + try { + Menu::where('parent_id', $parent->parent_id) + ->where('position', '>', $parent->position) + ->increment('position'); + + Menu::where('parent_id', $menu->parent_id) + ->where('position', '>', $menu->position) + ->decrement('position'); + + $menu->update([ + 'parent_id' => $parent->parent_id, + 'position' => $parent->position + 1, + ]); + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + + return redirect()->back()->with('error', __($e->getMessage())); + } + + return redirect()->back()->with('success', __( + 'position has been updated', + )); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 1c6e8db..4c8a528 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -64,7 +64,7 @@ class HandleInertiaRequests extends Middleware return $prev; }, $permissions); }, - '$menus' => $request->user()?->menus(), + '$menus' => fn () => $request->user()?->menus(), ]); } } diff --git a/app/Models/Menu.php b/app/Models/Menu.php index f6c7998..56b7f6d 100644 --- a/app/Models/Menu.php +++ b/app/Models/Menu.php @@ -26,12 +26,19 @@ class Menu extends Model 'deleteable', ]; + /** + * @var string[] + */ + protected $with = [ + 'permissions', + ]; + /** * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function parent() { - return $this->hasOne(Menu::class, 'id', 'parent_id')->without(['childs'])->with(['parent']); + return $this->hasOne(Menu::class, 'id', 'parent_id')->without(['childs'])->withCount(['childs']); } /** diff --git a/database/migrations/2022_07_15_192421_create_menus_table.php b/database/migrations/2022_07_15_192421_create_menus_table.php index c0abb19..c6fa2bf 100644 --- a/database/migrations/2022_07_15_192421_create_menus_table.php +++ b/database/migrations/2022_07_15_192421_create_menus_table.php @@ -21,7 +21,9 @@ return new class extends Migration $table->string('name'); $table->string('icon') ->default('circle'); - $table->string('route_or_url')->default('#'); + $table->string('route_or_url') + ->nullable() + ->default('#'); $table->unsignedTinyInteger('position'); $table->boolean('enable')->default(true); $table->boolean('deleteable')->default(true); diff --git a/database/seeders/MenuSeeder.php b/database/seeders/MenuSeeder.php index 85eb5f9..48f5ca1 100644 --- a/database/seeders/MenuSeeder.php +++ b/database/seeders/MenuSeeder.php @@ -37,6 +37,9 @@ class MenuSeeder extends Seeder 'icon' => 'key', 'position' => 1, 'deleteable' => false, + 'actives' => [ + 'superuser.permission.*', + ], ]); $permission->permissions()->attach( @@ -51,6 +54,9 @@ class MenuSeeder extends Seeder 'icon' => 'user-cog', 'position' => 2, 'deleteable' => false, + 'actives' => [ + 'superuser.role.*', + ], ]); $role->permissions()->attach( @@ -65,6 +71,9 @@ class MenuSeeder extends Seeder 'icon' => 'user', 'position' => 3, 'deleteable' => false, + 'actives' => [ + 'superuser.user.*', + ], ]); $user->permissions()->attach( @@ -79,6 +88,9 @@ class MenuSeeder extends Seeder 'icon' => 'bars', 'position' => 4, 'deleteable' => false, + 'actives' => [ + 'superuser.menu.*', + ], ]); $menu->permissions()->attach( diff --git a/resources/js/Pages/Superuser/Menu/Builder.vue b/resources/js/Pages/Superuser/Menu/Builder.vue new file mode 100644 index 0000000..bb70df4 --- /dev/null +++ b/resources/js/Pages/Superuser/Menu/Builder.vue @@ -0,0 +1,81 @@ + \ No newline at end of file diff --git a/resources/js/Pages/Superuser/Menu/Child.vue b/resources/js/Pages/Superuser/Menu/Child.vue new file mode 100644 index 0000000..e7fefec --- /dev/null +++ b/resources/js/Pages/Superuser/Menu/Child.vue @@ -0,0 +1,46 @@ + + + + + + + {{ menu.name }}({{ menu.position }}) + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/js/Pages/Superuser/Menu/Index.vue b/resources/js/Pages/Superuser/Menu/Index.vue new file mode 100644 index 0000000..4aae581 --- /dev/null +++ b/resources/js/Pages/Superuser/Menu/Index.vue @@ -0,0 +1,283 @@ + + + + + + + + + + + + + create + + + + + + + + + + + + + + + + + + + + + + + + + + + + name + + + + {{ form.errors.name }} + + + + + route name or url + + + + {{ form.errors.route_or_url }} + + + + + actives + + + + {{ form.errors.actives }} + + + + + permissions + + + + {{ form.errors.permissions }} + + + + icon + + + + + + + {{ form.icon }} + + + + + + change + + + + + + + + + + + + + {{ form.id ? 'update' : 'create' }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/js/Pages/Superuser/Menu/Parent.vue b/resources/js/Pages/Superuser/Menu/Parent.vue new file mode 100644 index 0000000..0de7293 --- /dev/null +++ b/resources/js/Pages/Superuser/Menu/Parent.vue @@ -0,0 +1,62 @@ + + + + + + + + {{ menu.name }} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index acd8ba7..cce2c0a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -22,5 +22,6 @@ Route::prefix('/v1')->name('api.v1.')->group(function () { Route::get('/superuser/role', [App\Http\Controllers\Superuser\RoleController::class, 'get'])->name('role'); Route::post('/superuser/role/paginate', [App\Http\Controllers\Superuser\RoleController::class, 'paginate'])->name('role.paginate'); Route::post('/superuser/user/paginate', [App\Http\Controllers\Superuser\UserController::class, 'paginate'])->name('user.paginate'); + Route::get('/superuser/menu', [App\Http\Controllers\Superuser\MenuController::class, 'get'])->name('menu'); }); }); \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 570a4cb..0e31128 100644 --- a/routes/web.php +++ b/routes/web.php @@ -37,5 +37,14 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'] Route::patch('/user/{user}/role/{role}/detach', [App\Http\Controllers\Superuser\UserController::class, 'detachRole'])->name('user.role.detach'); Route::patch('/user/{user}/permission/{permission}/detach', [App\Http\Controllers\Superuser\UserController::class, 'detachPermission'])->name('user.permission.detach'); + + Route::resource('menu', App\Http\Controllers\Superuser\MenuController::class)->only([ + 'index', 'store', 'update', 'destroy', + ]); + + Route::patch('/menu/{menu}/up', [App\Http\Controllers\Superuser\MenuController::class, 'up'])->name('menu.up'); + Route::patch('/menu/{menu}/down', [App\Http\Controllers\Superuser\MenuController::class, 'down'])->name('menu.down'); + Route::patch('/menu/{menu}/left', [App\Http\Controllers\Superuser\MenuController::class, 'left'])->name('menu.left'); + Route::patch('/menu/{menu}/right', [App\Http\Controllers\Superuser\MenuController::class, 'right'])->name('menu.right'); }); }); \ No newline at end of file
{{ menu.name }}({{ menu.position }})
create
+ +
{{ form.icon }}
change
{{ form.id ? 'update' : 'create' }}
{{ menu.name }}