feat(KAN-72): complete MCP ticket editing and discovery
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Support;
|
||||
|
||||
use App\Actions\Tickets\Support\AcceptanceCriteriaChecklistMapper;
|
||||
use App\Http\Requests\UpdateProjectTicketRequest;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketFieldDefinition;
|
||||
use App\Models\User;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
|
||||
class TicketUpdateInput
|
||||
{
|
||||
public function __construct(private readonly KanbanGitMcp $kanbanGitMcp) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function validate(Request $request, Ticket $ticket, User $user): array
|
||||
{
|
||||
$form = new UpdateProjectTicketRequest;
|
||||
$route = new Route('PUT', '/', fn () => null);
|
||||
$route->bind(\Illuminate\Http\Request::create('/', 'PUT'));
|
||||
$route->setParameter('project', $ticket->project);
|
||||
$route->setParameter('ticket', $ticket);
|
||||
$form->setRouteResolver(fn () => $route);
|
||||
$form->setUserResolver(fn () => $user);
|
||||
$rules = $form->rules();
|
||||
foreach ($rules as $key => &$rule) {
|
||||
if (! str_contains($key, '.')) {
|
||||
$rule = ['sometimes', ...$rule];
|
||||
}
|
||||
}
|
||||
unset($rule);
|
||||
$rules['acceptance_criteria'] = ['sometimes', 'array', 'list'];
|
||||
$rules['acceptance_criteria.*'] = ['array:id,text,checked,indent'];
|
||||
$rules['acceptance_criteria.*.id'] = ['sometimes', 'integer', 'min:1', 'distinct', Rule::exists('acceptance_criteria', 'id')->where('ticket_id', $ticket->id)];
|
||||
$rules['acceptance_criteria.*.checked'] = ['sometimes', 'boolean'];
|
||||
$rules['acceptance_criteria.*.indent'] = ['sometimes', 'integer', 'min:0', 'max:'.AcceptanceCriteriaChecklistMapper::MAX_INDENT_LEVEL];
|
||||
$rules['field_values'] = ['sometimes', 'array', 'list'];
|
||||
$rules['field_values.*'] = ['array:ticket_field_definition_id,value_number,value_user_id'];
|
||||
$rules['field_values.*.ticket_field_definition_id'][] = 'distinct';
|
||||
$rules['ticket_status_id'] = ['prohibited'];
|
||||
$rules['acceptance_criteria.*.text'][] = function (string $attribute, mixed $value, \Closure $fail): void {
|
||||
if (is_string($value) && trim($value) === '') {
|
||||
$fail('Criterion text must not be blank.');
|
||||
}
|
||||
};
|
||||
$input = $request->validate($rules);
|
||||
$previousIndent = -1;
|
||||
foreach (Arr::get($input, 'acceptance_criteria', []) as $index => $item) {
|
||||
$indent = Arr::get($item, 'indent', 0);
|
||||
if ($indent > $previousIndent + 1) {
|
||||
throw ValidationException::withMessages(["acceptance_criteria.{$index}.indent" => 'Indentation cannot skip a level; the first item must have indent 0.']);
|
||||
}
|
||||
$previousIndent = $indent;
|
||||
}
|
||||
$merged = array_replace($this->kanbanGitMcp->updatePayload($ticket), $input);
|
||||
$merged['acceptance_criteria'] = Arr::exists($input, 'acceptance_criteria')
|
||||
? AcceptanceCriteriaChecklistMapper::normalize($input['acceptance_criteria'])
|
||||
: AcceptanceCriteriaChecklistMapper::fromModelsWithIds($ticket->acceptanceCriteria()->get());
|
||||
|
||||
$validator = Validator::make($merged, $form->rules());
|
||||
$form->setValidator($validator);
|
||||
$validator->validate();
|
||||
foreach ($form->after() as $callback) {
|
||||
$callback($validator);
|
||||
}
|
||||
|
||||
$people = User::query()->visibleToProject($ticket->project)->pluck('id')->all();
|
||||
foreach (['assignee_user_id', 'author_user_id'] as $field) {
|
||||
if (Arr::exists($input, $field) && $input[$field] !== null && ! in_array((int) $input[$field], $people, true)) {
|
||||
$validator->errors()->add($field, 'The person must be able to view this project.');
|
||||
}
|
||||
}
|
||||
$definitions = $ticket->project->ticketFieldDefinitions()->get()->keyBy('id');
|
||||
foreach (Arr::get($input, 'field_values', []) as $index => $value) {
|
||||
$definition = $definitions->get($value['ticket_field_definition_id']);
|
||||
$incompatible = $definition->field_type === 'user' ? 'value_number' : 'value_user_id';
|
||||
if (Arr::get($value, $incompatible) !== null) {
|
||||
$validator->errors()->add("field_values.{$index}.{$incompatible}", 'The value must match the custom field type.');
|
||||
}
|
||||
if (Arr::get($value, 'value_user_id') !== null && ! in_array((int) $value['value_user_id'], $people, true)) {
|
||||
$validator->errors()->add("field_values.{$index}.value_user_id", 'The person must be able to view this project.');
|
||||
}
|
||||
}
|
||||
if ($validator->errors()->isNotEmpty()) {
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
if (Arr::exists($input, 'field_values')) {
|
||||
$replacement = $definitions->map(fn (TicketFieldDefinition $definition): array => [
|
||||
'ticket_field_definition_id' => $definition->id,
|
||||
'value_number' => null,
|
||||
'value_user_id' => null,
|
||||
])->all();
|
||||
foreach ($input['field_values'] as $value) {
|
||||
$replacement[$value['ticket_field_definition_id']] = $value;
|
||||
}
|
||||
$merged['field_values'] = array_values($replacement);
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user