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(), 'ticketLinkTypes' => $project->ticketLinkTypes()->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); $validated = $request->validated(); $name = Arr::get($validated, 'name'); $position = Arr::get($validated, 'position'); $ticketType = DB::transaction(function () use ($project, $name, $position): TicketType { $ticketType = $project->ticketTypes()->create([ 'is_builtin' => false, 'name' => $name, 'position' => $project->ticketTypes()->count() + 1, 'system_key' => null, ]); $this->reorderModels( $project->ticketTypes()->whereKeyNot($ticketType->id)->get()->values(), $ticketType, $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); $validated = $request->validated(); $name = Arr::get($validated, 'name'); $position = Arr::get($validated, 'position'); DB::transaction(function () use ($project, $name, $position, $ticketType): void { $ticketType->update([ 'name' => $name, ]); $this->reorderModels( $project->ticketTypes()->whereKeyNot($ticketType->id)->get()->values(), $ticketType, $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); $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); $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(); $label = Arr::get($validated, 'label'); $fieldType = Arr::get($validated, 'field_type'); $position = Arr::get($validated, 'position'); $ticketFieldDefinition = DB::transaction(function () use ($project, $fieldType, $label): TicketFieldDefinition { $ticketFieldDefinition = $project->ticketFieldDefinitions()->create([ 'field_type' => $fieldType, 'label' => $label, 'position' => $project->ticketFieldDefinitions()->count() + 1, ]); $this->reorderModels( $project->ticketFieldDefinitions()->whereKeyNot($ticketFieldDefinition->id)->get()->values(), $ticketFieldDefinition, $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(); $label = Arr::get($validated, 'label'); $fieldType = Arr::get($validated, 'field_type'); $position = Arr::get($validated, 'position'); DB::transaction(function () use ($project, $fieldType, $label, $ticketFieldDefinition, $position): void { $ticketFieldDefinition->update([ 'field_type' => $fieldType, 'label' => $label, ]); $this->reorderModels( $project->ticketFieldDefinitions()->whereKeyNot($ticketFieldDefinition->id)->get()->values(), $ticketFieldDefinition, $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->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()); } }