abortUnlessCanManage($request, $project); $this->initializeProjectTicketMetadata->handle($project); return Inertia::render('projects/settings/Metadata', [ 'fieldTypeOptions' => collect(TicketFieldDefinition::FIELD_TYPES) ->map(fn (string $fieldType): array => [ 'label' => ucfirst($fieldType), 'value' => $fieldType, ]) ->all(), 'project' => $project, 'status' => session('status'), 'ticketFieldDefinitions' => $project->ticketFieldDefinitions()->get(), 'ticketPriorities' => $project->ticketPriorities()->get(), 'ticketTypes' => $project->ticketTypes()->get(), ]); } public function storeType(StoreProjectNamedSettingRequest $request, Project $project): RedirectResponse { $this->abortUnlessCanManage($request, $project); $this->initializeProjectTicketMetadata->handle($project); $ticketType = DB::transaction(function () use ($project, $request): TicketType { $ticketType = $project->ticketTypes()->create([ 'is_builtin' => false, 'name' => $request->validated('name'), 'position' => $project->ticketTypes()->count() + 1, 'system_key' => null, ]); $this->reorderModels( $project->ticketTypes()->whereKeyNot($ticketType->id)->get()->values(), $ticketType, $request->position() ?? $project->ticketTypes()->count(), ); return $ticketType; }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketType->name} type was added."); } public function updateType( UpdateProjectNamedSettingRequest $request, Project $project, TicketType $ticketType, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); $this->initializeProjectTicketMetadata->handle($project); DB::transaction(function () use ($project, $request, $ticketType): void { $ticketType->update([ 'name' => $request->validated('name'), ]); $this->reorderModels( $project->ticketTypes()->whereKeyNot($ticketType->id)->get()->values(), $ticketType, $request->position(), ); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketType->fresh()->name} type was updated."); } public function destroyType(Request $request, Project $project, TicketType $ticketType): RedirectResponse { $this->abortUnlessCanManage($request, $project); abort_if($ticketType->is_builtin, 403); $deletedName = $ticketType->name; DB::transaction(function () use ($project, $ticketType): void { $ticketType->delete(); $this->persistOrderedModels($project->ticketTypes()->get()->values()); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$deletedName} type was removed."); } public function storePriority(StoreProjectNamedSettingRequest $request, Project $project): RedirectResponse { $this->abortUnlessCanManage($request, $project); $ticketPriority = DB::transaction(function () use ($project, $request): TicketPriority { $ticketPriority = $project->ticketPriorities()->create([ 'name' => $request->validated('name'), 'position' => $project->ticketPriorities()->count() + 1, ]); $this->reorderModels( $project->ticketPriorities()->whereKeyNot($ticketPriority->id)->get()->values(), $ticketPriority, $request->position() ?? $project->ticketPriorities()->count(), ); return $ticketPriority; }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketPriority->name} priority was added."); } public function updatePriority( UpdateProjectNamedSettingRequest $request, Project $project, TicketPriority $ticketPriority, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); DB::transaction(function () use ($project, $request, $ticketPriority): void { $ticketPriority->update([ 'name' => $request->validated('name'), ]); $this->reorderModels( $project->ticketPriorities()->whereKeyNot($ticketPriority->id)->get()->values(), $ticketPriority, $request->position(), ); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketPriority->fresh()->name} priority was updated."); } public function destroyPriority( Request $request, Project $project, TicketPriority $ticketPriority, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); $deletedName = $ticketPriority->name; DB::transaction(function () use ($project, $ticketPriority): void { $ticketPriority->delete(); $this->persistOrderedModels($project->ticketPriorities()->get()->values()); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$deletedName} priority was removed."); } public function storeFieldDefinition( StoreTicketFieldDefinitionRequest $request, Project $project, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); $ticketFieldDefinition = DB::transaction(function () use ($project, $request): TicketFieldDefinition { $ticketFieldDefinition = $project->ticketFieldDefinitions()->create([ 'field_type' => $request->validated('field_type'), 'label' => $request->validated('label'), 'position' => $project->ticketFieldDefinitions()->count() + 1, ]); $this->reorderModels( $project->ticketFieldDefinitions()->whereKeyNot($ticketFieldDefinition->id)->get()->values(), $ticketFieldDefinition, $request->position() ?? $project->ticketFieldDefinitions()->count(), ); return $ticketFieldDefinition; }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketFieldDefinition->label} field was added."); } public function updateFieldDefinition( UpdateTicketFieldDefinitionRequest $request, Project $project, TicketFieldDefinition $ticketFieldDefinition, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); DB::transaction(function () use ($project, $request, $ticketFieldDefinition): void { $ticketFieldDefinition->update([ 'field_type' => $request->validated('field_type'), 'label' => $request->validated('label'), ]); $this->reorderModels( $project->ticketFieldDefinitions()->whereKeyNot($ticketFieldDefinition->id)->get()->values(), $ticketFieldDefinition, $request->position(), ); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketFieldDefinition->fresh()->label} field was updated."); } public function destroyFieldDefinition( Request $request, Project $project, TicketFieldDefinition $ticketFieldDefinition, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); $deletedLabel = $ticketFieldDefinition->label; DB::transaction(function () use ($project, $ticketFieldDefinition): void { $ticketFieldDefinition->delete(); $this->persistOrderedModels($project->ticketFieldDefinitions()->get()->values()); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$deletedLabel} field was removed."); } private function abortUnlessCanManage(Request $request, Project $project): void { abort_unless( $this->projectPermissionService->userCanManageProjectAccess($request->user(), $project), 403, ); } /** * @param Collection $models */ private function persistOrderedModels(Collection $models): void { $temporaryOffset = ((int) $models->max('position')) + $models->count() + 1; $models->each(function (Model $model, int $index) use ($temporaryOffset): void { $model->forceFill([ 'position' => $temporaryOffset + $index, ])->saveQuietly(); }); $models->each(function (Model $model, int $index): void { $model->forceFill([ 'position' => $index + 1, ])->saveQuietly(); }); } /** * @param Collection $existingModels */ private function reorderModels(Collection $existingModels, Model $movingModel, int $desiredPosition): void { $insertAt = max(0, min($desiredPosition - 1, $existingModels->count())); $existingModels->splice($insertAt, 0, [$movingModel]); $this->persistOrderedModels($existingModels->values()); } }