From 4e3f77826a7e964d59f78507a3e265f4171c2ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Thu, 17 Jun 2021 17:37:18 +0200 Subject: [PATCH 1/6] Ticket 232: Fix new ticket mail (ticket type on mail vue) --- resources/views/emails/new-ticket.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/emails/new-ticket.blade.php b/resources/views/emails/new-ticket.blade.php index 7b18c5c..4a48997 100644 --- a/resources/views/emails/new-ticket.blade.php +++ b/resources/views/emails/new-ticket.blade.php @@ -2,7 +2,7 @@

Un nouveau ticket (n°{{ $ticket->id }}) a été créer sur le support web :

From b60913525b23d9618f0b56c1c814797d4e4fad15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Fri, 18 Jun 2021 15:39:19 +0200 Subject: [PATCH 2/6] - bump to Bootstrap 5 - added ACL policies - added CRUD generic - added User crud - added search bar --- app/Console/Commands/CreateUser.php | 20 +- app/Enums/CrudAction.php | 49 ++ app/Enums/CrudType.php | 56 ++ app/Enums/Role.php | 29 + app/Exceptions/CrudException.php | 9 + app/Http/Controllers/CrudController.php | 241 ++++++++ app/Http/Controllers/ProjectController.php | 54 +- app/Http/Controllers/TicketController.php | 95 ++-- app/Http/Controllers/UserController.php | 12 + app/Http/Middleware/IsAdmin.php | 4 +- app/Http/Requests/ChangeTicketStatus.php | 3 +- app/Http/Requests/CreateMessage.php | 3 +- app/Http/Requests/CrudIndexRequest.php | 30 + app/Http/Requests/CrudRequest.php | 79 +++ app/Http/Requests/SortTickets.php | 1 + app/Http/Requests/StoreCrudModel.php | 35 ++ app/Http/Requests/StoreProject.php | 3 +- app/Http/Requests/UpdateCrudModel.php | 35 ++ app/Http/Requests/UpdateTicket.php | 3 +- app/Http/Resources/CrudResource.php | 22 + app/Http/Resources/UserLabeledResource.php | 28 + app/Lib/PolicySystem/RootPolicy.php | 106 ++++ app/Lib/PolicySystem/StaticRules.php | 86 +++ app/Lib/Utils.php | 9 +- app/Models/Project.php | 52 ++ app/Models/ProjectUser.php | 11 + app/Models/User.php | 72 ++- app/Policies/AppPolicies.php | 43 ++ app/Policies/ProjectPolicy.php | 91 +-- app/Policies/Rules/UserRoleRules.php | 20 + app/Policies/Rules/UserStaticRules.php | 33 ++ app/Policies/TicketPolicy.php | 10 +- app/Policies/UserPolicy.php | 10 + app/Providers/AppServiceProvider.php | 5 +- app/Providers/AuthServiceProvider.php | 5 +- app/Providers/RouteServiceProvider.php | 4 + app/Providers/TranslationServiceProvider.php | 61 +++ app/Traits/IsCrudModel.php | 516 ++++++++++++++++++ composer.json | 1 + config/app.php | 6 +- ...06_17_173912_create_project_user_table.php | 64 +++ package.json | 6 +- resources/js/app.js | 1 + resources/js/bootstrap.js | 33 +- resources/js/components/belongsTo.vue | 112 ++++ resources/js/components/belongsToMany.vue | 108 ++++ resources/js/crud.js | 19 + resources/js/modules/asset.js | 8 + resources/js/modules/route.js | 8 + resources/js/modules/trans.js | 41 ++ resources/sass/_crud.scss | 12 + resources/sass/app.scss | 4 +- resources/views/crud/create.blade.php | 53 ++ resources/views/crud/edit.blade.php | 62 +++ resources/views/crud/formfield.blade.php | 38 ++ .../views/crud/formfields/belongsTo.blade.php | 30 + .../crud/formfields/belongsToMany.blade.php | 29 + .../views/crud/formfields/boolean.blade.php | 20 + .../views/crud/formfields/email.blade.php | 33 ++ .../views/crud/formfields/enum.blade.php | 13 + .../views/crud/formfields/float.blade.php | 18 + resources/views/crud/formfields/int.blade.php | 18 + .../views/crud/formfields/json.blade.php | 22 + .../views/crud/formfields/password.blade.php | 67 +++ .../views/crud/formfields/string.blade.php | 33 ++ .../views/crud/formfields/text.blade.php | 22 + resources/views/crud/index.blade.php | 111 ++++ resources/views/crud/modules/sorter.blade.php | 12 + resources/views/crud/printfield.blade.php | 48 ++ resources/views/crud/show.blade.php | 66 +++ resources/views/layout/app.blade.php | 34 +- .../views/modules/flashMessage.blade.php | 44 ++ resources/views/modules/sorter.blade.php | 4 +- resources/views/project/create.blade.php | 26 - resources/views/project/edit.blade.php | 32 -- resources/views/project/form-inputs.blade.php | 4 - resources/views/project/index.blade.php | 32 -- resources/views/ticket/create.blade.php | 4 +- resources/views/ticket/edit.blade.php | 4 +- resources/views/ticket/form-inputs.blade.php | 18 +- resources/views/ticket/index.blade.php | 85 +-- resources/views/ticket/show.blade.php | 56 +- routes/web.php | 3 + webpack.mix.js | 21 +- 84 files changed, 3019 insertions(+), 411 deletions(-) create mode 100644 app/Enums/CrudAction.php create mode 100644 app/Enums/CrudType.php create mode 100644 app/Enums/Role.php create mode 100644 app/Exceptions/CrudException.php create mode 100644 app/Http/Controllers/CrudController.php create mode 100644 app/Http/Controllers/UserController.php create mode 100644 app/Http/Requests/CrudIndexRequest.php create mode 100644 app/Http/Requests/CrudRequest.php create mode 100644 app/Http/Requests/StoreCrudModel.php create mode 100644 app/Http/Requests/UpdateCrudModel.php create mode 100644 app/Http/Resources/CrudResource.php create mode 100644 app/Http/Resources/UserLabeledResource.php create mode 100644 app/Lib/PolicySystem/RootPolicy.php create mode 100644 app/Lib/PolicySystem/StaticRules.php create mode 100644 app/Models/ProjectUser.php create mode 100644 app/Policies/AppPolicies.php create mode 100644 app/Policies/Rules/UserRoleRules.php create mode 100644 app/Policies/Rules/UserStaticRules.php create mode 100644 app/Policies/UserPolicy.php create mode 100644 app/Providers/TranslationServiceProvider.php create mode 100644 app/Traits/IsCrudModel.php create mode 100644 database/migrations/2021_06_17_173912_create_project_user_table.php create mode 100644 resources/js/components/belongsTo.vue create mode 100644 resources/js/components/belongsToMany.vue create mode 100644 resources/js/crud.js create mode 100644 resources/js/modules/asset.js create mode 100644 resources/js/modules/route.js create mode 100644 resources/js/modules/trans.js create mode 100644 resources/sass/_crud.scss create mode 100644 resources/views/crud/create.blade.php create mode 100644 resources/views/crud/edit.blade.php create mode 100644 resources/views/crud/formfield.blade.php create mode 100644 resources/views/crud/formfields/belongsTo.blade.php create mode 100644 resources/views/crud/formfields/belongsToMany.blade.php create mode 100644 resources/views/crud/formfields/boolean.blade.php create mode 100644 resources/views/crud/formfields/email.blade.php create mode 100644 resources/views/crud/formfields/enum.blade.php create mode 100644 resources/views/crud/formfields/float.blade.php create mode 100644 resources/views/crud/formfields/int.blade.php create mode 100644 resources/views/crud/formfields/json.blade.php create mode 100644 resources/views/crud/formfields/password.blade.php create mode 100644 resources/views/crud/formfields/string.blade.php create mode 100644 resources/views/crud/formfields/text.blade.php create mode 100644 resources/views/crud/index.blade.php create mode 100644 resources/views/crud/modules/sorter.blade.php create mode 100644 resources/views/crud/printfield.blade.php create mode 100644 resources/views/crud/show.blade.php create mode 100644 resources/views/modules/flashMessage.blade.php delete mode 100644 resources/views/project/create.blade.php delete mode 100644 resources/views/project/edit.blade.php delete mode 100644 resources/views/project/form-inputs.blade.php delete mode 100644 resources/views/project/index.blade.php diff --git a/app/Console/Commands/CreateUser.php b/app/Console/Commands/CreateUser.php index 404d0c9..16c73ea 100644 --- a/app/Console/Commands/CreateUser.php +++ b/app/Console/Commands/CreateUser.php @@ -2,6 +2,7 @@ namespace App\Console\Commands; +use App\Enums\Role; use App\Models\Project; use App\Models\User; use Illuminate\Console\Command; @@ -41,34 +42,27 @@ class CreateUser extends Command */ public function handle() { - $admin = false; - $name = $this->ask('What is the name?'); $email = $this->ask('What is the email?'); $pwd = $this->secret('What is the password?'); + $user = new User(); + $user->name = $name; + $user->email = $email; if ($this->confirm('Is the user an admin?')) { - $admin = true; + $user->role = Role::admin(); } else { - $project = null; + $user->role = Role::client(); try { $project = Project::where( 'id', $this->ask("What's the id of the project (s)he's working on?") )->firstOrFail(); + $user->projects()->sync([$project->id]); } catch (ModelNotFoundException $e) { $this->error('The project does not exists'); return -1; } } - - $user = new User(); - $user->name = $name; - $user->email = $email; - if ($admin) { - $user->role = 1; - } else { - $user->project_id = $project->id; - } $user->password = Hash::make($pwd); $user->save(); diff --git a/app/Enums/CrudAction.php b/app/Enums/CrudAction.php new file mode 100644 index 0000000..a059ded --- /dev/null +++ b/app/Enums/CrudAction.php @@ -0,0 +1,49 @@ + 0, + 'view' => 1, + 'create' => 2, + 'update' => 3, + 'delete' => 4, + ]; + } + + protected static function labels(): array + { + return [ + 'viewAny' => trans('Liste'), + 'view' => trans('Voir'), + 'create' => trans('Créer'), + 'update' => trans('Éditer'), + 'delete' => trans('Supprimer'), + ]; + } +} diff --git a/app/Enums/CrudType.php b/app/Enums/CrudType.php new file mode 100644 index 0000000..b5f8301 --- /dev/null +++ b/app/Enums/CrudType.php @@ -0,0 +1,56 @@ + 0, + 'int' => 1, + 'string' => 2, + 'text' => 3, + 'json' => 4, + 'email' => 5, + 'password' => 6, + 'float' => 7, + 'enum' => 8, + 'belongsTo' => 9, + 'belongsToMany' => 10 + ]; + } + + protected static function labels(): array + { + return [ + 'boolean' => trans('Booléen'), + 'int' => trans('Nombre'), + 'string' => trans('Chaîne de charactères simple'), + 'text' => trans('Texte'), + 'json' => trans('Json'), + 'email' => trans('Chaîne de charactères email'), + 'password' => trans('Mot de passe'), + 'float' => trans('Nombre à virgule flotante'), + 'enum' => trans('Énumération'), + 'belongsTo' => trans('Clé étrangère'), + 'belongsToMany' => trans('Relation Belongs To Many') + ]; + } +} diff --git a/app/Enums/Role.php b/app/Enums/Role.php new file mode 100644 index 0000000..4bd4d9b --- /dev/null +++ b/app/Enums/Role.php @@ -0,0 +1,29 @@ + 1, + 'client' => 2 + ]; + } + + protected static function labels(): array + { + return [ + 'admin' => trans('Administrateur'), + 'client' => trans('Client') + ]; + } +} diff --git a/app/Exceptions/CrudException.php b/app/Exceptions/CrudException.php new file mode 100644 index 0000000..cdf73a9 --- /dev/null +++ b/app/Exceptions/CrudException.php @@ -0,0 +1,9 @@ +authorizeResource(static::$modelClass); + + $modelClass = static::$modelClass; + if (is_null($modelClass) or !class_exists($modelClass)) { + throw new CrudException(sprintf( + '%s needs the modelClass prop to be a class, %s %s given', + __CLASS__, + gettype($modelClass), + $modelClass + )); + } + if (!is_subclass_of($modelClass, Model::class)) { + throw new CrudException(sprintf( + '%s needs the model prop to be a class the extends %s', + __CLASS__, + Model::class + )); + } + if (!request()->ajax() and !app()->runningInConsole()) { + self::shareCrudModelsClassesToView(); + $this->shareModelClassToView(); + $this->shareModelTableToView(); + $this->shareModelPropsToView(); + } + } + + public function index(CrudIndexRequest $request) + { + /** @var \Illuminate\Database\Eloquent\Builder */ + $models = static::$modelClass::query(); + + // Search on crud Label column + if ($request->search) { + $model = static::$modelClass; + $col = (new $model())->crudLabelColumn; + $models = $models->where($col, 'LIKE', "%$request->search%"); + } + + // Sort columns + $tableName = (new static::$modelClass())->getTable(); + if ($request->rst) { + Session::remove("crud.$tableName.sort_col"); + Session::remove("crud.$tableName.sort_way"); + } + if ($request->sort_col) { + Session::put("crud.$tableName.sort_col", $request->sort_col); + Session::save(); + } + if ($request->sort_way) { + Session::put("crud.$tableName.sort_way", $request->sort_way); + Session::save(); + } + + if ( + Session::has("crud.$tableName.sort_col") and + Session::has("crud.$tableName.sort_way") and + Schema::hasColumn($tableName, Session::get("crud.$tableName.sort_col")) + ) { + $models = $models->orderBy( + Session::get("crud.$tableName.sort_col"), + Session::get("crud.$tableName.sort_way") + ); + } + + $models = $this->indexQuery($models); + /** @var \Illuminate\Pagination\AbstractPaginator */ + $models = $models->paginate(config('crud.paginate', 15)); + if ($request->ajax()) { + return response()->json(CrudResource::collection($models)); + } else { + $models = $models->withQueryString(); + return view('crud.index', compact('models')); + } + } + + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass + public function show(Model $model) + { + return view('crud.show', compact('model')); + } + + public function create() + { + return view('crud.create'); + } + + public function store(StoreCrudModel $request) + { + $modelClass = static::$modelClass; + $model = new $modelClass(); + $model->fill($request->validated()); + $this->storeModel($model); + $model->saveOrFail(); + $model->saveRelations($request); + $this->storedModel($model); + return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model) + ->with('success', trans(':model à bien été enregisté.', ['model' => $model->getModelName()])); + } + + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass + public function edit(Model $model) + { + return view('crud.edit', compact('model')); + } + + public function update(UpdateCrudModel $request, Model $model) + { + $model->fill($request->validated()); + $this->updateModel($model); + $model->saveOrFail(); + $model->saveRelations($request); + $this->updatedModel($model); + return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model) + ->with('success', trans(':model à bien été enregisté.', ['model' => $model->getModelName()])); + } + + public function destroy(Model $model) + { + if ($restrict = $model->gotHasManyRelationWithRestrictOnDelete()) { + return redirect()->back() + ->with('error', trans( + ':model n\'as pas pu être supprimé car il est encore associé avec des :associated', + [ + 'model' => $model->getModelName(), + 'associated' => Str::plural($restrict) + ] + )); + } + if ($model->delete()) { + return \redirect()->route(sprintf('%s.index', $model->getTable())) + ->with('warning', trans(':model à bien été supprimé.', ['model' => $model->getModelName()])); + } else { + return redirect()->back() + ->with('error', trans(':model n\'as pas pu être supprimé', ['model' => $model->getModelName()])); + } + } + + public function getModelClass() + { + return static::$modelClass; + } + + /** + * Get the current model class + * + * @return void + */ + public static function shareCrudModelsClassesToView(): void + { + $models = []; + foreach (File::allFiles(\app_path('Http/Controllers')) as $file) { + $class = sprintf('App\Http\Controllers\%s', \substr($file->getFilename(), 0, -4)); + if (is_subclass_of($class, CrudController::class) and $class::$modelClass) { + $models[] = $class::$modelClass; + } + } + View::share('modelsClasses', $models); + } + + /** + * Get the current model class + * + * @return void + */ + private function shareModelClassToView(): void + { + View::share('modelClass', static::$modelClass); + } + + /** + * Get the current model table + * + * @return void + */ + private function shareModelTableToView(): void + { + /** @var \Illuminate\Database\Eloquent\Model $model */ + $model = static::$modelClass; + + View::share('modelTable', (new $model())->getTable()); + } + + /** + * Get the current model properties + * + * @return void + */ + private function shareModelPropsToView(): void + { + $model = static::$modelClass; + View::share('props', (new $model())->getEditableProperties()); + } +} diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index ef0910d..ab43dd2 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -2,59 +2,11 @@ namespace App\Http\Controllers; -use App\Http\Requests\StoreProject; -use App\Http\Requests\UpdateProject; use App\Models\Project; -use App\Models\Ticket; -class ProjectController extends Controller +class ProjectController extends CrudController { - public function __construct() - { - $this->authorizeResource(Project::class, 'project'); - } - public function index() - { - $projects = Project::all(); - return view('project.index', compact('projects')); - } - - public function create() - { - return view('project.create'); - } - - public function store(StoreProject $request) - { - $project = new Project($request->validated()); - $project->saveOrFail(); - - return redirect()->route('project.index'); - } - - public function edit(Project $project) - { - return view('project.edit', [ - 'project' => $project - ]); - } - - public function update(UpdateProject $request, Project $project) - { - $project->fill($request->validated()); - $project->saveOrFail(); - - return redirect()->back()->with('success', 'Modifications enregistrées !'); - } - - public function destroy(Project $project) - { - if (Ticket::where('project_id', $project->id)->count()) { - return redirect()->back() - ->withErrors("Ce projet ne peut être supprimer, des tickets lui sont encore liés."); - } - $project->delete(); - return redirect()->route('project.index'); - } + /** @var Project */ + protected static $modelClass = Project::class; } diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php index 31c9c0d..ce28b78 100644 --- a/app/Http/Controllers/TicketController.php +++ b/app/Http/Controllers/TicketController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Enums\TicketStatus; +use App\Enums\TicketType; use App\Events\NotifySubscriptions; use App\Http\Requests\ChangeTicketStatus; use App\Http\Requests\CreateMessage; @@ -18,6 +19,8 @@ use App\Models\Ticket; use App\Models\User; use Exception; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Mail; @@ -37,13 +40,20 @@ class TicketController extends Controller if (auth()->user()->project_id) { $tickets = $tickets->where('project_id', auth()->user()->project_id); } - - if ($request->status) { - $tickets = $tickets->where('status', TicketStatus::archived()); - } else { - $tickets = $tickets->where('status', '<>', TicketStatus::archived()); + $tickets = $tickets->where('status', $request->status ? '=' : '<>', TicketStatus::archived()); + if ($request->search) { + $tickets = $this->searchTicket($tickets, $request); } + $tickets = $this->filterTickets($tickets, $request); + $tickets = $tickets->paginate(); + $projects = Project::select('id', 'name')->get(); + + return view('ticket.index', compact('tickets', 'projects')); + } + + private function filterTickets(Builder $tickets, Request $request): Builder + { if ($request->rst) { Session::remove('tickets.pr'); Session::remove('tickets.sort_col'); @@ -54,48 +64,65 @@ class TicketController extends Controller } if ($request->sort_col) { Session::put('tickets.sort_col', $request->sort_col); - Session::save(); } if ($request->sort_way) { Session::put('tickets.sort_way', $request->sort_way); - Session::save(); } if ($request->pr) { Session::put('tickets.pr', $request->pr); - Session::save(); } + Session::save(); + if (Session::has('tickets.pr')) { $tickets = $tickets->where('project_id', Session::get('tickets.pr')); } - if (Session::has('tickets.sort_col') and Session::has('tickets.sort_way')) { - switch (Session::get('tickets.sort_col')) { - case 'id': - $tickets = $tickets->orderBy('id', Session::get('tickets.sort_way')); - break; - case 'project': - $tickets = $tickets->orderBy('project_id', Session::get('tickets.sort_way')); - break; - case 'created_at': - $tickets = $tickets->orderBy('created_at', Session::get('tickets.sort_way')); - break; - case 'created_by': - $tickets = $tickets->orderBy('created_by', Session::get('tickets.sort_way')); - break; - case 'type': - $tickets = $tickets->orderBy('type', Session::get('tickets.sort_way')); - break; - case 'status': - $tickets = $tickets->orderBy('status', Session::get('tickets.sort_way')); - break; - } + if ( + Session::has('tickets.sort_col') and + Session::has('tickets.sort_way') and + in_array(Session::get('tickets.sort_col'), [ + 'id', 'project', 'created_at', 'created_by', + 'type', 'status' + ]) + ) { + $tickets = $tickets->orderBy( + Session::get('tickets.sort_col'), + Session::get('tickets.sort_way') + ); } + return $tickets; + } - $tickets = $tickets->paginate(); - - $projects = Project::select('id', 'name')->get(); - - return view('ticket.index', compact('tickets', 'projects')); + private function searchTicket(Builder $tickets, Request $request): Builder + { + $words = explode(' ', $request->search); + return $tickets->where(function ($q) use ($words) { + foreach ($words as $word) { + if (!strlen($word)) { + continue; + } + $q = $q->where(function ($q) use ($word) { + $s = collect(TicketStatus::toArray())->filter(function (TicketStatus $t) use ($word) { + return strpos(strtolower($t->label), $word) !== false; + })->map(function (TicketStatus $ticket) { + return $ticket->value; + }); + $t = collect(TicketType::toArray())->filter(function (TicketType $t) use ($word) { + return strpos(strtolower($t->label), $word) !== false; + })->map(function (TicketType $ticket) { + return $ticket->value; + }); + $q->where('text', 'LIKE', "%$word%") + ->orWhereIn('status', $s) + ->orWhereIn('type', $t) + ->orWhere('file', 'LIKE', "%$word%") + ->orWhere('file2', 'LIKE', "%$word%") + ->orWhereHas('user', function ($q) use ($word) { + $q->where('name', 'LIKE', "%$word%"); + }); + }); + } + }); } public function show(Ticket $ticket) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..8e43ef0 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,12 @@ +role === 1) + if (Auth::user()->role->equals(Role::admin())) { return $next($request); + } return redirect()->route('home'); } } diff --git a/app/Http/Requests/ChangeTicketStatus.php b/app/Http/Requests/ChangeTicketStatus.php index f66afef..8386c9d 100644 --- a/app/Http/Requests/ChangeTicketStatus.php +++ b/app/Http/Requests/ChangeTicketStatus.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Enums\Role; use App\Enums\TicketStatus; use Illuminate\Foundation\Http\FormRequest; @@ -15,7 +16,7 @@ class ChangeTicketStatus extends FormRequest public function authorize() { return auth()->check() and - auth()->user()->role === 1; + auth()->user()->role->equals(Role::admin()); } /** diff --git a/app/Http/Requests/CreateMessage.php b/app/Http/Requests/CreateMessage.php index 8372c88..69d12af 100644 --- a/app/Http/Requests/CreateMessage.php +++ b/app/Http/Requests/CreateMessage.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Enums\Role; use Illuminate\Foundation\Http\FormRequest; class CreateMessage extends FormRequest @@ -16,7 +17,7 @@ class CreateMessage extends FormRequest $this->merge([ 'subscribe' => $this->subscribe ? true : false ]); - return auth()->check() and (auth()->user()->role === 1 or + return auth()->check() and (auth()->user()->role->equals(Role::admin()) or $this->ticket->project->id === auth()->user()->project_id); } diff --git a/app/Http/Requests/CrudIndexRequest.php b/app/Http/Requests/CrudIndexRequest.php new file mode 100644 index 0000000..c745aa3 --- /dev/null +++ b/app/Http/Requests/CrudIndexRequest.php @@ -0,0 +1,30 @@ +check(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'search' => 'nullable|string' + ]; + } +} diff --git a/app/Http/Requests/CrudRequest.php b/app/Http/Requests/CrudRequest.php new file mode 100644 index 0000000..f2d06f8 --- /dev/null +++ b/app/Http/Requests/CrudRequest.php @@ -0,0 +1,79 @@ +getCurrentModel()::getEditableProperties(); + $propsRules = []; + foreach ($this->getCurrentModel()::getRules() as $k => $rules) { + $propsRules[$k] = []; + $propIsAllowedOnUpdateAction = in_array($action, $props[$k]['actions']); + foreach ($rules as $rule) { + // dont copy required if prop is not allowed on update + if (!$propIsAllowedOnUpdateAction and is_string($rule) and $rule == 'required') { + continue; + } + if (!in_array($rule, $propsRules[$k])) { + $propsRules[$k][] = $rule; + } + // handle password + if ( + $props[$k]['type']->equals(CrudType::password()) and + !$this->{$k} or (is_string($this->{$k}) and !\strlen($this->{$k})) + ) { + $this->offsetUnset($k); + } + // handle boolean + if ($props[$k]['type']->equals(CrudType::boolean())) { + $this->merge([$k => $this->{$k} ? 1 : 0]); + } + // handle belongsToMany + if ($props[$k]['type']->equals(CrudType::belongsToMany())) { + $propsRules["$k.*"][0] = 'exists:' . + (new $props[$k]['belongsToMany']())->getTable() . ',id'; + } + } + } + return $propsRules; + } + /** + * Returns the first parameter that is a crud model + * + * @return string \Illuminate\Database\Eloquent\Model class name + * @throws \App\Exceptions\CrudException if the request does not have a crud model parameter + */ + protected function getCurrentModel(): string + { + if ( + !is_object($this->route()->controller) or + !$this->route()->controller instanceof CrudController + ) { + throw new CrudException(sprintf( + 'Request `%s` needs to be bound on route that use a CrudController', + get_class($this) + )); + } + return $this->route()->controller->getModelClass(); + } +} diff --git a/app/Http/Requests/SortTickets.php b/app/Http/Requests/SortTickets.php index e356532..d60352f 100644 --- a/app/Http/Requests/SortTickets.php +++ b/app/Http/Requests/SortTickets.php @@ -25,6 +25,7 @@ class SortTickets extends FormRequest public function rules() { return [ + 'search' => 'nullable|string', 'sort_way' => 'sometimes|in:asc,desc', 'status' => [ 'sometimes', diff --git a/app/Http/Requests/StoreCrudModel.php b/app/Http/Requests/StoreCrudModel.php new file mode 100644 index 0000000..fe8857e --- /dev/null +++ b/app/Http/Requests/StoreCrudModel.php @@ -0,0 +1,35 @@ +getCurrentModel(); + /** @var \App\Models\User|null $user */ + $user = auth()->user(); + $authorized = auth()->check() and $user->can('create', $model); + if ($authorized) { + $this->rules = $this->getRulesForAction(CrudAction::create()); + } + return $authorized; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return $this->rules; + } +} diff --git a/app/Http/Requests/StoreProject.php b/app/Http/Requests/StoreProject.php index 0ce8310..162b683 100644 --- a/app/Http/Requests/StoreProject.php +++ b/app/Http/Requests/StoreProject.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Enums\Role; use Illuminate\Foundation\Http\FormRequest; class StoreProject extends FormRequest @@ -14,7 +15,7 @@ class StoreProject extends FormRequest public function authorize() { return auth()->check() and - auth()->user()->role === 1; + auth()->user()->role->equals(Role::admin()); } /** diff --git a/app/Http/Requests/UpdateCrudModel.php b/app/Http/Requests/UpdateCrudModel.php new file mode 100644 index 0000000..91edc6a --- /dev/null +++ b/app/Http/Requests/UpdateCrudModel.php @@ -0,0 +1,35 @@ +getCurrentModel(); + /** @var \App\Models\User|null $user */ + $user = auth()->user(); + $authorized = auth()->check() and $user->can('update', $model); + if ($authorized) { + $this->rules = $this->getRulesForAction(CrudAction::update()); + } + return $authorized; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return $this->rules; + } +} diff --git a/app/Http/Requests/UpdateTicket.php b/app/Http/Requests/UpdateTicket.php index cdc15b9..a24a5fd 100644 --- a/app/Http/Requests/UpdateTicket.php +++ b/app/Http/Requests/UpdateTicket.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Enums\Role; use App\Enums\TicketStatus; class UpdateTicket extends StoreTicket @@ -14,7 +15,7 @@ class UpdateTicket extends StoreTicket 'status' => $this->project->status->value, ]); } - return auth()->check() and (auth()->user()->role === 1 or + return auth()->check() and (auth()->user()->role->equals(Role::admin()) or request()->route()->ticket->created_by === auth()->user()->id); } } diff --git a/app/Http/Resources/CrudResource.php b/app/Http/Resources/CrudResource.php new file mode 100644 index 0000000..dd2853b --- /dev/null +++ b/app/Http/Resources/CrudResource.php @@ -0,0 +1,22 @@ + $this->crudLabel, + 'value' => $this->crudValue + ]; + } +} diff --git a/app/Http/Resources/UserLabeledResource.php b/app/Http/Resources/UserLabeledResource.php new file mode 100644 index 0000000..09bfed0 --- /dev/null +++ b/app/Http/Resources/UserLabeledResource.php @@ -0,0 +1,28 @@ +collection->mapWithKeys(function (User $user, $key) { + return [ + $key => [ + 'label' => sprintf('%d %s', $user->id, $user->name), + 'key' => $user->id + ] + ]; + }); + } +} diff --git a/app/Lib/PolicySystem/RootPolicy.php b/app/Lib/PolicySystem/RootPolicy.php new file mode 100644 index 0000000..121d5d4 --- /dev/null +++ b/app/Lib/PolicySystem/RootPolicy.php @@ -0,0 +1,106 @@ +policy = \strtolower(\str_replace('Policy', '', \end($cls))); + } + + protected function actionPolicy(string $method, string $policy, User $user, Model $model = null) + { + $this->setPolicies($user, $model); + $ctx = [$policy, $user, $model]; + + if (!$this->hasPolicy($policy)) { + $m = __('Unhandled policy ":policy"', ['policy' => $policy]); + Log::critical($m, $ctx); + return $this->deny($m); + } + + if (!is_array($this->policies[$policy])) { + $m = __('Policy ":policy" has to be array', ['policy' => $policy]); + Log::critical($m, $ctx); + return $this->deny($m); + } + + // Policy method exists + if (!array_key_exists($method, $this->policies[$policy])) { + $m = __('Unhandled policy ":policy" for action ":action"', ['policy' => $policy, 'action' => $method]); + Log::critical($m, $ctx); + return $this->deny($m); + } + + // Call policy method + if (Sr::handle($this->policies[$policy][$method], $user, $model)) { + return $this->allow(); + } else { + $m = __('Policy ":policy" for action ":action" denied :onmodel', [ + 'policy' => $policy, + 'action' => $method, + 'onmodel' => $model ? get_class($model) : '' + ]); + if (config('app.policyDebug')) { + Log::debug(sprintf("%s user => %s model => %s", $m, $user, $model), $ctx); + } + return $this->deny($m); + } + + $m = __('Unhandled policy case on resource ":method"', ['method' => $method]); + Log::critical($m, $ctx); + return $this->deny(); + } + + /** + ** Do the magic calling Root policy from any policy + */ + public function __call($method, $args) + { + return call_user_func_array([$this, 'actionPolicy'], array_merge([$method, $this->policy], $args)); + } + + private function hasPolicy(string $policy) + { + return array_key_exists($policy, $this->policies); + } + + /** + * Allow action for the user + * + * @param string|null $message + * @return \Illuminate\Auth\Access\Response + */ + protected function allow(?string $message = null) + { + return Response::allow($message); + } + + /** + * Deny action for the user + * + * @param string|null $message + * @param mixed $code + * @return \Illuminate\Auth\Access\Response + */ + protected function deny(?string $message = null, $code = null) + { + if (!$message) { + $message = __('This action is not allowed to you for this resource.'); + } + return Response::deny($message, $code); + } +} diff --git a/app/Lib/PolicySystem/StaticRules.php b/app/Lib/PolicySystem/StaticRules.php new file mode 100644 index 0000000..034b05e --- /dev/null +++ b/app/Lib/PolicySystem/StaticRules.php @@ -0,0 +1,86 @@ +$match" : $match; + $fsligthbox = self::isImageFile($url) ? 'data-fslightbox' : ''; + return strlen($match) ? "$match" : $match; } }, $text ); return nl2br($text); } + + public static function isImageFile(string $url): bool + { + $m = []; + return \preg_match('/(.png|.jpg|.jpeg|.gif)$/', $url, $m) === 1; + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index f1498f8..28a236a 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -2,6 +2,10 @@ namespace App\Models; +use App\Enums\CrudAction; +use App\Enums\CrudType; +use App\Enums\Role; +use App\Traits\IsCrudModel; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\File; @@ -10,9 +14,52 @@ use Illuminate\Support\Str; class Project extends Model { use HasFactory; + use IsCrudModel; + + protected $crudLabelColumn = 'name'; + + protected $crudValueColumn = 'id'; protected $fillable = ['name']; + public function __construct($attributes = []) + { + self::$editableProperties = [ + 'name' => [ + 'label' => trans('Nom'), + 'type' => CrudType::string(), + 'validate' => [ + 'string', + 'max:255' + ], + 'required' => true, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'users' => [ + 'label' => trans('Utilisateurs'), + 'type' => CrudType::belongsToMany(), + 'belongsToMany' => User::class, + 'belongsToManyLabel' => 'name', + 'readonly' => function () { + return auth()->user()->role->equals(Role::admin()) ? false : true; + }, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ] + ]; + parent::__construct($attributes); + } + protected static function booted() { static::created(function (Project $project) { @@ -39,6 +86,11 @@ class Project extends Model }); } + public function users() + { + return $this->belongsToMany(User::class); + } + public function tickets() { return $this->hasMany(Ticket::class, 'project_id'); diff --git a/app/Models/ProjectUser.php b/app/Models/ProjectUser.php new file mode 100644 index 0000000..5d2558e --- /dev/null +++ b/app/Models/ProjectUser.php @@ -0,0 +1,11 @@ + Role::class + ]; + + protected $crudLabelColumn = 'name'; + + protected $crudValueColumn = 'id'; /** * The attributes that are mass assignable. @@ -41,6 +56,59 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', ]; + public function __construct($attributes = []) + { + self::$editableProperties = [ + 'name' => [ + 'label' => trans('Nom'), + 'type' => CrudType::string(), + 'validate' => [ + 'string', + 'max:255' + ], + 'required' => true, + 'nullable' => false, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'email' => [ + 'label' => trans('Email'), + 'type' => CrudType::email(), + 'required' => true, + 'nullable' => false, + 'validate' => [ + 'max:255' + ], + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ], + 'projects' => [ + 'label' => trans('Projets'), + 'type' => CrudType::belongsToMany(), + 'belongsToMany' => Project::class, + 'belongsToManyLabel' => 'name', + 'readonly' => function () { + return auth()->user()->role->equals(Role::admin()) ? false : true; + }, + 'actions' => [ + CrudAction::viewAny(), + CrudAction::view(), + CrudAction::create(), + CrudAction::update(), + ] + ] + ]; + parent::__construct($attributes); + } + public function tickets() { return $this->hasMany(Ticket::class, 'created_by'); @@ -56,8 +124,8 @@ class User extends Authenticatable return $this->belongsToMany(Ticket::class, 'subscriptions'); } - public function project() + public function projects() { - return $this->belongsTo(Project::class); + return $this->belongsToMany(Project::class); } } diff --git a/app/Policies/AppPolicies.php b/app/Policies/AppPolicies.php new file mode 100644 index 0000000..849952d --- /dev/null +++ b/app/Policies/AppPolicies.php @@ -0,0 +1,43 @@ +policies = [ + 'project' => [ + 'viewAny' => URR::make('userIsAdmin', $user), + 'view' => URR::make('userIsAdmin', $user), + 'update' => URR::make('userIsAdmin', $user), + 'create' => URR::make('userIsAdmin', $user), + 'delete' => URR::make('userIsAdmin', $user) + ], + 'user' => [ + 'viewAny' => URR::make('userIsAdmin', $user), + 'view' => URR::make('userIsAdmin', $user), + 'update' => URR::make('userIsAdmin', $user), + 'create' => URR::make('userIsAdmin', $user), + 'delete' => URR::make('userIsAdmin', $user) + ], + ]; + } +} diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php index 9513d25..4ef9498 100644 --- a/app/Policies/ProjectPolicy.php +++ b/app/Policies/ProjectPolicy.php @@ -2,98 +2,11 @@ namespace App\Policies; -use App\Models\Project; -use App\Models\User; use Illuminate\Auth\Access\HandlesAuthorization; -class ProjectPolicy +class ProjectPolicy extends AppPolicies { use HandlesAuthorization; - /** - * Determine whether the user can view any models. - * - * @param \App\Models\User $user - * @return mixed - */ - public function viewAny(User $user) - { - return $user->role === 1; - } - - /** - * Determine whether the user can view the model. - * - * @param \App\Models\User $user - * @param \App\Models\Project $project - * @return mixed - */ - //phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed - public function view(User $user, Project $project) - { - return $user->role === 1; - } - - /** - * Determine whether the user can create models. - * - * @param \App\Models\User $user - * @return mixed - */ - public function create(User $user) - { - return $user->role === 1; - } - - /** - * Determine whether the user can update the model. - * - * @param \App\Models\User $user - * @param \App\Models\Project $project - * @return mixed - */ - //phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed - public function update(User $user, Project $project) - { - return $user->role === 1; - } - - /** - * Determine whether the user can delete the model. - * - * @param \App\Models\User $user - * @param \App\Models\Project $project - * @return mixed - */ - //phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed - public function delete(User $user, Project $project) - { - return $user->role === 1; - } - - /** - * Determine whether the user can restore the model. - * - * @param \App\Models\User $user - * @param \App\Models\Project $project - * @return mixed - */ - //phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed - public function restore(User $user, Project $project) - { - return $user->role === 1; - } - - /** - * Determine whether the user can permanently delete the model. - * - * @param \App\Models\User $user - * @param \App\Models\Project $project - * @return mixed - */ - //phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed - public function forceDelete(User $user, Project $project) - { - return $user->role === 1; - } + // Handle delete has tickets } diff --git a/app/Policies/Rules/UserRoleRules.php b/app/Policies/Rules/UserRoleRules.php new file mode 100644 index 0000000..afbfd55 --- /dev/null +++ b/app/Policies/Rules/UserRoleRules.php @@ -0,0 +1,20 @@ +role->equals(Role::admin()); + } +} diff --git a/app/Policies/Rules/UserStaticRules.php b/app/Policies/Rules/UserStaticRules.php new file mode 100644 index 0000000..9258d8c --- /dev/null +++ b/app/Policies/Rules/UserStaticRules.php @@ -0,0 +1,33 @@ +id == $userToCheck->id) != 0; + } + + protected static function usersAreNotTheSame(User $user, User $userToCheck) + { + return !self::usersAreTheSame($user, $userToCheck); + } + + protected static function everyOne() + { + return true; + } + + protected static function noOne() + { + return false; + } +} diff --git a/app/Policies/TicketPolicy.php b/app/Policies/TicketPolicy.php index 1f023b7..fc105a5 100644 --- a/app/Policies/TicketPolicy.php +++ b/app/Policies/TicketPolicy.php @@ -2,6 +2,7 @@ namespace App\Policies; +use App\Enums\Role; use App\Enums\TicketStatus; use App\Models\Ticket; use App\Models\User; @@ -59,7 +60,7 @@ class TicketPolicy */ public function update(User $user, Ticket $ticket) { - return Auth::user()->role === 1 or $ticket->created_by == $user->id; + return Auth::user()->role->equals(Role::admin()) or $ticket->created_by == $user->id; } /** @@ -84,7 +85,7 @@ class TicketPolicy */ public function restore(User $user, Ticket $ticket) { - return Auth::user()->role === 1 or $ticket->created_by == $user->id; + return Auth::user()->role->equals(Role::admin()) or $ticket->created_by == $user->id; } /** @@ -102,7 +103,7 @@ class TicketPolicy public function changeStatus(User $user) { - return $user->role === 1; + return $user->role->equals(Role::admin()); } // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed @@ -120,7 +121,8 @@ class TicketPolicy // OR project_id == user->projetct_id if ( - !($user->role === 1 + !( + $user->role->equals(Role::admin()) or $ticket->created_by === $user->id or $ticket->project_id === $user->project_id) ) diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000..23d56de --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,10 @@ +count()); diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 3a82fd0..074e26b 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -15,10 +15,7 @@ class AuthServiceProvider extends ServiceProvider * * @var array */ - protected $policies = [ - Project::class => ProjectPolicy::class, - Ticket::class => TicketPolicy::class - ]; + protected $policies = []; /** * Register any authentication / authorization services. diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 3bd3c81..cd8fc15 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Models\Project; +use App\Models\User; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Http\Request; @@ -47,6 +49,8 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace) ->group(base_path('routes/web.php')); }); + Route::model('user', User::class); + Route::model('project', Project::class); } /** diff --git a/app/Providers/TranslationServiceProvider.php b/app/Providers/TranslationServiceProvider.php new file mode 100644 index 0000000..a91772e --- /dev/null +++ b/app/Providers/TranslationServiceProvider.php @@ -0,0 +1,61 @@ +collectLocalesStrings() as $locale) { // suported locales + Cache::rememberForever(sprintf('translations.%s', $locale), function () use ($locale) { + $translations = [ + 'php' => $this->phpTranslations($locale), + 'json' => $this->jsonTranslations($locale), + ]; + return $translations; + }); + } + } + + private function collectLocalesStrings() + { + return collect(File::allFiles(resource_path('lang/')))->flatMap(function ($file) { + if ($file->getRelativePath()) { + return [$file->getRelativePath() => '']; + } + })->keys(); + } + + private function phpTranslations($locale) + { + $path = resource_path("lang/$locale"); + + return collect(File::allFiles($path))->flatMap(function ($file) use ($locale) { + $key = ($translation = $file->getBasename('.php')); + + return [$key => trans($translation, [], $locale)]; + }); + } + + private function jsonTranslations($locale) + { + $path = resource_path("lang/$locale.json"); + + if (is_string($path) && is_readable($path)) { + return json_decode(file_get_contents($path), true); + } + + return []; + } +} diff --git a/app/Traits/IsCrudModel.php b/app/Traits/IsCrudModel.php new file mode 100644 index 0000000..3dcf992 --- /dev/null +++ b/app/Traits/IsCrudModel.php @@ -0,0 +1,516 @@ + null, + 'placeholder' => '', + 'type' => null, + 'enum' => null, + 'nullable' => false, + 'readonly' => false, + 'disabled' => false, + 'required' => false, + 'validate' => [], + 'rules' => [], + 'actions' => [], + 'belongsTo' => null, + 'belongsToMany' => null, + 'getAttribute' => null, + 'setAttribute' => null + ]; + + // OVERLOAD MODEL FUNCTION + public function __get($key) + { + if ( + isset(static::$editableProperties[$key]['getAttribute']) and + is_callable(static::$editableProperties[$key]['getAttribute']) + ) { + return static::$editableProperties[$key]['getAttribute']($this); + } + return parent::__get($key); + } + + // OVERLOAD MODEL FUNCTION + public function __set($key, $value) + { + if ( + isset($this::$editableProperties[$key]['setAttribute']) and + is_callable($this::$editableProperties[$key]['setAttribute']) + ) { + return $this::$editableProperties[$key]['setAttribute']($this, $value); + } + return parent::__set($key, $value); + } + + // OVERLOAD MODEL FUNCTION + public function fill(array $attributes) + { + parent::fill($attributes); + foreach ($attributes as $k => $attribute) { + if ( + isset(static::$editableProperties[$k]['setAttribute']) and + is_callable(static::$editableProperties[$k]['setAttribute']) + ) { + $this->{$k} = $attribute; + } + } + } + + public function saveRelations(CrudRequest $request) + { + $validated = $request->validated(); + $props = self::getEditableProperties(); + foreach ($props as $k => $prop) { + if ($prop['belongsToMany'] and \is_array($validated[$k])) { + $this->{$k}()->sync($validated[$k]); + } + } + } + + /** + * Has this model Has many relations with delete restrict ? + * + * @return bool|string + */ + public function gotHasManyRelationWithRestrictOnDelete() + { + $sm = DB::getDoctrineSchemaManager(); + foreach (self::getRelationships() as $relation => $type) { + if ($type[0] === 'HasMany') { + $table = (new $type[1]())->getTable(); + /** @var \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey */ + foreach ($sm->listTableForeignKeys($table) as $foreignKey) { + // /vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php onEvent() + if ($foreignKey->onDelete() === 'CASCADE') { + // allow only CASCADE action, otherwise it would break the app + continue; + } + return class_basename($type[1]); + } + } + } + return false; + } + + /** + * Get the crud label + */ + public function getCrudLabelAttribute() + { + return $this->{$this->crudLabelColumn}; + } + + /** + * Get the crud value + */ + public function getCrudValueAttribute() + { + if (is_null($this->crudValueColumn)) { + throw new CrudException(sprintf( + '[\'belongsTo\'] model `%s` must have `crudValueColumn` property', + get_class($this), + CrudAction::class + )); + } + return $this->{$this->crudValueColumn}; + } + + /** + * Get the crud label column + */ + public function getCrudLabelColumnAttribute() + { + if (is_null($this->crudLabelColumn)) { + throw new CrudException(sprintf( + '[\'belongsTo\'] model `%s` must have `crudLabelColumn` property', + get_class($this), + CrudAction::class + )); + } + return $this->crudLabelColumn; + } + + /** + * Get the crud value column + */ + public function getCrudValueColumnAttribute() + { + return $this->crudValueColumn; + } + + /** + * Get the model validation rules + * + * @return array + */ + public static function getRules(): array + { + return collect(static::$editableProperties)->mapWithKeys(function ($item, $key) { + $item = \array_merge(self::$schemaCrud, $item); + $defaultRules = []; + if (!$item['nullable']) { + $defaultRules[] = 'required'; + } elseif ($item['required']) { + $defaultRules[] = 'required'; + } elseif ($item['readonly']) { + $defaultRules[] = 'prohibited'; + } + switch ($item['type']) { + case CrudType::boolean(): + $defaultRules[] = 'boolean'; + break; + case CrudType::email(): + $defaultRules[] = 'email:rfc,dns'; + break; + case CrudType::belongsTo(): + $defaultRules[] = 'integer'; + $defaultRules[] = 'min:0'; + break; + case CrudType::int(): + $defaultRules[] = 'integer'; + break; + case CrudType::belongsToMany(): + $defaultRules[] = 'array'; + break; + case CrudType::password(): + // phpcs:ignore Generic.Files.LineLength.TooLong + $defaultRules[] = "regex:/^(?=.*[A-Z])(?=.*[²&~\"#'\{\(\[\-|`_\\^@\)\°=+\}¨^\$£¤%?,;.:\/!§*<>])(?=.*[0-9].*)(?=.*[a-z]).{8,}$/"; + break; + case CrudType::text(): + case CrudType::string(): + default: + $defaultRules[] = 'string'; + break; + } + return [$key => array_merge($defaultRules, $item['validate'] ?? self::$schemaCrud['validate'])]; + })->all(); + } + + /** + * Get the model Name + * + * @return string + */ + public function getModelName(): string + { + return class_basename(get_class($this)); + } + + /** + * Get the Editable Properties + * + * @return array + */ + public static function getEditableProperties(): array + { + static::assertEditablePropertiesIsCorrect(); + return static::$editableProperties; + } + + /** + * Assert self::$editableProperties is in a correct format + * + * 'string' => CrudType::value() + * + * @return void + * @throws Exception + */ + private static function assertEditablePropertiesIsCorrect(): void + { + if (empty(static::$editableProperties)) { + throw new CrudException('$editableProperties must be defined in ' . __CLASS__); + } + foreach (static::$editableProperties as $name => &$prop) { + if (!is_string($name)) { + throw new CrudException(sprintf( + 'editableProperties array value must be an array given: %s', + print_r($prop, true) // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found + )); + } + if (!is_array($prop)) { + throw new CrudException(sprintf( + 'editableProperties array value must be an array given: %s', + print_r($prop, true) // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found + )); + } + $prop = array_merge_recursive_distinct(self::$schemaCrud, $prop); + + // Auto label + $prop['label'] = $prop['label'] ?? ucfirst($name); + self::assertLabelIscorrect($prop); + self::assertPlaceholderIscorrect($prop); + self::assertTypeIscorrect($prop); + switch ($prop['type']) { + // Email auto rule + case CrudType::email(): + $prop['validate'] = is_array($prop['validate']) ? $prop['validate'] : []; + if (!array_search('email:rfc,dns', $prop['validate'])) { + $prop['validate'][] = 'email:rfc,dns'; + } + break; + case CrudType::json(): + $prop['validate'] = is_array($prop['validate']) ? $prop['validate'] : []; + if (!array_search('json', $prop['validate'])) { + $prop['validate'][] = 'json'; + } + break; + } + self::assertNullableIscorrect($prop); + self::assertReadonlyIscorrect($prop); + self::assertDisabledIscorrect($prop); + self::assertValidateIscorrect($prop); + self::assertRulesArecorrect($prop); + self::assertActionsArecorrect($prop); + self::assertBelongsToIsCorrect($prop); + self::assertEnumIscorrect($prop); + self::assertGetAttributeIscorrect($prop); + self::assertSetAttributeIscorrect($prop); + self::assertBelongsToManyIsCorrect($prop); + } + } + + private static function assertLabelIscorrect(array $prop): void + { + if (!isset($prop['label']) or !is_string($prop['label'])) { + throw new CrudException('$editableProperties[\'label\'] array value must be a string'); + } + } + + private static function assertPlaceholderIscorrect(array $prop): void + { + if (!isset($prop['placeholder']) or !is_string($prop['placeholder'])) { + throw new CrudException('$editableProperties[\'placeholder\'] array value must be a string'); + } + } + + private static function assertTypeIscorrect(array $prop): void + { + if (!isset($prop['type']) or !is_object($prop['type']) or get_class($prop['type']) !== CrudType::class) { + throw new CrudException('$editableProperties[\'type\'] value must be a CrudType enum'); + } + } + + private static function assertNullableIscorrect(array $prop): void + { + if (!isset($prop['nullable']) or !is_bool($prop['nullable'])) { + throw new CrudException('$editableProperties[\'nullable\'] array value must be a boolean'); + } + } + + private static function assertReadonlyIscorrect(array $prop): void + { + if ( + !isset($prop['readonly']) or + (!is_bool($prop['readonly']) and !\is_callable($prop['readonly'])) + ) { + throw new CrudException('$editableProperties[\'readonly\'] array value must be a boolean or a function returnning a boolean'); + } + } + + private static function assertDisabledIscorrect(array $prop): void + { + if (!isset($prop['disabled']) or !is_bool($prop['disabled'])) { + throw new CrudException('$editableProperties[\'disabled\'] array value must be a boolean'); + } + } + + private static function assertRequiredIscorrect(array $prop): void + { + if (!isset($prop['required']) or !is_bool($prop['required'])) { + throw new CrudException('$editableProperties[\'required\'] array value must be a boolean'); + } + } + + private static function assertValidateIscorrect(array $prop): void + { + if (!isset($prop['validate']) or !\is_array($prop['validate'])) { + throw new CrudException('$editableProperties[\'validate\'] array value must be an array'); + } + } + + private static function assertGetAttributeIscorrect(array $prop): void + { + if (!is_null($prop['getAttribute']) and !is_callable($prop['getAttribute'])) { + throw new CrudException('$editableProperties[\'getAttribute\'] array value must be callable'); + } + } + + private static function assertSetAttributeIscorrect(array $prop): void + { + if (!is_null($prop['setAttribute']) and !is_callable($prop['setAttribute'])) { + throw new CrudException('$editableProperties[\'setAttribute\'] array value must be callable'); + } + } + + private static function assertRulesArecorrect(array $prop): void + { + $allowedRules = [ + 'min' => function ($v) { + return \is_numeric($v); + }, + 'max' => function ($v) { + return \is_numeric($v); + }, + 'step' => function ($v) { + return \is_numeric($v); + }, + ]; + if (!isset($prop['rules']) or !\is_array($prop['rules'])) { + throw new CrudException('$editableProperties[\'rules\'] array value must be an array'); + } + if (count(array_diff_key($prop['rules'], $allowedRules))) { + throw new CrudException(sprintf( + '$editableProperties[\'rules\'] array contains unallowed rule, allowed are : %s', + implode(',', array_keys($allowedRules)) + )); + } + foreach ($allowedRules as $k => $f) { + if (array_key_exists($k, $prop['rules']) and !$f($prop['rules'][$k])) { + throw new CrudException(sprintf( + '$editableProperties[\'rules\'] rule `%s` does not validate with value `%s`', + $k, + $prop['rules'][$k] + )); + } + } + } + + private static function assertActionsArecorrect(array $prop): void + { + if (!isset($prop['actions']) or !\is_array($prop['actions'])) { + throw new CrudException('$editableProperties[\'actions\'] array value must be an array'); + } + foreach ($prop['actions'] as $action) { + if (!is_object($action) or get_class($action) !== CrudAction::class) { + throw new CrudException(sprintf( + '$editableProperties[\'actions\'] array value must contain `%s` objects', + CrudAction::class + )); + } + } + } + + private static function assertEnumIscorrect(array $prop): void + { + if ( + $prop['type']->equals(CrudType::enum()) and (is_null($prop['enum']) or + !\is_subclass_of($prop['enum'], BaseEnum::class)) + ) { + throw new CrudException(sprintf( + '$editableProperties[\'enum\'] array value must be an enum `%s`', + BaseEnum::class + )); + } + } + + private static function assertBelongsToIsCorrect(array $prop): void + { + if (is_null($prop['belongsTo'])) { + return; + } + if ( + $prop['type']->equals(CrudType::belongsTo()) and + (!\is_string($prop['belongsTo']) or + !\class_exists($prop['belongsTo']) or + is_a($prop['belongsTo'], Model::class, true) or + !in_array(IsCrudModel::class, class_uses_recursive($prop['belongsTo']))) + ) { + throw new CrudException(sprintf( + '$editableProperties[\'belongsTo\'] array value must be a CrudModel given: `%s`', + $prop['belongsTo'] + )); + } + } + + private static function assertBelongsToManyIsCorrect(array $prop): void + { + if (is_null($prop['belongsToMany'])) { + return; + } + if ( + $prop['type']->equals(CrudType::belongsToMany()) and + (!\is_string($prop['belongsToMany']) or + !\class_exists($prop['belongsToMany']) or + is_a($prop['belongsToMany'], Model::class, true) or + !in_array(IsCrudModel::class, class_uses_recursive($prop['belongsToMany']))) + ) { + throw new CrudException(sprintf( + '$editableProperties[\'belongsToMany\'] array value must be a CrudModel given: `%s`', + $prop['belongsToMany'] + )); + } + if ( + $prop['type']->equals(CrudType::belongsToMany()) and + (!\is_string($prop['belongsToManyLabel'])) + ) { + throw new CrudException(sprintf( + '$editableProperties[\'belongsToManyLabel\'] must be string using belongsToMany, given: `%s`', + $prop['belongsToMany'] + )); + } + } + + /** + * Get eloquent relationships + * + * @url https://laracasts.com/discuss/channels/eloquent/get-all-model-relationships + * + * @return array + */ + private static function getRelationships() + { + $instance = new static(); + + // Get public methods declared without parameters and non inherited + $class = get_class($instance); + $allMethods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC); + $methods = array_filter( + $allMethods, + function ($method) use ($class) { + return $method->class === $class + && !$method->getParameters() // relationships have no parameters + && $method->getName() !== 'getRelationships' // prevent infinite recursion + && $method->getName() !== 'gotHasManyRelationWithRestrictOnDelete'; // prevent infinite recursion + } + ); + + DB::beginTransaction(); + + $relations = []; + foreach ($methods as $method) { + try { + $methodName = $method->getName(); + $methodReturn = $instance->$methodName(); + if (!$methodReturn instanceof Relation) { + continue; + } + } catch (\Throwable $th) { + continue; + } + + $type = (new \ReflectionClass($methodReturn))->getShortName(); + $model = get_class($methodReturn->getRelated()); + $relations[$methodName] = [$type, $model]; + } + + DB::rollBack(); + + return $relations; + } +} diff --git a/composer.json b/composer.json index 3f90a66..2e5e731 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "license": "MIT", "require": { "php": "^7.3|^8.0", + "doctrine/dbal": "^3.1", "fideloper/proxy": "^4.4", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", diff --git a/config/app.php b/config/app.php index 58b8ecc..bba609a5 100644 --- a/config/app.php +++ b/config/app.php @@ -174,6 +174,7 @@ return [ // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, + App\Providers\TranslationServiceProvider::class ], @@ -228,7 +229,10 @@ return [ 'View' => Illuminate\Support\Facades\View::class, 'TicketType' => App\Enums\TicketType::class, 'TicketStatus' => App\Enums\TicketStatus::class, - 'Utils' => App\Lib\Utils::class + 'Utils' => App\Lib\Utils::class, + 'CrudAction' => App\Enums\CrudAction::class, + 'CrudType' => App\Enums\CrudType::class, + 'Role' => App\Enums\Role::class, ], ]; diff --git a/database/migrations/2021_06_17_173912_create_project_user_table.php b/database/migrations/2021_06_17_173912_create_project_user_table.php new file mode 100644 index 0000000..88aa981 --- /dev/null +++ b/database/migrations/2021_06_17_173912_create_project_user_table.php @@ -0,0 +1,64 @@ +foreignId('project_id')->constrained('projects'); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->unique(['project_id', 'user_id']); + }); + /** @var \App\Models\User */ + foreach (User::all() as $user) { + if ($user->project_id) { + $uP = new ProjectUser(); + $uP->project_id = $user->project_id; + $uP->user_id = $user->id; + $uP->saveOrFail(); + } + } + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['project_id']); + $table->dropColumn('project_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->foreignId('project_id')->nullable() + ->after('role')->constrained('projects'); + }); + /** @var \App\Models\ProjectUser */ + foreach (ProjectUser::all() as $uP) { + try { + /** @var \App\Models\User */ + $user = User::whereId($uP->user_id)->firstOrFail(); + $user->project_id = $uP->project_id; + $user->saveOrFail(); + //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } catch (ModelNotFoundException $e) { + // ignore + } + } + Schema::dropIfExists('project_user'); + } +} diff --git a/package.json b/package.json index 4541492..d9270d2 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,18 @@ }, "devDependencies": { "axios": "^0.21.1", - "bootstrap": "^4.0.0", "cross-env": "^7.0", "fslightbox": "^3.2.3", "jquery": "^3.2", "laravel-mix": "^6.0.16", + "laravel-mix-polyfill": "^3.0.1", "lodash": "^4.17.19", - "popper.js": "^1.12", "resolve-url-loader": "^3.1.0", "sass": "^1.15.2", "sass-loader": "^8.0.0", + "vue": "^2.6.14", + "vue-loader": "^15.9.7", + "vue-select": "^3.11.2", "vue-template-compiler": "^2.6.12" } } diff --git a/resources/js/app.js b/resources/js/app.js index 40c55f6..267d60a 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,2 @@ require('./bootstrap'); +require('./crud'); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 08f1b20..fdb2696 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -7,10 +7,8 @@ window._ = require('lodash'); */ try { - window.Popper = require('popper.js').default; window.$ = window.jQuery = require('jquery'); - require('bootstrap'); require('fslightbox'); } catch (e) {} @@ -40,3 +38,34 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // cluster: process.env.MIX_PUSHER_APP_CLUSTER, // forceTLS: true // }); + +window.Vue = require('vue').default; +import vSelect from "vue-select" +Vue.component('v-select', vSelect) + +let vueOptions = { + el: "#app" +}; + +// register modules +const modules = require.context("./modules", true, /\.js$/i); +modules.keys().map(key => Vue.mixin(modules(key).default)); + +// Register components +const vues = require.context("./components", true, /\.vue$/i); +vues.keys().map(key => + Vue.component( + key + .split("/") + .pop() + .split(".")[0] + .replace(/\.?([A-Z])/g, function (x, y) { + return "-" + y.toLowerCase(); + }) + .replace(/^-/, "") + .replace("-component", ""), + vues(key).default + ) +); + +const app = new Vue(vueOptions); diff --git a/resources/js/components/belongsTo.vue b/resources/js/components/belongsTo.vue new file mode 100644 index 0000000..3a2653f --- /dev/null +++ b/resources/js/components/belongsTo.vue @@ -0,0 +1,112 @@ + + + diff --git a/resources/js/components/belongsToMany.vue b/resources/js/components/belongsToMany.vue new file mode 100644 index 0000000..2437db8 --- /dev/null +++ b/resources/js/components/belongsToMany.vue @@ -0,0 +1,108 @@ + + + diff --git a/resources/js/crud.js b/resources/js/crud.js new file mode 100644 index 0000000..7c19b0f --- /dev/null +++ b/resources/js/crud.js @@ -0,0 +1,19 @@ +window.__CRUD = { + confirmDelete: (e) => { + let self = e.target + e.preventDefault(); + Swal.fire({ + title: 'Confirmez', + text: 'Voulez-vous vraiment supprimer cet élément ?', + icon: 'question', + confirmButtonText: 'Supprimer', + showCancelButton: true, + cancelButtonText: 'Annuler', + confirmButtonColor: '#dc3545' + }).then((result) => { + if (result.isConfirmed) { + self.submit() + } + }) + } +} diff --git a/resources/js/modules/asset.js b/resources/js/modules/asset.js new file mode 100644 index 0000000..7dc439b --- /dev/null +++ b/resources/js/modules/asset.js @@ -0,0 +1,8 @@ +export default { + methods: { + asset(path) { + return window._asset + path; + } + }, +} + diff --git a/resources/js/modules/route.js b/resources/js/modules/route.js new file mode 100644 index 0000000..ef195db --- /dev/null +++ b/resources/js/modules/route.js @@ -0,0 +1,8 @@ +export default { + methods: { + route(routeName) { + return Object.keys(window._routes).indexOf(routeName) !== -1 ? window._routes[routeName] : null; + } + }, +} + diff --git a/resources/js/modules/trans.js b/resources/js/modules/trans.js new file mode 100644 index 0000000..522fb8b --- /dev/null +++ b/resources/js/modules/trans.js @@ -0,0 +1,41 @@ +export default { + data() { + return { + locale: window._locale + }; + }, + methods: { + /** + * Translate the given key. + */ + __(key, replace) { + let translation, translationNotFound = true, vars; + + try { + translation = key.split('.').reduce((t, i) => t[i] || null, window._translations.php) + if (translation) { + translationNotFound = false; + } + } catch (e) { + translation = key; + } + + if (translationNotFound) { + translation = window._translations['json'][key] ? window._translations['json'][key] : key; + } + vars = translation.match(/:[0-9A-Za-z_]*/g); + if (vars) { + for (let match of vars) { + try { + translation = translation.replace(new RegExp(match, 'g'), replace[match.substr(1)]); + } catch (e) { + console.error('String replace failed ', e); + } + } + } + + return translation + } + }, +} + diff --git a/resources/sass/_crud.scss b/resources/sass/_crud.scss new file mode 100644 index 0000000..7251816 --- /dev/null +++ b/resources/sass/_crud.scss @@ -0,0 +1,12 @@ +@import "vue-select/src/scss/vue-select.scss"; + +// readonly checkboxes +select[readonly],input[type='checkbox'][readonly]{ + pointer-events: none; +} + +//bootstrap overload +.btn-group > form:not(:first-child) > .btn-group:not(:last-child) > .btn, .btn-group > form:not(:first-child) > .btn:last-child:not(.dropdown-toggle) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} diff --git a/resources/sass/app.scss b/resources/sass/app.scss index fcf3e2d..3020e10 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -1,12 +1,10 @@ // Fonts @import url('https://fonts.googleapis.com/css?family=Nunito'); +@import 'crud'; // Variables @import 'variables'; -// Bootstrap -@import '~bootstrap/scss/bootstrap'; - a.col_sort { &:link,:visited,:hover,:active { text-decoration: none; diff --git a/resources/views/crud/create.blade.php b/resources/views/crud/create.blade.php new file mode 100644 index 0000000..ba950ff --- /dev/null +++ b/resources/views/crud/create.blade.php @@ -0,0 +1,53 @@ +@extends('layout.app') + +@section('title', sprintf('%s - Ajouter', Str::plural(class_basename($modelClass)))) +@section('description', sprintf('Ajouter un %s', class_basename($modelClass))) +@section('metaIndex', 'noindex,nofollow') + +@section('content') +

+
+
+
+ @include('modules.flashMessage') +
+ @can('viewAny', $modelClass) + {{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Ajouter') }} + @else + {{ Str::plural(class_basename($modelClass)) }} - {{ __('Ajouter') }} + @endcan +
+
+
+ @csrf + + + + + + + + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @php + // create form cannot accept readonly field + $prop['readonly'] = false; + @endphp + @if(in_array(CrudAction::create(), $prop['actions'])) + + @include('crud.formfield') + + @endif + @endforeach + +
{{ __('Propriété') }}{{ __('Valeur') }}
+
+ +
+
+
+
+
+
+
+@endsection diff --git a/resources/views/crud/edit.blade.php b/resources/views/crud/edit.blade.php new file mode 100644 index 0000000..1698c41 --- /dev/null +++ b/resources/views/crud/edit.blade.php @@ -0,0 +1,62 @@ +@extends('layout.app') + +@section('title', sprintf('%s - Editer', trans(Str::plural(class_basename($modelClass))))) +@section('description', sprintf('Editer un %s', trans(class_basename($modelClass)))) +@section('metaIndex', 'noindex,nofollow') + +@section('content') +
+
+
+
+ @include('modules.flashMessage') +
+ @can('viewAny', $modelClass) + {{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Editer') }} + @else + {{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Editer') }} + @endcan +
+ @can('view', $model) + {{ __('Voir') }} + @endcan + @can('delete', $model) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+
+
+ @method('put') + @csrf + + + + + + + + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if(in_array(CrudAction::update(), $prop['actions'])) + + @include('crud.formfield') + + @endif + @endforeach + +
{{ __('Propriété') }}{{ __('Valeur') }}
+
+ +
+
+
+
+
+
+
+@endsection diff --git a/resources/views/crud/formfield.blade.php b/resources/views/crud/formfield.blade.php new file mode 100644 index 0000000..5ec65d1 --- /dev/null +++ b/resources/views/crud/formfield.blade.php @@ -0,0 +1,38 @@ +@php +$readonly = is_callable($prop['readonly']) ? $prop['readonly']() : $prop['readonly']; +@endphp +@switch($prop['type']) + @case(CrudType::password()) + @include('crud.formfields.password') + @break + @case(CrudType::boolean()) + @include('crud.formfields.boolean') + @break + @case(CrudType::int()) + @include('crud.formfields.int') + @break + @case(CrudType::float()) + @include('crud.formfields.float') + @break + @case(CrudType::email()) + @include('crud.formfields.email') + @break + @case(CrudType::text()) + @include('crud.formfields.text') + @break + @case(CrudType::json()) + @include('crud.formfields.json') + @break + @case(CrudType::enum()) + @include('crud.formfields.enum') + @break + @case(CrudType::belongsTo()) + @include('crud.formfields.belongsTo') + @break + @case(CrudType::belongsToMany()) + @include('crud.formfields.belongsToMany') + @break + @case(CrudType::string()) + @default + @include('crud.formfields.string') +@endswitch diff --git a/resources/views/crud/formfields/belongsTo.blade.php b/resources/views/crud/formfields/belongsTo.blade.php new file mode 100644 index 0000000..eebc463 --- /dev/null +++ b/resources/views/crud/formfields/belongsTo.blade.php @@ -0,0 +1,30 @@ +@php + $related = isset($model) ? $props[$fieldName]['belongsTo']::find($model->{$fieldName}) : null; + if($related) { + $related = [ + 'label' => $related->crudLabel, + 'value' => $related->crudValue + ]; + } +@endphp + + + + + + + + +@push('scriptsConstants') + +@endpush diff --git a/resources/views/crud/formfields/belongsToMany.blade.php b/resources/views/crud/formfields/belongsToMany.blade.php new file mode 100644 index 0000000..20bd8f0 --- /dev/null +++ b/resources/views/crud/formfields/belongsToMany.blade.php @@ -0,0 +1,29 @@ +@php + $related = isset($model) ? $model->{$fieldName}->mapWithKeys(function ($model) use($prop) { + return [$model->{'id'} => [ + 'label' => $model->{$prop['belongsToManyLabel']}, + 'value' => $model->{'id'} + ]]; + }) : []; +@endphp + + + + + + + + +@push('scriptsConstants') + +@endpush diff --git a/resources/views/crud/formfields/boolean.blade.php b/resources/views/crud/formfields/boolean.blade.php new file mode 100644 index 0000000..69d83d4 --- /dev/null +++ b/resources/views/crud/formfields/boolean.blade.php @@ -0,0 +1,20 @@ + + + + + + diff --git a/resources/views/crud/formfields/email.blade.php b/resources/views/crud/formfields/email.blade.php new file mode 100644 index 0000000..a9e6fad --- /dev/null +++ b/resources/views/crud/formfields/email.blade.php @@ -0,0 +1,33 @@ + + + + + @if(strlen($prop['placeholder'])) +
+ + +
+ @else + + @endif + diff --git a/resources/views/crud/formfields/enum.blade.php b/resources/views/crud/formfields/enum.blade.php new file mode 100644 index 0000000..046ecd0 --- /dev/null +++ b/resources/views/crud/formfields/enum.blade.php @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/views/crud/formfields/float.blade.php b/resources/views/crud/formfields/float.blade.php new file mode 100644 index 0000000..ec3e719 --- /dev/null +++ b/resources/views/crud/formfields/float.blade.php @@ -0,0 +1,18 @@ + + + + + + diff --git a/resources/views/crud/formfields/int.blade.php b/resources/views/crud/formfields/int.blade.php new file mode 100644 index 0000000..ec3e719 --- /dev/null +++ b/resources/views/crud/formfields/int.blade.php @@ -0,0 +1,18 @@ + + + + + + diff --git a/resources/views/crud/formfields/json.blade.php b/resources/views/crud/formfields/json.blade.php new file mode 100644 index 0000000..0171ff2 --- /dev/null +++ b/resources/views/crud/formfields/json.blade.php @@ -0,0 +1,22 @@ + + + + +
+ +
+ +@push('scripts') + + +@endpush diff --git a/resources/views/crud/formfields/password.blade.php b/resources/views/crud/formfields/password.blade.php new file mode 100644 index 0000000..f9dacf1 --- /dev/null +++ b/resources/views/crud/formfields/password.blade.php @@ -0,0 +1,67 @@ + + + + + @if(strlen($prop['placeholder'])) +
+ + +
+ @else + + @endif + + + + + + + @if(strlen($prop['placeholder'])) +
+ + +
+ @else + + @endif + diff --git a/resources/views/crud/formfields/string.blade.php b/resources/views/crud/formfields/string.blade.php new file mode 100644 index 0000000..566e7a4 --- /dev/null +++ b/resources/views/crud/formfields/string.blade.php @@ -0,0 +1,33 @@ + + + + + @if(strlen($prop['placeholder'])) +
+ + +
+ @else + + @endif + diff --git a/resources/views/crud/formfields/text.blade.php b/resources/views/crud/formfields/text.blade.php new file mode 100644 index 0000000..2a3ea54 --- /dev/null +++ b/resources/views/crud/formfields/text.blade.php @@ -0,0 +1,22 @@ + + + + +
+ +
+ +@push('scripts') + + +@endpush diff --git a/resources/views/crud/index.blade.php b/resources/views/crud/index.blade.php new file mode 100644 index 0000000..3ae79c3 --- /dev/null +++ b/resources/views/crud/index.blade.php @@ -0,0 +1,111 @@ +@extends('layout.app') + +@section('title', sprintf('%s - Liste', trans(Str::plural(class_basename($modelClass))))) +@section('description', sprintf('Liste des %s', trans(Str::plural(class_basename($modelClass))))) +@section('metaIndex', 'noindex,nofollow') + +@section('content') +
+
+
+
+ @include('modules.flashMessage') +
+ {{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Liste') }} + @can('create', $modelClass) + {{ __('Ajouter') }} + @endcan +
+
+
+
+ {{ $models->links() }} +
+
+
+
+ + + + Réinitialiser + +
+
+
+
+ + + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if(in_array(CrudAction::viewAny(), $prop['actions'])) + @switch($prop['type']) + @case(CrudType::password()) + {{-- Ignore --}} + @break + @default + + @endswitch + @endif + @endforeach + + + + + @if(count($models)) + @foreach($models as $model) + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if(in_array(CrudAction::viewAny(), $prop['actions'])) + @switch($prop['type']) + @case(CrudType::password()) + {{-- Ignore --}} + @break + @default + + @endswitch + @endif + @endforeach + + + @endforeach + @else + + + + @endif + +
+ {{ $prop['label'] }} + @include('crud.modules.sorter', ['colname' => $fieldName]) +
@include('crud.printfield', [ + 'action' => CrudAction::viewAny(), + 'field' => $model->{$fieldName}, + 'props' => $prop + ]) +
+ @can('view', $model) + {{ __('Voir') }} + @endcan + @can('update', $model) + {{ __('Éditer') }} + @endcan + @can('delete', $model) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+ Rien à afficher +
+ {{ $models->links() }} +
+
+
+
+
+@endsection diff --git a/resources/views/crud/modules/sorter.blade.php b/resources/views/crud/modules/sorter.blade.php new file mode 100644 index 0000000..ff049a3 --- /dev/null +++ b/resources/views/crud/modules/sorter.blade.php @@ -0,0 +1,12 @@ +@php +$tableName = (new $modelClass())->getTable(); +$sortParams = ['sort_col' => $colname]; +$sortParams = request()->search ? + array_merge($sortParams, ['search' => request()->search]) : $sortParams; +@endphp +@if(!session("crud.$tableName.sort_way") or session("crud.$tableName.sort_col") !== $colname or (session("crud.$tableName.sort_way") === 'desc' and session("crud.$tableName.sort_col") === $colname)) + 'asc'])) }}"> ↓  +@endif +@if(!session("crud.$tableName.sort_way") or session("crud.$tableName.sort_col") !== $colname or (session("crud.$tableName.sort_way") === 'asc' and session("crud.$tableName.sort_col") === $colname)) + 'desc'])) }}"> ↑  +@endif diff --git a/resources/views/crud/printfield.blade.php b/resources/views/crud/printfield.blade.php new file mode 100644 index 0000000..0975095 --- /dev/null +++ b/resources/views/crud/printfield.blade.php @@ -0,0 +1,48 @@ +@switch($action) + @case(CrudAction::viewAny()) + @switch($props['type']) + @case(CrudType::email()) + {{ $field }} + @break + @case(CrudType::enum()) + {{ $model->{$fieldName}->label }} + @break + @case(CrudType::belongsTo()) + {{ $prop['belongsTo']::find($model->{$fieldName})->{$model->crudLabelColumn} }} + @break + @case(CrudType::belongsToMany()) + {{ $model->{$fieldName}->implode($prop['belongsToManyLabel'], ',') }} + @break + @case(CrudType::boolean()) + @if($model->{$fieldName})OUI @else NON @endif + @break + @default + {{ $field }} + @endswitch + @break + + @case(CrudAction::view()) + @switch($props['type']) + @case(CrudType::email()) + {{ $field }} + @break + @case(CrudType::enum()) + {{ $model->{$fieldName}->label }} + @break + @case(CrudType::boolean()) + @if($model->{$fieldName})OUI @else NON @endif + @break + @case(CrudType::belongsTo()) + {{ $prop['belongsTo']::find($model->{$fieldName})->{$model->crudLabelColumn} }} + @break + @case(CrudType::belongsToMany()) + {{ $model->{$fieldName}->implode($prop['belongsToManyLabel'], ',') }} + @break + @default + {{ $field }} + @endswitch + @break + + @default + @dd('unknown action') +@endswitch diff --git a/resources/views/crud/show.blade.php b/resources/views/crud/show.blade.php new file mode 100644 index 0000000..38490dc --- /dev/null +++ b/resources/views/crud/show.blade.php @@ -0,0 +1,66 @@ +@extends('layout.app') + +@section('title', sprintf('%s - Voir', trans(Str::plural(class_basename($modelClass))))) +@section('description', sprintf('Voir un %s', trans(class_basename($modelClass)))) +@section('metaIndex', 'noindex,nofollow') + +@section('content') +
+
+
+
+ @include('modules.flashMessage') +
+ @can('viewAny', $modelClass) + {{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Voir') }} + @else + {{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Voir') }} + @endcan +
+ @can('update', $model) + {{ __('Modifier') }} + @endcan + @can('delete', $model) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+
+
+ + + + + + + + + @foreach ($modelClass::getEditableProperties() as $fieldName => $prop) + @if(in_array(CrudAction::view(), $prop['actions'])) + @switch($prop['type']) + @case(CrudType::password()) + {{-- Ignore --}} + @break + @default + + + + + @endswitch + @endif + @endforeach + +
{{ __('Propriété') }}{{ __('Valeur') }}
{{ $prop['label'] }}@include('crud.printfield', [ + 'action' => CrudAction::view(), + 'field' => $model->{$fieldName}, + 'props' => $prop + ])
+
+
+
+
+
+@endsection diff --git a/resources/views/layout/app.blade.php b/resources/views/layout/app.blade.php index 045a890..b3ea3c2 100644 --- a/resources/views/layout/app.blade.php +++ b/resources/views/layout/app.blade.php @@ -4,9 +4,13 @@ Support LDCWEB + + + - + + -
+
@yield('content')
+ + @stack('scriptsConstants') + @stack('scripts') diff --git a/resources/views/modules/flashMessage.blade.php b/resources/views/modules/flashMessage.blade.php new file mode 100644 index 0000000..919668a --- /dev/null +++ b/resources/views/modules/flashMessage.blade.php @@ -0,0 +1,44 @@ +@if ($message = Session::get('success')) +
+ + {!! nl2br(e($message)) !!} +
+@endif + + +@if ($message = Session::get('error')) +
+ + {!! nl2br(e($message)) !!} +
+@endif + + +@if ($message = Session::get('warning')) +
+ + {!! nl2br(e($message)) !!} +
+@endif + + +@if ($message = Session::get('info')) +
+ + {!! nl2br(e($message)) !!} +
+@endif + + +@if ($errors->any()) +
+ + @if ($errors->any()) +
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+ @endif +
+@endif diff --git a/resources/views/modules/sorter.blade.php b/resources/views/modules/sorter.blade.php index 1f09fbe..b7ae505 100644 --- a/resources/views/modules/sorter.blade.php +++ b/resources/views/modules/sorter.blade.php @@ -1,6 +1,6 @@ @if(!session('tickets.sort_way') or session('tickets.sort_col') !== $colname or (session('tickets.sort_way') === 'desc' and session('tickets.sort_col') === $colname)) - ↓  + ↓  @endif @if(!session('tickets.sort_way') or session('tickets.sort_col') !== $colname or (session('tickets.sort_way') === 'asc' and session('tickets.sort_col') === $colname)) - ↑  + ↑  @endif diff --git a/resources/views/project/create.blade.php b/resources/views/project/create.blade.php deleted file mode 100644 index 77c2a14..0000000 --- a/resources/views/project/create.blade.php +++ /dev/null @@ -1,26 +0,0 @@ -@extends('layout.app') - -@section('project', 'active') - -@section('content') -
-
-
-
-
Créer un projet
-
-
- @csrf - - @include('project.form-inputs') - -
- -
-
-
-
-
-
-
-@endsection diff --git a/resources/views/project/edit.blade.php b/resources/views/project/edit.blade.php deleted file mode 100644 index d017170..0000000 --- a/resources/views/project/edit.blade.php +++ /dev/null @@ -1,32 +0,0 @@ -@extends('layout.app') - -@section('project', 'active') - -@section('content') -
-
-
-
-
Modifier un projetRetour
-
-
- @csrf - @method('PUT') - @if (session('success')) -
- {!! session('success') !!} -
- @endif - - @include('project.form-inputs') - -
- -
-
-
-
-
-
-
-@endsection diff --git a/resources/views/project/form-inputs.blade.php b/resources/views/project/form-inputs.blade.php deleted file mode 100644 index 4910d21..0000000 --- a/resources/views/project/form-inputs.blade.php +++ /dev/null @@ -1,4 +0,0 @@ -@include('modules.error-messages') -
- -
diff --git a/resources/views/project/index.blade.php b/resources/views/project/index.blade.php deleted file mode 100644 index 22326a9..0000000 --- a/resources/views/project/index.blade.php +++ /dev/null @@ -1,32 +0,0 @@ -@extends('layout.app') - -@section('project', 'active') - -@section('content') -
- @include('modules.error-messages') -

