| @@ -1,12 +1,15 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentBoards\App\Modules\MCP\Tools; |
| 4 | 4 | |
| 5 | +use FluentBoards\App\Models\Relation; | |
| 5 | 6 | use FluentBoards\App\Models\Stage; |
| 6 | 7 | use FluentBoards\App\Models\Task; |
| 7 | 8 | use FluentBoards\App\Modules\MCP\Helpers\MCPHelper; |
| 8 | 9 | use FluentBoards\App\Services\Constant; |
| 10 | +use FluentBoards\App\Services\NotificationService; | |
| 11 | +use FluentBoards\App\Services\PermissionManager; | |
| 9 | 12 | use FluentBoards\App\Services\TaskService; |
| 10 | 13 | |
| 11 | 14 | /** |
| 12 | 15 | * Task read/write MCP tools. |
| @@ -54,8 +57,10 @@ | ||
| 54 | 57 | 'due_date' => isset($params['due_date']) ? (array) $params['due_date'] : [], |
| 55 | 58 | ]; |
| 56 | 59 | |
| 57 | 60 | $paginated = (new TaskService())->getTableTasks($board->id, $args); |
| 61 | + // Every summary includes its stage; load once for the page instead of per task. | |
| 62 | + $paginated->getCollection()->load('stage'); | |
| 58 | 63 | |
| 59 | 64 | return [ |
| 60 | 65 | 'items' => MCPHelper::formatTaskList($paginated->items()), |
| 61 | 66 | 'pagination' => [ |
| @@ -152,8 +157,28 @@ | ||
| 152 | 157 | if (!MCPHelper::canWriteBoard($task->board_id)) { |
| 153 | 158 | return MCPHelper::error('forbidden', __('You do not have permission to update this task', 'fluent-boards')); |
| 154 | 159 | } |
| 155 | 160 | |
| 161 | + $assigneeIds = null; | |
| 162 | + if (array_key_exists('assignees', $params)) { | |
| 163 | + $assigneeIds = array_values(array_unique(MCPHelper::sanitizeIdArray($params['assignees']))); | |
| 164 | + $task->load('assignees'); | |
| 165 | + $currentAssigneeIds = []; | |
| 166 | + foreach ($task->assignees as $assignee) { | |
| 167 | + $currentAssigneeIds[] = (int) $assignee->ID; | |
| 168 | + } | |
| 169 | + | |
| 170 | + $nonMemberIds = self::findIneligibleAssigneeUserIds( | |
| 171 | + array_diff($assigneeIds, $currentAssigneeIds), | |
| 172 | + $task->board_id | |
| 173 | + ); | |
| 174 | + if ($nonMemberIds) { | |
| 175 | + return MCPHelper::error('forbidden', __('Some users are not members of this board', 'fluent-boards'), [ | |
| 176 | + 'non_board_member_user_ids' => $nonMemberIds, | |
| 177 | + ]); | |
| 178 | + } | |
| 179 | + } | |
| 180 | + | |
| 156 | 181 | $service = new TaskService(); |
| 157 | 182 | $updatable = [ |
| 158 | 183 | 'title' => 'text', |
| 159 | 184 | 'description' => 'markdown', |
| @@ -178,10 +203,10 @@ | ||
| 178 | 203 | $value = self::sanitizeUpdateValue($params[$field], $type); |
| 179 | 204 | $task = $service->updateTaskProperty($field, $value, $task); |
| 180 | 205 | } |
| 181 | 206 | |
| 182 | - if (array_key_exists('assignees', $params)) { | |
| 183 | - $task = self::syncAssignees($task, MCPHelper::sanitizeIdArray($params['assignees'])); | |
| 207 | + if ($assigneeIds !== null) { | |
| 208 | + $task = self::syncAssignees($task, $assigneeIds); | |
| 184 | 209 | } |
| 185 | 210 | |
| 186 | 211 | MCPHelper::loadTaskDetails($task); |
| 187 | 212 | |
| @@ -245,8 +270,11 @@ | ||
| 245 | 270 | $task = $task->reopen(); |
| 246 | 271 | } |
| 247 | 272 | |
| 248 | 273 | do_action('fluent_boards/task_stage_updated', $task, $oldStageId); |
| 274 | + | |
| 275 | + $recipients = (new NotificationService())->filterAssigneeToSendEmail($task->id, Constant::BOARD_EMAIL_STAGE_CHANGE); | |
| 276 | + $service->sendMailAfterTaskModify('stage_change', $recipients, $task->id); | |
| 249 | 277 | } |
| 250 | 278 | |
| 251 | 279 | do_action('fluent_boards/task_updated', $task, 'position'); |
| 252 | 280 | MCPHelper::loadTaskDetails($task); |
| @@ -279,9 +307,9 @@ | ||
| 279 | 307 | ]); |
| 280 | 308 | } |
| 281 | 309 | |
| 282 | 310 | $hasUserIds = array_key_exists('user_ids', $params); |
| 283 | - $userIds = MCPHelper::sanitizeIdArray($params['user_ids'] ?? []); | |
| 311 | + $userIds = array_values(array_unique(MCPHelper::sanitizeIdArray($params['user_ids'] ?? []))); | |
| 284 | 312 | if (!$hasUserIds && !empty($params['user_id'])) { |
| 285 | 313 | $userIds = [absint($params['user_id'])]; |
| 286 | 314 | } |
| 287 | 315 | |
| @@ -321,8 +349,15 @@ | ||
| 321 | 349 | $toAdd = array_diff($userIds, $currentIds); |
| 322 | 350 | $toRemove = array_diff($currentIds, $userIds); |
| 323 | 351 | } |
| 324 | 352 | |
| 353 | + $nonMemberIds = self::findIneligibleAssigneeUserIds($toAdd, $task->board_id); | |
| 354 | + if ($nonMemberIds) { | |
| 355 | + return MCPHelper::error('forbidden', __('Some users are not members of this board', 'fluent-boards'), [ | |
| 356 | + 'non_board_member_user_ids' => $nonMemberIds, | |
| 357 | + ]); | |
| 358 | + } | |
| 359 | + | |
| 325 | 360 | $service = new TaskService(); |
| 326 | 361 | |
| 327 | 362 | foreach (array_merge($toRemove, $toAdd) as $userId) { |
| 328 | 363 | $service->updateAssignee($userId, $task); |
| @@ -336,9 +371,9 @@ | ||
| 336 | 371 | 'board_id' => (int) $task->board_id, |
| 337 | 372 | 'mode' => $mode, |
| 338 | 373 | 'added' => array_values($toAdd), |
| 339 | 374 | 'removed' => array_values($toRemove), |
| 340 | - 'assignees' => MCPHelper::formatUserList($task->assignees), | |
| 375 | + 'assignees' => MCPHelper::formatUserList($task->assignees, $task->board_id), | |
| 341 | 376 | 'message' => __('Task assignees have been updated', 'fluent-boards'), |
| 342 | 377 | ]; |
| 343 | 378 | } |
| 344 | 379 | |
| @@ -378,8 +413,35 @@ | ||
| 378 | 413 | |
| 379 | 414 | $found = array_map('intval', (array) $found); |
| 380 | 415 | |
| 381 | 416 | return array_values(array_diff($userIds, $found)); |
| 417 | + } | |
| 418 | + | |
| 419 | + /** | |
| 420 | + * @return array User ids that are neither board members nor global administrators. | |
| 421 | + */ | |
| 422 | + private static function findIneligibleAssigneeUserIds($userIds, $boardId) | |
| 423 | + { | |
| 424 | + if (!$userIds) { | |
| 425 | + return []; | |
| 426 | + } | |
| 427 | + | |
| 428 | + $memberIds = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER) | |
| 429 | + ->where('object_id', (int) $boardId) | |
| 430 | + ->whereIn('foreign_id', $userIds) | |
| 431 | + ->pluck('foreign_id') | |
| 432 | + ->toArray(); | |
| 433 | + | |
| 434 | + $memberIds = array_map('intval', $memberIds); | |
| 435 | + | |
| 436 | + $ineligibleIds = []; | |
| 437 | + foreach (array_diff($userIds, $memberIds) as $userId) { | |
| 438 | + if (!PermissionManager::isAdmin($userId)) { | |
| 439 | + $ineligibleIds[] = $userId; | |
| 440 | + } | |
| 441 | + } | |
| 442 | + | |
| 443 | + return $ineligibleIds; | |
| 382 | 444 | } |
| 383 | 445 | |
| 384 | 446 | private static function sanitizeUpdateValue($value, $type) |
| 385 | 447 | { |