*/ 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 */ 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.'); } } } }]; } }