PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
← All changes | app/Modules/MCP/Tools/BoardTools.php +227 -7 1.952.1.0 View file →
@@ -2,10 +2,13 @@
2 2
3 3 namespace FluentBoards\App\Modules\MCP\Tools;
4 4
5 5 use FluentBoards\App\Models\Board;
6 +use FluentBoards\App\Models\Stage;
6 7 use FluentBoards\App\Modules\MCP\Helpers\MCPHelper;
7 8 use FluentBoards\App\Services\BoardService;
9 +use FluentBoards\App\Services\Constant;
10 +use FluentBoards\App\Services\FolderService;
8 11 use FluentBoards\App\Services\Helper;
9 12 use FluentBoards\App\Services\LabelService;
10 13 use FluentBoards\App\Services\PermissionManager;
11 14 use FluentBoards\App\Services\StageService;
@@ -14,8 +17,10 @@
14 17 * Board read/write tools and board permission helpers.
15 18 */
16 19 class BoardTools
17 20 {
21 + const MAX_MEMBERS_PER_BOARD_CREATE = 50;
22 +
18 23 public static function canReadBoard($params = [])
19 24 {
20 25 $boardId = isset($params['board_id']) ? absint($params['board_id']) : 0;
21 26 return $boardId && MCPHelper::canReadBoard($boardId);
@@ -109,15 +114,28 @@
109 114 'allowed' => ['to-do', 'roadmap'],
110 115 ]);
111 116 }
112 117
118 + $memberIds = self::validateBoardMemberIds($params['member_ids'] ?? []);
119 + if (is_wp_error($memberIds)) {
120 + return $memberIds;
121 + }
122 +
123 + $labelPresetValidation = self::validateLabelPresets($params['labels'] ?? null);
124 + if (is_wp_error($labelPresetValidation)) {
125 + return $labelPresetValidation;
126 + }
127 +
128 + $description = isset($params['description']) ? MCPHelper::sanitizeMarkdown($params['description']) : '';
129 +
113 130 $boardData = Helper::sanitizeBoard([
114 131 'title' => $title,
115 - 'description' => isset($params['description']) ? wp_kses_post($params['description']) : '',
132 + 'description' => '',
116 133 'type' => $type,
117 134 'currency' => !empty($params['currency']) ? sanitize_text_field($params['currency']) : 'USD',
118 135 'crm_contact_id' => !empty($params['crm_contact_id']) ? absint($params['crm_contact_id']) : 0,
119 136 ]);
137 + $boardData['description'] = $description;
120 138
121 139 $boardService = new BoardService();
122 140 $labelService = new LabelService();
123 141 $stageService = new StageService();
@@ -122,16 +140,27 @@
122 140 $labelService = new LabelService();
123 141 $stageService = new StageService();
124 142
125 143 $board = $boardService->createBoard($boardData);
126 - $labelService->createDefaultLabel($board->id);
127 144
145 + self::createBoardLabels($labelService, $board->id, $params['labels'] ?? null);
146 +
147 + $stages = self::sanitizeStages($params['stages'] ?? []);
148 +
128 149 if ($type === 'roadmap') {
129 - $stageService->createRoadmapStages($board, self::sanitizeRoadmapStages($params['stages'] ?? []));
150 + if (!$stages) {
151 + $stages = self::defaultRoadmapStages();
152 + }
153 + $stageService->createRoadmapStages($board, $stages);
154 + } elseif ($stages) {
155 + $stageService->createStages($board, $stages);
156 + self::applyStageStatusOverrides($board->id, $stages);
130 157 } else {
131 158 $stageService->createDefaultStages($board);
132 159 }
133 160
161 + self::addBoardMembers($boardService, $board->id, $memberIds);
162 +
134 163 if (!empty($boardData['crm_contact_id'])) {
135 164 $boardService->updateAssociateMember($boardData['crm_contact_id'], $board->id);
136 165 }
137 166
@@ -136,10 +165,10 @@
136 165 }
137 166
138 167 do_action('fluent_boards/board_created', $board);
139 168
140 - if (defined('FLUENT_BOARDS_PRO') && !empty($params['folder_id']) && class_exists('FluentBoardsPro\App\Services\FolderService')) {
141 - (new \FluentBoardsPro\App\Services\FolderService())->addBoardToFolder(absint($params['folder_id']), [$board->id]);
169 + if (!empty($params['folder_id'])) {
170 + (new FolderService())->addBoardToFolder(absint($params['folder_id']), [$board->id]);
142 171 }
143 172
144 173 $board = Board::with(['stages', 'labels', 'users'])->find($board->id);
145 174
@@ -167,9 +196,9 @@
167 196 'last_page' => (int) $paginated->lastPage(),
168 197 ];
169 198 }
170 199
171 - private static function sanitizeRoadmapStages($stages)
200 + private static function sanitizeStages($stages)
172 201 {
173 202 $items = [];
174 203
175 204 if (!is_array($stages)) {
@@ -185,14 +214,205 @@
185 214 if ($title === '') {
186 215 continue;
187 216 }
188 217
189 - $items[] = [
218 + $item = [
190 219 'title' => $title,
191 220 'slug' => !empty($stage['slug']) ? sanitize_title($stage['slug']) : sanitize_title($title),
192 221 'position' => !empty($stage['position']) ? absint($stage['position']) : $index + 1,
193 222 ];
223 +
224 + if (!empty($stage['default_task_status']) && in_array($stage['default_task_status'], ['open', 'closed'], true)) {
225 + $item['default_task_status'] = $stage['default_task_status'];
226 + }
227 +
228 + $items[] = $item;
194 229 }
195 230
196 231 return $items;
232 + }
233 +
234 + private static function defaultRoadmapStages()
235 + {
236 + return [
237 + [
238 + 'title' => 'Pending',
239 + 'slug' => 'pending',
240 + 'position' => 1,
241 + ],
242 + [
243 + 'title' => 'Under Consideration',
244 + 'slug' => 'under_consideration',
245 + 'position' => 2,
246 + ],
247 + [
248 + 'title' => 'Planned',
249 + 'slug' => 'planned',
250 + 'position' => 3,
251 + ],
252 + [
253 + 'title' => 'Launched',
254 + 'slug' => 'launched',
255 + 'position' => 4,
256 + ],
257 + ];
258 + }
259 +
260 + private static function validateBoardMemberIds($memberIds)
261 + {
262 + if (!is_array($memberIds)) {
263 + return MCPHelper::error('invalid_param', __('member_ids must be an array', 'fluent-boards'));
264 + }
265 +
266 + if (count($memberIds) > self::MAX_MEMBERS_PER_BOARD_CREATE) {
267 + return MCPHelper::error('invalid_param', __('Too many members in one call', 'fluent-boards'), [
268 + 'max' => self::MAX_MEMBERS_PER_BOARD_CREATE,
269 + ]);
270 + }
271 +
272 + $memberIds = MCPHelper::sanitizeIdArray($memberIds);
273 + if (!$memberIds) {
274 + return [];
275 + }
276 +
277 + $found = get_users([
278 + 'include' => $memberIds,
279 + 'fields' => 'ID',
280 + 'number' => count($memberIds),
281 + ]);
282 + $found = array_map('intval', (array) $found);
283 + $unknownIds = array_values(array_diff($memberIds, $found));
284 +
285 + if ($unknownIds) {
286 + return MCPHelper::error('not_found', __('Some users do not exist', 'fluent-boards'), [
287 + 'unknown_user_ids' => $unknownIds,
288 + ]);
289 + }
290 +
291 + return $memberIds;
292 + }
293 +
294 + /**
295 + * StageService::createStages() only marks stages titled "completed"/"done" as closing stages,
296 + * so honour explicit per-stage statuses once the stages exist.
297 + */
298 + private static function applyStageStatusOverrides($boardId, $stages)
299 + {
300 + $overrides = [];
301 + foreach ($stages as $index => $stage) {
302 + if (!empty($stage['default_task_status'])) {
303 + $overrides[$index] = $stage['default_task_status'];
304 + }
305 + }
306 +
307 + if (!$overrides) {
308 + return;
309 + }
310 +
311 + $created = Stage::where('board_id', $boardId)
312 + ->whereNull('archived_at')
313 + ->orderBy('position', 'asc')
314 + ->get();
315 +
316 + foreach ($overrides as $index => $status) {
317 + $stage = $created[$index] ?? null;
318 + if (!$stage) {
319 + continue;
320 + }
321 +
322 + $settings = $stage->settings ?: [];
323 + if (($settings['default_task_status'] ?? '') === $status) {
324 + continue;
325 + }
326 +
327 + $settings['default_task_status'] = $status;
328 + $stage->settings = $settings;
329 + $stage->save();
330 + }
331 + }
332 +
333 + /**
334 + * Mirrors BoardController::createBoardLabelsFromRequest().
335 + */
336 + private static function createBoardLabels($labelService, $boardId, $labels)
337 + {
338 + if (!is_array($labels) || !$labels) {
339 + $labelService->createDefaultLabel($boardId);
340 + return;
341 + }
342 +
343 + foreach ($labels as $label) {
344 + if (!is_array($label)) {
345 + continue;
346 + }
347 +
348 + $labelData = Helper::sanitizeLabel([
349 + 'label' => $label['title'] ?? ($label['label'] ?? ''),
350 + 'bg_color' => $label['bg_color'] ?? '',
351 + 'color' => $label['color'] ?? '',
352 + 'color_preset' => $label['color_preset'] ?? '',
353 + ]);
354 +
355 + if (empty($labelData['label']) && empty($labelData['bg_color'])) {
356 + continue;
357 + }
358 +
359 + $labelService->createLabel([
360 + 'label' => $labelData['label'] ?? '',
361 + 'bg_color' => !empty($labelData['bg_color']) ? $labelData['bg_color'] : '#f3f4f6',
362 + 'color' => !empty($labelData['color']) ? $labelData['color'] : '#1B2533',
363 + 'color_preset' => $labelData['color_preset'] ?? '',
364 + ], $boardId);
365 + }
366 + }
367 +
368 + /**
369 + * Validate label preset ids before the board is persisted.
370 + *
371 + * @param mixed $labels
372 + * @return true|\WP_Error
373 + */
374 + private static function validateLabelPresets($labels)
375 + {
376 + if (!is_array($labels)) {
377 + return true;
378 + }
379 +
380 + foreach ($labels as $label) {
381 + if (!is_array($label)) {
382 + continue;
383 + }
384 +
385 + $labelData = Helper::sanitizeLabel([
386 + Constant::LABEL_COLOR_PRESET_SETTING => $label[Constant::LABEL_COLOR_PRESET_SETTING] ?? null,
387 + ]);
388 + $presetId = $labelData[Constant::LABEL_COLOR_PRESET_SETTING] ?? null;
389 +
390 + if ($presetId === null || $presetId === '') {
391 + continue;
392 + }
393 +
394 + if (!is_string($presetId) || !Constant::getLabelColorPreset($presetId)) {
395 + return MCPHelper::error('invalid_param', __('Invalid label color preset', 'fluent-boards'));
396 + }
397 + }
398 +
399 + return true;
400 + }
401 +
402 + private static function addBoardMembers($boardService, $boardId, $memberIds)
403 + {
404 + if (!$memberIds) {
405 + return;
406 + }
407 +
408 + $currentUserId = get_current_user_id();
409 +
410 + foreach ($memberIds as $memberId) {
411 + if ($memberId === $currentUserId) {
412 + continue;
413 + }
414 +
415 + $boardService->addMembersInBoard($boardId, $memberId);
416 + }
197 417 }
198 418 }