abortUnlessCanManage($request, $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, 'error' => session('error'), 'status' => session('status'), 'ticketFieldDefinitions' => $project->ticketFieldDefinitions()->get(), 'ticketLinkTypes' => $project->ticketLinkTypes()->get(), 'ticketPriorities' => $project->ticketPriorities()->get(), 'ticketTypes' => $project->ticketTypes()->withCount('tickets')->get(), ]); } public function update(UpdateProjectTicketMetadataSettings $request, Project $project): RedirectResponse { $this->abortUnlessCanManage($request, $project); $validated = $request->validated(); $project->update([ 'show_author' => Arr::get($validated, 'show_author', false), 'show_story_points' => Arr::get($validated, 'show_story_points', false), ]); return to_route('projects.settings.metadata', $project) ->with('status', 'Settings have been updated.'); } public function storeType(StoreProjectTicketTypeRequest $request, Project $project): RedirectResponse { $this->abortUnlessCanManage($request, $project); $validated = $request->validated(); $ticketType = DB::transaction(function () use ($project, $validated): TicketType { $ticketType = $project->ticketTypes()->create([ 'name' => Arr::get($validated, 'name'), 'position' => $project->ticketTypes()->count() + 1, 'is_parent' => Arr::get($validated, 'is_parent', false), ]); $this->reorderModels( $project->ticketTypes()->whereKeyNot($ticketType->id)->get()->values(), $ticketType, Arr::get($validated, 'position') ?? $project->ticketTypes()->count(), ); return $ticketType; }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketType->name} type was added."); } public function updateType( UpdateProjectTicketTypeRequest $request, Project $project, TicketType $ticketType, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); $validated = $request->validated(); DB::transaction(function () use ($project, $validated, $ticketType): void { $ticketType->update([ 'name' => Arr::get($validated, 'name'), ]); $this->reorderModels( $project->ticketTypes()->whereKeyNot($ticketType->id)->get()->values(), $ticketType, Arr::get($validated, 'position'), ); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketType->fresh()->name} type was updated."); } public function destroyType( DestroyProjectTicketTypeRequest $request, Project $project, TicketType $ticketType, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); if ($project->ticketTypes()->count() === 1) { return to_route('projects.settings.metadata', $project) ->with('error', true) ->with('status', 'The last ticket type cannot be deleted.'); } $deletedName = $ticketType->name; $validated = $request->validated(); DB::transaction(function () use ($deletedName, $project, $request, $ticketType, $validated): void { $replacementTicketTypeId = Arr::get($validated, 'replacement_ticket_type_id'); if ($replacementTicketTypeId !== null) { $replacementTicketType = $project->ticketTypes() ->findOrFail($replacementTicketTypeId); Ticket::query() ->where('project_id', $project->id) ->where('ticket_type_id', $ticketType->id) ->with(['ticketType']) ->get() ->each(function (Ticket $ticket) use ($deletedName, $replacementTicketType, $request): void { $ticket->update([ 'ticket_type_id' => $replacementTicketType->id, ]); $this->createTicketWorklogEntry->handle( $ticket->fresh(), $request->user(), 'updated', "Updated type because {$deletedName} type was deleted.", [ 'ticket_type' => [ 'from' => $deletedName, 'to' => $replacementTicketType->name, ], ], ); }); } $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); $validated = $request->validated(); $name = Arr::get($validated, 'name'); $position = Arr::get($validated, 'position'); $ticketPriority = DB::transaction(function () use ($project, $name, $position): TicketPriority { $ticketPriority = $project->ticketPriorities()->create([ 'name' => $name, 'position' => $project->ticketPriorities()->count() + 1, ]); $this->reorderModels( $project->ticketPriorities()->whereKeyNot($ticketPriority->id)->get()->values(), $ticketPriority, $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); $validated = $request->validated(); $name = Arr::get($validated, 'name'); $position = Arr::get($validated, 'position'); DB::transaction(function () use ($project, $name, $position, $ticketPriority): void { $ticketPriority->update([ 'name' => $name, ]); $this->reorderModels( $project->ticketPriorities()->whereKeyNot($ticketPriority->id)->get()->values(), $ticketPriority, $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); if ($project->ticketPriorities()->count() === 1) { return to_route('projects.settings.metadata', $project) ->with('error', true) ->with('status', 'The last ticket priority cannot be deleted.'); } $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); $validated = $request->validated(); $ticketFieldDefinition = DB::transaction(function () use ($project, $validated): TicketFieldDefinition { $ticketFieldDefinition = $project->ticketFieldDefinitions()->create([ 'field_type' => Arr::get($validated, 'field_type'), 'label' => Arr::get($validated, 'label'), 'position' => $project->ticketFieldDefinitions()->count() + 1, ]); $this->reorderModels( $project->ticketFieldDefinitions()->whereKeyNot($ticketFieldDefinition->id)->get()->values(), $ticketFieldDefinition, Arr::get($validated, '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); $validated = $request->validated(); DB::transaction(function () use ($project, $ticketFieldDefinition, $validated): void { $ticketFieldDefinition->update([ 'field_type' => Arr::get($validated, 'field_type'), 'label' => Arr::get($validated, 'label'), ]); $this->reorderModels( $project->ticketFieldDefinitions()->whereKeyNot($ticketFieldDefinition->id)->get()->values(), $ticketFieldDefinition, Arr::get($validated, '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."); } public function storeLinkType(StoreProjectNamedSettingRequest $request, Project $project): RedirectResponse { $validated = $request->validated(); $name = Arr::get($validated, 'name'); $position = Arr::get($validated, 'position'); $this->abortUnlessCanManage($request, $project); $ticketLinkType = DB::transaction(function () use ($project, $position, $name): TicketLinkType { $ticketLinkType = $project->ticketLinkTypes()->create([ 'label' => $name, 'position' => $project->ticketLinkTypes()->count() + 1, ]); $this->reorderModels( $project->ticketLinkTypes()->whereKeyNot($ticketLinkType->id)->get()->values(), $ticketLinkType, $position ?? $project->ticketLinkTypes()->count(), ); return $ticketLinkType; }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketLinkType->label} link type was added."); } public function updateLinkType( UpdateProjectNamedSettingRequest $request, Project $project, TicketLinkType $ticketLinkType, ): RedirectResponse { $validated = $request->validated(); $name = Arr::get($validated, 'name'); $position = Arr::get($validated, 'position'); $this->abortUnlessCanManage($request, $project); DB::transaction(function () use ($project, $name, $position, $ticketLinkType): void { $ticketLinkType->update([ 'label' => $name, ]); $this->reorderModels( $project->ticketLinkTypes()->whereKeyNot($ticketLinkType->id)->get()->values(), $ticketLinkType, $position, ); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$ticketLinkType->fresh()->label} link type was updated."); } public function destroyLinkType( Request $request, Project $project, TicketLinkType $ticketLinkType, ): RedirectResponse { $this->abortUnlessCanManage($request, $project); $deletedLabel = $ticketLinkType->label; DB::transaction(function () use ($project, $ticketLinkType): void { $ticketLinkType->delete(); $this->persistOrderedModels($project->ticketLinkTypes()->get()->values()); }); return to_route('projects.settings.metadata', $project) ->with('status', "{$deletedLabel} link type was removed."); } private function abortUnlessCanManage(Request $request, Project $project): void { abort_unless( $this->permissionService->allows($request->user(), Permission::ProjectAdmin, $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()); } }