Gestion des projetsCréer un projet

- - - - - - - - @foreach ($projects as $project) - - - - - - @endforeach - -
Projet
{{ $project->name }}Modifier -
- @method('DELETE') - @csrf - -
-
-
-@endsection diff --git a/resources/views/ticket/create.blade.php b/resources/views/ticket/create.blade.php index 5acbd26..cc643e6 100644 --- a/resources/views/ticket/create.blade.php +++ b/resources/views/ticket/create.blade.php @@ -1,13 +1,11 @@ @extends('layout.app') -@section('tickets', 'active') - @section('content')
-
Créer un ticketRetour
+
Créer un ticketRetour
@csrf diff --git a/resources/views/ticket/edit.blade.php b/resources/views/ticket/edit.blade.php index df0a330..19bed6f 100644 --- a/resources/views/ticket/edit.blade.php +++ b/resources/views/ticket/edit.blade.php @@ -1,13 +1,11 @@ @extends('layout.app') -@section('tickets', 'active') - @section('content')
-
Modifier un ticketRetour
+
Modifier un ticketRetour
@csrf diff --git a/resources/views/ticket/form-inputs.blade.php b/resources/views/ticket/form-inputs.blade.php index 0e9b04f..390a639 100644 --- a/resources/views/ticket/form-inputs.blade.php +++ b/resources/views/ticket/form-inputs.blade.php @@ -3,9 +3,9 @@ @can('changeStatus', App\Model\Ticket::class)
- role->equals(Role::admin())) disabled @endif> @foreach($projects as $project) - @if (Auth::user()->role === 1) + @if (Auth::user()->role->equals(Role::admin())) @elseif ($project->id === Auth::user()->project_id) @@ -16,7 +16,7 @@ @endcan
- @foreach(TicketType::toArray() as $ticketType) @endforeach @@ -25,7 +25,7 @@ @can('changeStatus', App\Model\Ticket::class)
- @foreach(TicketStatus::toArray() as $ticketStatus) @endforeach @@ -34,12 +34,12 @@ @endcan
-
+
- +
- -
+

