142 lines
9.4 KiB
PHP
142 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Requests\Concerns;
|
|
|
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
|
use App\Actions\Projects\ProjectConfigurationIdentity;
|
|
use App\Enums\NotificationChannel;
|
|
use App\Enums\NotificationEventType;
|
|
use App\Enums\NotificationFilterAttribute;
|
|
use App\Enums\NotificationFilterOperator;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\Validator;
|
|
|
|
trait ValidatesProjectTemplateConfiguration
|
|
{
|
|
/** @return array<string, mixed> */
|
|
protected function configurationRules(): array
|
|
{
|
|
$rules = ['configuration_snapshot' => ['sometimes', 'array:'.implode(',', DuplicateProjectConfiguration::copyableAreas())]];
|
|
foreach (ProjectConfigurationIdentity::relations() as $area => $relation) {
|
|
$path = 'configuration_snapshot.'.$area;
|
|
$rules[$path] = ['sometimes', 'array', 'max:200'];
|
|
$rules[$path.'.*.configuration_key'] = ['required', 'uuid', 'distinct:strict'];
|
|
}
|
|
$rules['configuration_snapshot.project_settings'] = ['sometimes', 'array:show_author,show_story_points'];
|
|
foreach (['show_author', 'show_story_points'] as $field) {
|
|
$rules['configuration_snapshot.project_settings.'.$field] = ['required_with:configuration_snapshot.project_settings', 'boolean'];
|
|
}
|
|
foreach (['board_columns', 'ticket_statuses', 'ticket_types', 'ticket_priorities'] as $area) {
|
|
$rules["configuration_snapshot.{$area}.*.name"] = ['required', 'string', 'max:255'];
|
|
}
|
|
foreach (['board_columns', 'ticket_statuses', 'ticket_types', 'ticket_priorities', 'ticket_fields', 'ticket_link_types'] as $area) {
|
|
$rules["configuration_snapshot.{$area}.*.position"] = ['required', 'integer', 'min:1', 'max:10000'];
|
|
if ($area !== 'ticket_statuses') {
|
|
$rules["configuration_snapshot.{$area}.*.position"][] = 'distinct:strict';
|
|
}
|
|
}
|
|
$fields = [
|
|
'board_columns' => 'configuration_key,name,position',
|
|
'ticket_statuses' => 'configuration_key,name,position,board_column_key',
|
|
'ticket_types' => 'configuration_key,name,position,is_parent',
|
|
'ticket_priorities' => 'configuration_key,name,position',
|
|
'ticket_fields' => 'configuration_key,label,position,field_type',
|
|
'ticket_link_types' => 'configuration_key,label,counterpart_label,position',
|
|
'repository_connections' => 'configuration_key,provider,base_url,provider_repository_id,owner_name,repository_name,repository_full_name,repository_url',
|
|
'notification_actions' => 'configuration_key,name,enabled,event_type,channel,channel_configuration,message_template,delivery_url,filters',
|
|
];
|
|
foreach ($fields as $area => $allowed) {
|
|
$rules["configuration_snapshot.{$area}.*"] = ['array:'.$allowed];
|
|
}
|
|
$rules['configuration_snapshot.ticket_statuses.*.board_column_key'] = ['nullable', 'uuid'];
|
|
$rules['configuration_snapshot.ticket_types.*.is_parent'] = ['required', 'boolean'];
|
|
$rules['configuration_snapshot.ticket_fields.*.field_type'] = ['required', Rule::in(['number', 'user'])];
|
|
$rules['configuration_snapshot.ticket_fields.*.label'] = ['required', 'string', 'max:255'];
|
|
$rules['configuration_snapshot.ticket_link_types.*.label'] = ['required', 'string', 'max:255'];
|
|
$rules['configuration_snapshot.ticket_link_types.*.counterpart_label'] = ['required', 'string', 'max:255'];
|
|
$rules['configuration_snapshot.status_transitions'] = ['sometimes', 'array', 'max:200'];
|
|
$rules['configuration_snapshot.status_transitions.*'] = ['array:status_key,target_keys'];
|
|
$rules['configuration_snapshot.status_transitions.*.status_key'] = ['required', 'uuid', 'distinct:strict'];
|
|
$rules['configuration_snapshot.status_transitions.*.target_keys'] = ['present', 'array', 'max:200'];
|
|
$rules['configuration_snapshot.status_transitions.*.target_keys.*'] = ['uuid'];
|
|
foreach (['provider_repository_id', 'owner_name', 'repository_name', 'repository_full_name'] as $field) {
|
|
$rules['configuration_snapshot.repository_connections.*.'.$field] = ['required', 'string', 'max:255'];
|
|
}
|
|
$rules['configuration_snapshot.repository_connections.*.provider'] = ['required', Rule::in(['gitea'])];
|
|
foreach (['base_url', 'repository_url'] as $field) {
|
|
$rules['configuration_snapshot.repository_connections.*.'.$field] = ['required', 'url:http,https', 'max:255'];
|
|
}
|
|
$prefix = 'configuration_snapshot.notification_actions.*.';
|
|
$rules[$prefix.'name'] = ['required', 'string', 'max:255'];
|
|
$rules[$prefix.'enabled'] = ['required', 'boolean'];
|
|
$rules[$prefix.'event_type'] = ['required', Rule::in(NotificationEventType::values())];
|
|
$rules[$prefix.'channel'] = ['required', Rule::in(NotificationChannel::values())];
|
|
$rules[$prefix.'delivery_url'] = ['required', 'url:http,https', 'max:2048'];
|
|
$rules[$prefix.'message_template'] = ['nullable', 'string', 'max:2000'];
|
|
$rules[$prefix.'channel_configuration'] = ['sometimes', 'array:allowed_mentions'];
|
|
$rules[$prefix.'channel_configuration.allowed_mentions'] = ['sometimes', 'array'];
|
|
$rules[$prefix.'channel_configuration.allowed_mentions.*'] = [Rule::in(['everyone', 'roles', 'users'])];
|
|
$rules[$prefix.'filters'] = ['sometimes', 'array', 'max:25'];
|
|
$rules[$prefix.'filters.*'] = ['array:attribute,operator,value'];
|
|
$rules[$prefix.'filters.*.attribute'] = ['required', Rule::in(NotificationFilterAttribute::values())];
|
|
$rules[$prefix.'filters.*.operator'] = ['required', Rule::in(NotificationFilterOperator::values())];
|
|
$rules[$prefix.'filters.*.value'] = ['nullable'];
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/** @return array<int, callable(Validator): void> */
|
|
public function after(): array
|
|
{
|
|
return [function (Validator $validator): void {
|
|
if ($validator->errors()->isNotEmpty()) {
|
|
return;
|
|
}
|
|
$validated = $validator->validated();
|
|
$configuration = Arr::get($validated, 'configuration_snapshot', []);
|
|
$areas = Arr::get($validated, 'copy_areas', []);
|
|
$columns = array_column(Arr::get($configuration, 'board_columns', []), 'configuration_key');
|
|
$statuses = array_column(Arr::get($configuration, 'ticket_statuses', []), 'configuration_key');
|
|
foreach (Arr::get($configuration, 'ticket_statuses', []) as $index => $status) {
|
|
$key = Arr::get($status, 'board_column_key');
|
|
if ($key !== null && in_array('board_columns', $areas, true) && ! in_array($key, $columns, true)) {
|
|
$validator->errors()->add("configuration_snapshot.ticket_statuses.{$index}.board_column_key", 'Choose a column from this template.');
|
|
}
|
|
}
|
|
foreach (Arr::get($configuration, 'status_transitions', []) as $index => $transition) {
|
|
if (! in_array(Arr::get($transition, 'status_key'), $statuses, true)
|
|
|| array_diff(Arr::get($transition, 'target_keys', []), $statuses) !== []) {
|
|
$validator->errors()->add("configuration_snapshot.status_transitions.{$index}", 'Choose statuses from this template.');
|
|
}
|
|
if (in_array('status_transitions', $areas, true) && ! in_array('ticket_statuses', $areas, true)
|
|
&& Arr::get($transition, 'target_keys', []) !== []) {
|
|
$validator->errors()->add('copy_areas', 'Include ticket statuses when defining template transitions.');
|
|
}
|
|
}
|
|
$repositoryIdentifiers = [];
|
|
foreach (Arr::get($configuration, 'repository_connections', []) as $index => $connection) {
|
|
$identifier = serialize([$connection['provider'], $connection['base_url'], $connection['provider_repository_id']]);
|
|
if (isset($repositoryIdentifiers[$identifier])) {
|
|
$validator->errors()->add("configuration_snapshot.repository_connections.{$index}.provider_repository_id", 'This repository is already included in the template.');
|
|
}
|
|
$repositoryIdentifiers[$identifier] = true;
|
|
}
|
|
foreach (Arr::get($configuration, 'notification_actions', []) as $index => $action) {
|
|
if (Arr::get($action, 'channel') === 'discord' && blank(Arr::get($action, 'message_template'))) {
|
|
$validator->errors()->add("configuration_snapshot.notification_actions.{$index}.message_template", 'A Discord message is required.');
|
|
}
|
|
foreach (Arr::get($action, 'filters', []) as $filterIndex => $filter) {
|
|
$operator = Arr::get($filter, 'operator');
|
|
$value = Arr::get($filter, 'value');
|
|
$valid = is_string($value) || $value === null || (is_array($value) && collect($value)->every(static fn ($item): bool => is_string($item)));
|
|
if (! $valid || (in_array($operator, ['equals', 'not_equals', 'in'], true) && blank($value))
|
|
|| (in_array($operator, ['exists', 'not_exists'], true) && filled($value))) {
|
|
$validator->errors()->add("configuration_snapshot.notification_actions.{$index}.filters.{$filterIndex}.value", 'Choose a value that matches the filter operator.');
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
}
|