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/Http/Controllers/TaskController.php +138 -19 1.95.32.1.0 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Http\Controllers;
4 4
5 +use FluentBoards\Framework\Database\Orm\ModelNotFoundException;
5 6 use FluentBoards\App\Models\Meta;
6 7 use FluentBoards\App\Models\Stage;
7 8 use FluentBoards\App\Models\Task;
8 9 use FluentBoards\App\Models\Board;
@@ -37,10 +38,25 @@
37 38 public function getTopTasksForBoards()
38 39 {
39 40 $userId = get_current_user_id();
40 41 $task_ids = PermissionManager::getTaskIdsWatchByUser($userId);
41 - $tasksArray = $this->taskService->getTasksForBoards(['assigned', 'overdue', 'upcoming', 'completed', 'others'], 6, $task_ids);
42 - $taskCounts = $this->taskService->getTaskCountsForBoards(['assigned', 'overdue', 'upcoming', 'completed', 'others'], $task_ids);
42 + $boardIds = PermissionManager::getBoardIdsForUser($userId);
43 + $taskCategories = ['due_today', 'assigned', 'overdue', 'upcoming', 'mentioned', 'completed', 'others'];
44 + $tasksArray = $this->taskService->getTasksForBoards($taskCategories, 6, $task_ids);
45 + $taskCounts = $this->taskService->getTaskCountsForBoards($taskCategories, $task_ids);
46 + $taskCounts['all_boards'] = empty($boardIds)
47 + ? 0
48 + : (int) Board::whereIn('id', $boardIds)
49 + ->whereNull('archived_at')
50 + ->excludeTemplates()
51 + ->count();
52 + $taskCounts['all_tasks'] = empty($task_ids)
53 + ? 0
54 + : (int) Task::whereIn('id', $task_ids)
55 + ->whereNull('archived_at')
56 + ->whereNull('parent_id')
57 + ->onActiveAvailableBoards()
58 + ->count();
43 59
44 60 return [
45 61 'data' => $tasksArray,
46 62 'counts' => $taskCounts,
@@ -72,11 +88,9 @@
72 88 // Process each task
73 89 $this->processTasks($tasks, $board);
74 90
75 91 if ($board->type === 'roadmap') {
76 - foreach ($tasks as $task) {
77 - $task->vote_statistics = $this->taskService->getIdeaVoteStatistics($task->id);
78 - }
92 + $this->taskService->loadIdeaVoteStatistics($tasks);
79 93 }
80 94
81 95 return [
82 96 'tasks' => $tasks,
@@ -426,13 +440,32 @@
426 440 }
427 441 }
428 442 }
429 443
430 -
444 + /**
445 + * Create a task with field-specific sanitization for its request data.
446 + *
447 + * @param Request $request
448 + * @param int $board_id
449 + * @return mixed
450 + */
431 451 public function create(Request $request, $board_id)
432 452 {
433 453 $board_id = absint($board_id);
434 - $taskData = $this->taskSanitizeAndValidate($request->getSafe('task'), [
454 + $safeTaskData = $request->getSafe('task');
455 + $rawTaskData = $request->get('task', []);
456 +
457 + // Milkdown serializes pasted URLs as <https://...>, which generic text
458 + // sanitization removes as a tag.
459 + if (
460 + is_array($safeTaskData) &&
461 + is_array($rawTaskData) &&
462 + array_key_exists('description', $rawTaskData)
463 + ) {
464 + $safeTaskData['description'] = fluent_boards_sanitize_description($rawTaskData['description']);
465 + }
466 +
467 + $taskData = $this->taskSanitizeAndValidate($safeTaskData, [
435 468 'title' => 'required|string',
436 469 'board_id' => 'required|numeric',
437 470 'stage_id' => 'required|numeric',
438 471 'priority' => 'nullable|string',
@@ -440,17 +473,28 @@
440 473 'is_template' => 'string',
441 474 ]);
442 475
443 476 try {
477 + if (isset($taskData['assignees'])) {
478 + $taskData['assignees'] = array_filter(array_map('intval', (array) $taskData['assignees']));
479 + }
480 +
481 + if (isset($taskData['labels'])) {
482 + $taskData['labels'] = array_filter(array_map('intval', (array) $taskData['labels']));
483 + }
484 +
444 485 if ($taskData['board_id'] != $board_id) {
445 486 throw new \Exception(esc_html__('Board id is not valid', 'fluent-boards'));
446 487 }
447 488
448 489 $task = $this->taskService->createTask($taskData, $board_id);
490 + $message = $task->type === 'roadmap'
491 + ? __('Idea has been successfully created', 'fluent-boards')
492 + : __('Task has been successfully created', 'fluent-boards');
449 493
450 494 return $this->sendSuccess([
451 495 'task' => $task,
452 - 'message' => __('Task has been successfully created', 'fluent-boards'),
496 + 'message' => $message,
453 497 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($task->board_id)
454 498 ], 201);
455 499 } catch (\Exception $e) {
456 500 return $this->sendError($e->getMessage(), 400);
@@ -481,8 +525,9 @@
481 525
482 526 $task->load(['board', 'stage', 'labels', 'assignees','watchers']);
483 527
484 528 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
529 + $task->watchers = Helper::sanitizeUserCollections($task->watchers);
485 530
486 531 $task->isOverdue = $task->isOverdue();
487 532 $task->contact = Task::lead_contact($task->crm_contact_id);
488 533 $task->board->stages = $stageService->stagesByBoardId($board_id);
@@ -497,8 +542,10 @@
497 542 return [
498 543 'task' => $task
499 544 ];
500 545
546 + } catch (ModelNotFoundException $e) {
547 + throw $e;
501 548 } catch (\Exception $e ) {
502 549 return $this->sendError($e->getMessage(), 400);
503 550 }
504 551
@@ -710,9 +757,9 @@
710 757 $task_id = absint($task_id);
711 758 //Properties in col: settings, assignees,crm_contact_id, archived_at(AUTO_SET_TIMESTAMP) , status, title, description, priority, is_watching, is_template
712 759 $col = $request->getSafe('property', 'sanitize_text_field');
713 760 if ($col === 'description') {
714 - $value = $request->getSafe('value', 'wp_kses_post');
761 + $value = $request->getSafe('value', 'fluent_boards_sanitize_description');
715 762 } elseif ($col === 'settings' || $col === 'assignees') {
716 763 $value = $request->get('value');
717 764 if (is_array($value) && isset($value['cover']) && is_array($value['cover'])) {
718 765 if (isset($value['cover']['backgroundColor'])) {
@@ -718,8 +765,19 @@
718 765 if (isset($value['cover']['backgroundColor'])) {
719 766 $value['cover']['backgroundColor'] = sanitize_text_field($value['cover']['backgroundColor']);
720 767 }
721 768 }
769 + } elseif ($col === 'is_watching') {
770 + $value = $request->get('value');
771 + if (is_array($value)) {
772 + $action = isset($value['action']) ? sanitize_text_field($value['action']) : 'start';
773 + $value = [
774 + 'userId' => isset($value['userId']) ? absint($value['userId']) : 0,
775 + 'action' => in_array($action, ['start', 'stop'], true) ? $action : 'start',
776 + ];
777 + } else {
778 + $value = sanitize_text_field($value);
779 + }
722 780 } else {
723 781 $value = $request->getSafe('value', 'sanitize_text_field');
724 782 }
725 783
@@ -738,8 +796,12 @@
738 796 if ($col === 'parent_id' && $validatedData[$col]) {
739 797 $this->taskService->findTaskOnBoard($validatedData[$col], $board_id, false);
740 798 }
741 799
800 + if ($task->parent_id && $col === 'started_at') {
801 + $validatedData[$col] = null;
802 + }
803 +
742 804 $oldDateValue = null;
743 805 if (in_array($col, ['due_at', 'started_at'])) {
744 806 $oldDateValue = $task->{$col};
745 807 }
@@ -755,8 +817,13 @@
755 817 $task->contact = Helper::crm_contact($task->crm_contact_id);
756 818 $task->is_watching = $task->isWatching();
757 819 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
758 820
821 + if ($col === 'is_watching') {
822 + $task->load('watchers');
823 + $task->watchers = Helper::sanitizeUserCollections($task->watchers);
824 + }
825 +
759 826 if ($task->parent_id) {
760 827 $task->subtask_group_id = TaskMeta::where('task_id', $task->id)->where('key', Constant::SUBTASK_GROUP_CHILD)->value('value');
761 828 }
762 829
@@ -781,8 +848,28 @@
781 848 'updatedTasks' => $updatedTasks
782 849 ];
783 850 }
784 851
852 + /**
853 + * Remove a Fluent Support association from a task without deleting the ticket.
854 + *
855 + * @param int $board_id
856 + * @param int $task_id
857 + * @return mixed
858 + */
859 + public function removeSupportTicketLink($board_id, $task_id)
860 + {
861 + $boardId = absint($board_id);
862 + $taskId = absint($task_id);
863 + $task = $this->taskService->removeSupportTicketLink($taskId, $boardId);
864 +
865 + return $this->sendSuccess([
866 + 'message' => __('Support ticket link has been removed', 'fluent-boards'),
867 + 'task' => $task,
868 + 'updatedTasks' => [$task],
869 + ]);
870 + }
871 +
785 872 public function updateTaskDates(Request $request, $board_id, $task_id)
786 873 {
787 874 $board_id = absint($board_id);
788 875 $task_id = absint($task_id);
@@ -803,10 +890,16 @@
803 890 $hasRemindAt = array_key_exists('remind_at', $payload);
804 891
805 892 $startAt = $hasStartAt ? $request->getSafe('started_at', 'sanitize_text_field', NULL) : $task->started_at;
806 893 $dueAt = $hasDueAt ? $request->getSafe('due_at', 'sanitize_text_field', NULL) : $task->due_at;
894 + $isSubtask = (bool) $task->parent_id;
807 895
808 - if ($hasStartAt && $hasDueAt && $startAt && $dueAt) {
896 + if ($isSubtask) {
897 + $startAt = null;
898 + $hasStartAt = $hasStartAt || (bool) $task->started_at;
899 + }
900 +
901 + if (!$isSubtask && $hasStartAt && $hasDueAt && $startAt && $dueAt) {
809 902 if (strtotime($startAt) > strtotime($dueAt)) {
810 903 $startAt = substr($dueAt, 0, 10) . ' 00:00:00';
811 904 }
812 905 }
@@ -1031,8 +1124,11 @@
1031 1124 }
1032 1125
1033 1126 return [$col => $sanitizedAndValidatedValue];
1034 1127 }
1128 + if ('is_watching' == $col && is_array($value)) {
1129 + return [$col => $value];
1130 + }
1035 1131 $data = Helper::sanitizeTask([$col => $value]);
1036 1132
1037 1133 return $this->validate($data, [
1038 1134 $col => $rule,
@@ -1217,9 +1313,10 @@
1217 1313 // Pagination parameters
1218 1314 $page = $request->getSafe('page', 'intval', 1);
1219 1315 $perPage = $request->getSafe('per_page', 'intval', 10);
1220 1316 $filter = $request->getSafe('filter', 'sanitize_text_field', 'newest'); // Filter for comments and activities
1221 - $commentsAndActivities = $this->taskService->getCommentsAndActivities($task_id, $perPage, $page, $filter, $board_id);
1317 + $feedType = $request->getSafe('feed_type', 'sanitize_text_field', 'all');
1318 + $commentsAndActivities = $this->taskService->getCommentsAndActivities($task_id, $perPage, $page, $filter, $board_id, $feedType);
1222 1319 // Return the response with the task, paginated comments and activities, total count, current page, and items per page
1223 1320 return $this->sendSuccess([
1224 1321 'comments_and_activities' => $commentsAndActivities,
1225 1322 ]);
@@ -1317,12 +1414,16 @@
1317 1414 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1318 1415
1319 1416 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1320 1417 $task = $this->taskService->createTaskFromImage($board_id, $stageId, $uploadInfo, $file);
1418 + $message = $task->type === 'roadmap'
1419 + ? __('Idea has been created', 'fluent-boards')
1420 + : __('Task has been created', 'fluent-boards');
1421 +
1321 1422 return $this->sendSuccess([
1322 1423 'task' => $task,
1323 1424 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($board_id),
1324 - 'message' => __('Task has been created', 'fluent-boards'),
1425 + 'message' => $message,
1325 1426 ], 200);
1326 1427
1327 1428 }
1328 1429
@@ -1395,42 +1496,48 @@
1395 1496 public function getTaskTabsConfig()
1396 1497 {
1397 1498 $default_config = [
1398 1499 [
1500 + 'name' => 'due_today',
1501 + 'label' => __('Due Today', 'fluent-boards'),
1502 + 'visible' => 'true',
1503 + 'order' => 1
1504 + ],
1505 + [
1399 1506 'name' => 'assigned',
1400 1507 'label' => __('Assigned', 'fluent-boards'),
1401 1508 'visible' => 'true',
1402 - 'order' => 1
1509 + 'order' => 2
1403 1510 ],
1404 1511 [
1405 1512 'name' => 'upcoming',
1406 1513 'label' => __('Upcoming', 'fluent-boards'),
1407 1514 'visible' => 'true',
1408 - 'order' => 2
1515 + 'order' => 3
1409 1516 ],
1410 1517 [
1411 1518 'name' => 'overdue',
1412 1519 'label' => __('Overdue', 'fluent-boards'),
1413 1520 'visible' => 'true',
1414 - 'order' => 3
1521 + 'order' => 4
1415 1522 ],
1416 1523 [
1417 1524 'name' => 'mentioned',
1418 1525 'label' => __('Mentioned', 'fluent-boards'),
1419 1526 'visible' => 'true',
1420 - 'order' => 4
1527 + 'order' => 5
1421 1528 ],
1422 1529 [
1423 1530 'name' => 'completed',
1424 1531 'label' => __('Completed', 'fluent-boards'),
1425 1532 'visible' => 'true',
1426 - 'order' => 5
1533 + 'order' => 6
1427 1534 ],
1428 1535 [
1429 1536 'name' => 'others',
1430 1537 'label' => __('Others', 'fluent-boards'),
1431 1538 'visible' => 'true',
1432 - 'order' => 6
1539 + 'order' => 7
1433 1540 ]
1434 1541 ];
1435 1542 $availableTabNames = array_column($default_config, 'name');
1436 1543
@@ -1459,10 +1566,21 @@
1459 1566
1460 1567 if (!empty($missingTabs)) {
1461 1568 $newConfig = [];
1462 1569 $order = 1;
1570 + $addedDueToday = false;
1463 1571 $addedAssigned = false;
1464 1572 foreach ($config as $tab) {
1573 + if (!$addedDueToday) {
1574 + $dueTodayTab = array_filter($missingTabs, fn($t) => $t['name'] === 'due_today');
1575 + if (!empty($dueTodayTab)) {
1576 + $dueTodayTab = reset($dueTodayTab);
1577 + $dueTodayTab['order'] = $order++;
1578 + $newConfig[] = $dueTodayTab;
1579 + $addedDueToday = true;
1580 + }
1581 + }
1582 +
1465 1583 if ($tab['name'] === 'upcoming' && !$addedAssigned) {
1466 1584 $assignedTab = array_filter($missingTabs, fn($t) => $t['name'] === 'assigned');
1467 1585 if (!empty($assignedTab)) {
1468 1586 $assignedTab = reset($assignedTab);
@@ -1474,9 +1592,9 @@
1474 1592 $tab['order'] = $order++;
1475 1593 $newConfig[] = $tab;
1476 1594 }
1477 1595 foreach ($missingTabs as $missingTab) {
1478 - if ($missingTab['name'] !== 'assigned') {
1596 + if (!in_array($missingTab['name'], ['assigned', 'due_today'], true)) {
1479 1597 $missingTab['order'] = $order++;
1480 1598 $newConfig[] = $missingTab;
1481 1599 }
1482 1600 }
@@ -1491,8 +1609,9 @@
1491 1609 }
1492 1610
1493 1611 // Always apply fresh translations based on tab name
1494 1612 $labelMap = [
1613 + 'due_today' => __('Due Today', 'fluent-boards'),
1495 1614 'assigned' => __('Assigned', 'fluent-boards'),
1496 1615 'upcoming' => __('Upcoming', 'fluent-boards'),
1497 1616 'overdue' => __('Overdue', 'fluent-boards'),
1498 1617 'mentioned' => __('Mentioned', 'fluent-boards'),