Fichier joint 1

+
@if (isset($ticket) and $ticket->file !== null)
@@ -48,7 +48,7 @@
@endif
- +

Fichier joint 2

@if (isset($ticket) and $ticket->file2 !== null) diff --git a/resources/views/ticket/index.blade.php b/resources/views/ticket/index.blade.php index 48d20af..1756513 100644 --- a/resources/views/ticket/index.blade.php +++ b/resources/views/ticket/index.blade.php @@ -1,28 +1,45 @@ @extends('layout.app') -@section('tickets', 'active') - @section('content')

Gestion des tickets @if(request()->status) (Archivés) @endif - Créer un ticket - @if(request()->status) - Tickets en cours - @else - Tickets Archivés - @endif - Réinitialiser le tri - @if(auth()->user()->role === 1) -
- +
+
+
+ + @if(auth()->user()->role->equals(Role::admin())) +
+ +
+ @endif +
+
+
+
+ +
+ + + +
+ +
+
- @endif

@@ -32,27 +49,27 @@ @include('modules.sorter', ['colname' => 'id']) - - + + - - @if (Auth::user()->role === 1) - + + @if (Auth::user()->role->equals(Role::admin())) + @endif @@ -65,9 +82,9 @@ - @if (Auth::user()->role->equals(Role::admin())) - @endif + @if(!count($tickets)) + + + + @endif @foreach ($tickets as $ticket) - @@ -115,7 +115,7 @@ diff --git a/resources/views/ticket/show.blade.php b/resources/views/ticket/show.blade.php index 23fad03..8de3b1e 100644 --- a/resources/views/ticket/show.blade.php +++ b/resources/views/ticket/show.blade.php @@ -103,7 +103,7 @@
{!! Utils::plainToHtml($message->message) !!} @if ($message->file !== null) -
Ouvrir la pièce jointe
+
Ouvrir la pièce jointe
@endif

