diff --git a/.agents/skills/project-request-validation-style/SKILL.md b/.agents/skills/project-request-validation-style/SKILL.md new file mode 100644 index 0000000..69747cb --- /dev/null +++ b/.agents/skills/project-request-validation-style/SKILL.md @@ -0,0 +1,28 @@ +--- +name: project-request-validation-style +description: "Always apply in this project when changing Laravel controllers or FormRequest classes. Enforces dedicated FormRequest usage for controller validation, validated-array access via Arr::get(), and forbids custom single-field getter methods or request helper extraction like boolean()/integer()/string() for app input handling." +--- + +# Project Request Validation Style + +## When to Apply + +Activate this skill whenever work in this project changes: + +- Laravel controllers +- Laravel FormRequest classes +- Validation flow between controllers and requests + +## Rules + +- Do not add inline controller validation with `$request->validate(...)` when the endpoint has request-specific validation needs. +- Prefer dedicated `FormRequest` classes and use `$validated = $request->validated();` in controllers. +- When reading validated arrays, use `Arr::get($validated, 'key')` instead of direct array access. +- Do not use request helper extraction like `$request->boolean()`, `$request->integer()`, `$request->string()`, or similar helpers for application input handling. +- Do not add custom FormRequest getter helpers such as `points()`, `branchName()`, `ticketAttributes()`, or similar single-purpose accessors. +- Build controller payloads from the validated array directly with `Arr::get(...)`. + +## Notes + +- `Arr::get(...)` already returns `null` by default, so do not wrap it in redundant existence checks when `null` is acceptable. +- Keep validation logic in the FormRequest and payload shaping in the controller or dedicated action. diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 5fa0fb4..10ed4fa 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Actions\ProjectPermissions\ProjectPermissionService; use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata; +use App\Http\Requests\DeleteProjectRequest; use App\Http\Requests\StoreProjectRequest; use App\Http\Requests\SyncProjectPermissionsRequest; use App\Models\Project; @@ -44,6 +45,7 @@ class ProjectController extends Controller return Inertia::render('projects/Index', [ 'projects' => $projectQuery ->get(), + 'status' => session('status'), ]); } @@ -76,6 +78,14 @@ class ProjectController extends Controller ]); } + public function general(Request $request, Project $project): Response + { + return Inertia::render('projects/settings/General', [ + 'project' => $project, + 'status' => session('status'), + ]); + } + public function access(Request $request, Project $project): Response { $user = $request->user(); @@ -146,4 +156,16 @@ class ProjectController extends Controller return to_route('projects.access', $project) ->with('status', "{$team->name} permissions were updated."); } + + public function destroy(DeleteProjectRequest $request, Project $project): RedirectResponse + { + $request->validated(); + + $projectName = $project->name; + + $project->delete(); + + return to_route('projects.index') + ->with('status', "Project {$projectName} was deleted."); + } } diff --git a/app/Http/Controllers/ProjectTicketController.php b/app/Http/Controllers/ProjectTicketController.php index 30105cc..ab6a878 100644 --- a/app/Http/Controllers/ProjectTicketController.php +++ b/app/Http/Controllers/ProjectTicketController.php @@ -109,7 +109,16 @@ class ProjectTicketController extends Controller $validated = $request->validated(); $fieldValues = Arr::get($validated, 'field_values'); - $ticketAttributes = $request->ticketAttributes(); + $ticketAttributes = [ + 'acceptance_criteria' => Arr::get($validated, 'acceptance_criteria'), + 'blocked_comment' => Arr::get($validated, 'blocked_comment'), + 'description' => Arr::get($validated, 'description'), + 'is_blocked' => Arr::get($validated, 'is_blocked', false), + 'parent_ticket_id' => Arr::get($validated, 'parent_ticket_id'), + 'ticket_priority_id' => Arr::get($validated, 'ticket_priority_id'), + 'ticket_type_id' => Arr::get($validated, 'ticket_type_id'), + 'title' => Arr::get($validated, 'title'), + ]; $wasUpdated = $this->updateProjectTicket->handle( $ticket, diff --git a/app/Http/Requests/DeleteProjectRequest.php b/app/Http/Requests/DeleteProjectRequest.php new file mode 100644 index 0000000..509d917 --- /dev/null +++ b/app/Http/Requests/DeleteProjectRequest.php @@ -0,0 +1,43 @@ +user() !== null; + } + + /** + * @return array|string> + */ + public function rules(): array + { + /** @var Project $project */ + $project = $this->route('project'); + + return [ + 'confirmation_key' => [ + 'required', + 'string', + Rule::in([$project->key]), + ], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'confirmation_key.in' => 'Enter the exact project key to confirm deletion.', + ]; + } +} diff --git a/app/Http/Requests/StoreProjectRequest.php b/app/Http/Requests/StoreProjectRequest.php index 1739dba..479af80 100644 --- a/app/Http/Requests/StoreProjectRequest.php +++ b/app/Http/Requests/StoreProjectRequest.php @@ -4,6 +4,7 @@ namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Arr; use Illuminate\Validation\Rule; class StoreProjectRequest extends FormRequest @@ -18,9 +19,11 @@ class StoreProjectRequest extends FormRequest protected function prepareForValidation(): void { - if ($this->has('key')) { + $key = Arr::get($this->all(), 'key'); + + if ($key !== null) { $this->merge([ - 'key' => strtoupper((string) $this->string('key')->trim()), + 'key' => strtoupper(trim((string) $key)), ]); } } diff --git a/app/Http/Requests/UpdateProjectTicketRequest.php b/app/Http/Requests/UpdateProjectTicketRequest.php index 02191cd..1072471 100644 --- a/app/Http/Requests/UpdateProjectTicketRequest.php +++ b/app/Http/Requests/UpdateProjectTicketRequest.php @@ -71,14 +71,16 @@ class UpdateProjectTicketRequest extends FormRequest { return [ function (Validator $validator): void { + /** @var array $validated */ + $validated = $this->validated(); + /** @var Project $project */ $project = $this->route('project'); /** @var Ticket $ticket */ $ticket = $this->route('ticket'); - $parentTicketId = $this->input('parent_ticket_id'); - $parentTicketId = is_numeric($parentTicketId) ? (int) $parentTicketId : null; + $parentTicketId = Arr::get($validated, 'parent_ticket_id'); if ($parentTicketId !== null) { if ($parentTicketId === $ticket->id) { @@ -105,12 +107,10 @@ class UpdateProjectTicketRequest extends FormRequest } } - $requestedTicketTypeId = $this->input('ticket_type_id'); - $requestedTicketType = is_numeric($requestedTicketTypeId) - ? TicketType::query() - ->where('project_id', $project->id) - ->find((int) $requestedTicketTypeId) - : null; + $requestedTicketTypeId = Arr::get($validated, 'ticket_type_id'); + $requestedTicketType = TicketType::query() + ->where('project_id', $project->id) + ->find($requestedTicketTypeId); if ( $requestedTicketType?->system_key !== 'parent' @@ -119,61 +119,13 @@ class UpdateProjectTicketRequest extends FormRequest $validator->errors()->add('ticket_type_id', 'Tickets with child tickets must keep a Parent type.'); } - if ($this->boolean('is_blocked') && blank($this->input('blocked_comment'))) { + if (Arr::get($validated, 'is_blocked', false) && blank(Arr::get($validated, 'blocked_comment'))) { $validator->errors()->add('blocked_comment', 'Blocked tickets require a blocker comment.'); } }, ]; } - /** - * @return array{ - * acceptance_criteria: string|null, - * blocked_comment: string|null, - * description: string|null, - * is_blocked: bool, - * parent_ticket_id: int|null, - * ticket_priority_id: int, - * ticket_type_id: int, - * title: string - * } - */ - public function ticketAttributes(): array - { - /** @var array{ - * acceptance_criteria: string|null, - * blocked_comment: string|null, - * description: string|null, - * is_blocked: bool, - * parent_ticket_id: int|null, - * ticket_priority_id: int, - * ticket_type_id: int, - * title: string - * } $attributes - */ - $attributes = array_merge( - [ - 'acceptance_criteria' => null, - 'blocked_comment' => null, - 'description' => null, - 'is_blocked' => false, - 'parent_ticket_id' => null, - ], - Arr::only($this->validated(), [ - 'acceptance_criteria', - 'blocked_comment', - 'description', - 'is_blocked', - 'parent_ticket_id', - 'ticket_priority_id', - 'ticket_type_id', - 'title', - ]), - ); - - return $attributes; - } - private function createsCycle(Ticket $ticket, Ticket $candidateParent): bool { $currentParent = $candidateParent; diff --git a/docs/missing-features.md b/docs/missing-features.md index 31b80db..3464d57 100644 --- a/docs/missing-features.md +++ b/docs/missing-features.md @@ -3,29 +3,11 @@ ### Ticket - general: - - new skill, user resources - - ueberall $array['''] ersetzen durch Arr::get() - (mehr über middlewares steuern) - why uses everywhere forceFill? - - $request->validated('xxx') ersetzten durch: - - ``` $validated = $request->validate(); $xxx = Arr:get($validated, 'xxx'); ``` - - oder: ```$request->position()```, methoden entfernen, wie: - ``` - public function position(): ?int { - position = this->validated('position'); - - if ($position === null) { - return null; - } - - return (int) $position; - } - ``` - app/Http/Requests/UpdateProjectTicketRequest.php and other ProjectTicket only to Ticket... - mehrere actions/quereys in services auslagern -- 02: - - Delete Projekt as Admin - 03 - permission models? eg.g syncUserPermissions uses only db table - mit morph auf perm user (z.b. team oder user) @@ -70,4 +52,4 @@ - Git listBranches replace baseUrl, owner, rperoName with GItReprository - Git prs replace baseUrl, owner, rperoName with GItReprository - git php docu replace @ reutrn with data models - - replace Kanbangit with env App Name \ No newline at end of file + - replace Kanbangit with env App Name diff --git a/resources/js/pages/projects/Access.vue b/resources/js/pages/projects/Access.vue index 18e675a..69864fc 100644 --- a/resources/js/pages/projects/Access.vue +++ b/resources/js/pages/projects/Access.vue @@ -13,6 +13,7 @@ import { } from '@/components/ui/card'; import AppLayout from '@/layouts/AppLayout.vue'; import { access, index, show } from '@/routes/projects/index'; +import { general as generalSettings } from '@/routes/projects/settings'; import type { BreadcrumbItem, ProjectAccessPageProps } from '@/types'; const props = defineProps(); @@ -57,9 +58,21 @@ const hasPermission = ( /> - +
+ + + +
+
+ {{ status }} +
+
+ +
+ +
+ +
+ +