Le {{ \Carbon\Carbon::parse($message->created_at)->format('d/m/Y à H:i')}} par {{ $message->user->name }}
@@ -111,7 +111,7 @@
{!! Utils::plainToHtml($message->message) !!} @if ($message->file !== null) -
Ouvrir la pièce jointe
+
Ouvrir la pièce jointe
@endif

Le {{ \Carbon\Carbon::parse($message->created_at)->format('d/m/Y à H:i')}} par {{ $message->user->name }}
@@ -132,13 +132,13 @@ @if ($ticket->file !== null)
  • Pièce jointe n°1 : - file))data-fslightbox @endif href="{{ asset($ticket->file) }}" target="_blank">Ouvrir la pièce jointe + file))data-fslightbox="gallery" @endif href="{{ asset($ticket->file) }}" target="_blank">Ouvrir la pièce jointe
  • @endif @if ($ticket->file2 !== null)
  • Pièce jointe n°2 : - file))data-fslightbox @endif href="{{ asset($ticket->file2) }}" target="_blank">Ouvrir la pièce jointe + file))data-fslightbox="gallery" @endif href="{{ asset($ticket->file2) }}" target="_blank">Ouvrir la pièce jointe
  • @endif
    - Projet  + {{__('Projet') }}  @include('modules.sorter', ['colname' => 'project']) - Type  + {{__('Type') }}  @include('modules.sorter', ['colname' => 'type'])FichierFichier 2{{__('Fichier') }}{{__('Fichier 2') }} - Date de création  + {{__('Date de création') }}  @include('modules.sorter', ['colname' => 'created_at']) - Créé par  + {{__('Créé par') }}  @include('modules.sorter', ['colname' => 'created_by']) - Statut  + {{__('Statut') }}  @include('modules.sorter', ['colname' => 'status']) ContenuActions{{__('Contenu') }}{{__('Actions') }}
    {{ $ticket->project->name }} @if ($ticket->type->equals(TicketType::bug())) - {{ $ticket->type->label }} + {{ $ticket->type->label }} @else - {{ $ticket->type->label }} + {{ $ticket->type->label }} @endif @@ -95,7 +112,7 @@ {{-- Modal du ticket --}} - @@ -105,11 +122,9 @@ {{__('Contenu') }}{{__('Actions') }}

    {{ __('Aucun ticket') }}

    + {{ $ticket->id }} - + {{ $ticket->project->name }} @if ($ticket->type->equals(TicketType::bug())) @@ -113,7 +116,7 @@ {{-- Modal du ticket --}} @@ -130,23 +133,23 @@

    {!! nl2br(e($ticket->text)) !!}

    - {{ __('Messages') }} + {{ __('Messages') }} @can('update', $ticket) - Modifier + {{ __('Modifier') }} @endcan @can('delete', $ticket) @if(!$ticket->status->equals(TicketStatus::archived()))
    @method('PUT') @csrf - +
    @endif @endcan From 08b8da431a29f1d99bee4721dcf50c5efa948f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Fri, 18 Jun 2021 16:35:37 +0200 Subject: [PATCH 5/6] Heads Up new theme --- resources/views/layout/app.blade.php | 4 ++-- resources/views/ticket/index.blade.php | 10 +++++----- resources/views/ticket/show.blade.php | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/views/layout/app.blade.php b/resources/views/layout/app.blade.php index b3ea3c2..5f9240b 100644 --- a/resources/views/layout/app.blade.php +++ b/resources/views/layout/app.blade.php @@ -9,7 +9,7 @@ - + @@ -99,7 +99,7 @@
    @yield('content')
    - + @stack('scriptsConstants') @stack('scripts') diff --git a/resources/views/ticket/index.blade.php b/resources/views/ticket/index.blade.php index 4c557aa..5e5f196 100644 --- a/resources/views/ticket/index.blade.php +++ b/resources/views/ticket/index.blade.php @@ -34,7 +34,7 @@
    - +
    @@ -92,12 +92,12 @@
    @if ($ticket->file !== null) - Fichier + Fichier @endif @if ($ticket->file2 !== null) - Fichier 2 + Fichier 2 @endif {{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }} {{-- Modal du ticket --}} - @@ -133,7 +133,7 @@

    {!! nl2br(e($ticket->text)) !!}

    diff --git a/resources/views/ticket/show.blade.php b/resources/views/ticket/show.blade.php index b7f3f85..23fad03 100644 --- a/resources/views/ticket/show.blade.php +++ b/resources/views/ticket/show.blade.php @@ -84,7 +84,7 @@ From f825ce8a9c8b037450af35678666bf7310b581c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Fri, 18 Jun 2021 16:52:57 +0200 Subject: [PATCH 6/6] - Set jquery from cdn - fix fslightbox - fslightbox gallery mode --- app/Lib/Utils.php | 4 ++-- package.json | 1 - resources/js/bootstrap.js | 8 ++------ resources/views/layout/app.blade.php | 1 + resources/views/ticket/index.blade.php | 4 ++-- resources/views/ticket/show.blade.php | 8 ++++---- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/app/Lib/Utils.php b/app/Lib/Utils.php index 061afa8..d2f3bf7 100644 --- a/app/Lib/Utils.php +++ b/app/Lib/Utils.php @@ -24,7 +24,7 @@ class Utils if (\strpos($url, 'http') === false and \strpos($url, 'https') === false) { $url = sprintf('http://%s', $url); } - $fsligthbox = self::isImageFile($url) ? 'data-fslightbox' : ''; + $fsligthbox = self::isImageFile($url) ? 'data-fslightbox="gallery"' : ''; return strlen($match) ? "$match" : $match; } }, @@ -36,6 +36,6 @@ class Utils public static function isImageFile(string $url): bool { $m = []; - return \preg_match('/(.png|.jpg|.jpeg|.gif)$/', $url, $m) === 1; + return \preg_match('/(\.png|\.jpg|\.jpeg|\.gif)$/', $url, $m) === 1; } } diff --git a/package.json b/package.json index d9270d2..7a09662 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "axios": "^0.21.1", "cross-env": "^7.0", "fslightbox": "^3.2.3", - "jquery": "^3.2", "laravel-mix": "^6.0.16", "laravel-mix-polyfill": "^3.0.1", "lodash": "^4.17.19", diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index fdb2696..3997bc7 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -6,12 +6,6 @@ window._ = require('lodash'); * code may be modified to fit the specific needs of your application. */ -try { - window.$ = window.jQuery = require('jquery'); - - require('fslightbox'); -} catch (e) {} - /** * We'll load the axios HTTP library which allows us to easily issue requests * to our Laravel back-end. This library automatically handles sending the @@ -69,3 +63,5 @@ vues.keys().map(key => ); const app = new Vue(vueOptions); + +require('fslightbox'); diff --git a/resources/views/layout/app.blade.php b/resources/views/layout/app.blade.php index 5f9240b..e611d89 100644 --- a/resources/views/layout/app.blade.php +++ b/resources/views/layout/app.blade.php @@ -99,6 +99,7 @@
    @yield('content')
    + @stack('scriptsConstants') diff --git a/resources/views/ticket/index.blade.php b/resources/views/ticket/index.blade.php index 5e5f196..ecbe38a 100644 --- a/resources/views/ticket/index.blade.php +++ b/resources/views/ticket/index.blade.php @@ -92,12 +92,12 @@
    @if ($ticket->file !== null) - Fichier + Fichier @endif @if ($ticket->file2 !== null) - Fichier 2 + Fichier 2 @endif {{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}