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/Services/BoardService.php +285 -186 1.95.32.1.0 View file →
@@ -2,10 +2,12 @@
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 5 use FluentBoards\App\Models\Activity;
6 +use FluentBoards\App\Models\Attachment;
6 7 use FluentBoards\App\Models\Board;
7 8 use FluentBoards\App\Models\Comment;
9 +use FluentBoards\App\Models\Folder;
8 10 use FluentBoards\App\Models\Label;
9 11 use FluentBoards\App\Models\Meta;
10 12 use FluentBoards\App\Models\Relation;
11 13 use FluentBoards\App\Models\Stage;
@@ -12,12 +14,14 @@
12 14 use FluentBoards\App\Models\Task;
13 15 use FluentBoards\App\Models\TaskMeta;
14 16 use FluentBoards\App\Models\User;
15 17 use FluentBoards\App\Services\Libs\FileSystem;
16 -use FluentBoardsPro\App\Models\Folder;
18 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
17 19
18 20 class BoardService
19 21 {
22 + private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
23 +
20 24 public function getBoardsByType($type)
21 25 {
22 26 return Board::where('type', sanitize_text_field($type))
23 27 ->whereNull('archived_at')
@@ -146,9 +150,9 @@
146 150 {
147 151 $boardData = [
148 152 'title' => $boardData['title'],
149 153 'type' => $boardData['type'] ? $boardData['type'] : 'to-do',
150 - 'description' => $boardData['description'],
154 + 'description' => DescriptionMarkdownConverter::normalize($boardData['description'] ?? ''),
151 155 'currency' => isset($boardData['currency']) ? $boardData['currency'] : 'USD',
152 156 'background' => isset($boardData['background']) ? $boardData['background'] : '',
153 157 'created_by' => isset($boardData['created_by']) ? $boardData['created_by'] : get_current_user_id()
154 158 ];
@@ -161,12 +165,23 @@
161 165
162 166 return $board;
163 167 }
164 168
169 + /**
170 + * Attach a user-owned board to its creator with Board Admin preferences.
171 + *
172 + * @param Board $board
173 + * @return void
174 + */
165 175 public function setCurrentUserPreferencesOnBoardCreate($board)
166 176 {
177 + $creatorId = absint($board->created_by);
178 + if (!$creatorId) {
179 + return;
180 + }
181 +
167 182 $board->users()->attach(
168 - $board->created_by,
183 + $creatorId,
169 184 [
170 185 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
171 186 'settings' => maybe_serialize([
172 187 Constant::IS_BOARD_ADMIN => true
@@ -214,9 +229,18 @@
214 229 ->first();
215 230 if ($recentlyOpened) {
216 231 $recentBoardIds = $recentlyOpened->value;
217 232
233 + // Recently opened meta can be empty or legacy-shaped; only splice a usable board ID list.
234 + if (!is_array($recentBoardIds)) {
235 + return;
236 + }
237 +
218 238 $index = array_search($boardId, $recentBoardIds);
239 + if ($index === false) {
240 + return;
241 + }
242 +
219 243 array_splice($recentBoardIds, $index, 1);
220 244
221 245 $recentlyOpened->value = $recentBoardIds;
222 246 $recentlyOpened->save();
@@ -231,9 +255,9 @@
231 255 } else {
232 256 throw new \Exception(esc_html__('Title cannot be empty', 'fluent-boards'));
233 257 }
234 258 if (isset($data['description'])) {
235 - $data['description'] = $data['description'];
259 + $data['description'] = DescriptionMarkdownConverter::normalize($data['description']);
236 260 }
237 261 $board->fill($data);
238 262 $board->save();
239 263 // do_action('fluent_boards/board_updated', $board);
@@ -290,12 +314,19 @@
290 314 }
291 315 return $processedStages;
292 316 }
293 317
318 + /**
319 + * Archive a stage and persist the user who archived it for future archive-list metadata.
320 + */
294 321 public function archiveStage($boardId, $stage)
295 322 {
323 + $settings = $stage->settings ?: [];
324 + $settings['archived_by_id'] = absint(get_current_user_id()) ?: null;
325 +
296 326 $stage->archived_at = current_time('mysql');
297 327 $stage->position = 0;
328 + $stage->settings = $settings;
298 329 $stage->save();
299 330
300 331 do_action('fluent_boards/stage_archived', $boardId, $stage); // Old hook
301 332 do_action('fluent_boards/stage_archived_with_tasks', $boardId, $stage); // New hook
@@ -301,14 +332,21 @@
301 332 do_action('fluent_boards/stage_archived_with_tasks', $boardId, $stage); // New hook
302 333 return $stage;
303 334 }
304 335
336 + /**
337 + * Restore an archived stage and clear stale archived-by metadata.
338 + */
305 339 public function restoreStage($boardId, $stage)
306 340 {
307 341 $stageService = new StageService();
308 342 $lastStagePosition = $stageService->getLastPositionOfStagesOfBoard($stage->board_id);
343 + $settings = $stage->settings ?: [];
344 + $settings['archived_by_id'] = null;
345 +
309 346 $stage->archived_at = null;
310 347 $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
348 + $stage->settings = $settings;
311 349 $stage->save();
312 350 do_action('fluent_boards/board_stage_restored', $boardId, $stage->title); // Old hook
313 351 do_action('fluent_boards/stage_restored_with_tasks', $boardId, $stage); // New hook
314 352 return $stage;
@@ -335,14 +373,29 @@
335 373
336 374 return $isAlreadyMember ?? false;
337 375 }
338 376
377 + /**
378 + * Add a WordPress user to a board.
379 + *
380 + * @return User|false|null User on success, false for an existing relation,
381 + * or null when the board/user does not exist.
382 + */
339 383 public function addMembersInBoard($boardId, $memberId, $isViewerOnly = null)
340 384 {
385 + $boardId = intval($boardId);
386 + $memberId = intval($memberId);
387 + $isViewerOnly = sanitize_text_field((string)$isViewerOnly);
388 +
389 + if ($boardId <= 0 || $memberId <= 0) {
390 + return null;
391 + }
392 +
341 393 $board = Board::find($boardId);
394 + $boardMember = User::find($memberId);
342 395
343 - if (!$board) {
344 - return false;
396 + if (!$board || !$boardMember) {
397 + return null;
345 398 }
346 399 $isAlreadyMember = $this->isAlreadyMember($boardId, $memberId);
347 400 if($isAlreadyMember) {
348 401 return false;
@@ -362,9 +415,8 @@
362 415 'settings' => maybe_serialize($settings),
363 416 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
364 417 ]
365 418 );
366 - $boardMember = User::find($memberId);
367 419 if(!$isViewerOnly) {
368 420 do_action('fluent_boards/board_member_added', $boardId, $boardMember);
369 421 } else {
370 422 do_action('fluent_boards/board_viewer_added', $boardId, $boardMember);
@@ -406,8 +458,75 @@
406 458 $user['is_board_admin'] = false;
407 459 return $user;
408 460 }
409 461
462 + /**
463 + * Create or update a board access relation with the selected member role.
464 + */
465 + public function syncBoardUserRole($boardId, $userId, $role)
466 + {
467 + $boardId = absint($boardId);
468 + $userId = absint($userId);
469 + $role = sanitize_text_field($role);
470 +
471 + if (!$boardId || !$userId || !in_array($role, ['admin', 'member', 'viewer'], true)) {
472 + return false;
473 + }
474 +
475 + $board = Board::find($boardId);
476 + $user = User::find($userId);
477 +
478 + if (!$board || !$user) {
479 + return false;
480 + }
481 +
482 + $boardUser = Relation::where('object_id', $boardId)
483 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
484 + ->where('foreign_id', $userId)
485 + ->first();
486 +
487 + $previousSettings = $boardUser ? (array)$boardUser->settings : [];
488 +
489 + // Board roles live as flags on the board_user relation; member access means both flags stay false.
490 + $settings = [
491 + 'is_admin' => 'admin' === $role,
492 + 'is_viewer_only' => 'viewer' === $role,
493 + ];
494 +
495 + if ($boardUser) {
496 + $boardUser->settings = $settings;
497 + $boardUser->save();
498 + } else {
499 + // New access should get the same default notification preferences as the normal add-member flow.
500 + $board->users()->attach(
501 + $userId,
502 + [
503 + 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
504 + 'settings' => maybe_serialize($settings),
505 + 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
506 + ]
507 + );
508 + }
509 +
510 + // Only emit admin transition hooks when the role actually changes.
511 + if ('admin' === $role && empty($previousSettings['is_admin'])) {
512 + do_action('fluent_boards/board_admin_added', $boardId, $userId);
513 + } elseif (!empty($previousSettings['is_admin'])) {
514 + do_action('fluent_boards/board_admin_removed', $boardId, $userId);
515 + }
516 +
517 + if ('viewer' === $role) {
518 + do_action('fluent_boards/board_viewer_added', $boardId, $user);
519 + } elseif ('member' === $role) {
520 + do_action('fluent_boards/board_member_added', $boardId, $user);
521 + }
522 +
523 + $user['is_admin'] = 'admin' === $role;
524 + $user['is_board_admin'] = 'admin' === $role;
525 +
526 + return $user;
527 + }
528 +
410 529 public function getUsersOfBoards()
411 530 {
412 531 $userBoards = Relation::whereNotNull('board_id')
413 532 ->where('user_id', get_current_user_id())
@@ -416,37 +535,68 @@
416 535 return $userBoards;
417 536 }
418 537
419 538 /**
420 - * change board background
421 - * @param mixed $backgroundData
422 - * @return string
539 + * Change or clear the board background.
540 + *
541 + * Image attachments must belong to the target board and use the board
542 + * background attachment type before their identifiers can be persisted.
543 + *
544 + * @param array $backgroundData
545 + * @param int $board_id
546 + * @return array|string
547 + * @throws \Exception
423 548 */
424 -
425 549 public function setBoardBackground($backgroundData, $board_id)
426 550 {
427 - $board = Board::find($board_id);
551 + $boardId = absint($board_id);
552 + $board = Board::find($boardId);
553 +
554 + if (!$board) {
555 + throw new \Exception(esc_html__('Board not found.', 'fluent-boards'));
556 + }
557 +
428 558 $oldBackground = $board->background;
559 +
560 + if (!empty($backgroundData['reset'])) {
561 + $board->background = '';
562 + $board->save();
563 + do_action('fluent_boards/board_background_updated', $boardId, $oldBackground);
564 +
565 + return $board->background;
566 + }
567 +
429 568 $background = $board->background;
430 -
431 - // if board background has color
432 - if (isset($backgroundData['color'])) {
433 - $background['color'] = $backgroundData['color'];
434 - $background['image_url'] = null;
435 - $background['is_image'] = false;
569 + if (!is_array($background)) {
570 + $background = [];
436 571 }
437 572
438 - // if board background has image
573 + // Resolve image metadata from the board-owned attachment, never from the client URL.
439 574 if (isset($backgroundData['image_url'])) {
440 - $background['image_url'] = $backgroundData['image_url'];
575 + $attachmentId = absint($backgroundData['id'] ?? 0);
576 + $attachment = Attachment::where('id', $attachmentId)
577 + ->where('object_id', $boardId)
578 + ->where('object_type', Constant::BOARD_BACKGROUND_IMAGE)
579 + ->first();
580 +
581 + if (!$attachment) {
582 + throw new \Exception(esc_html__('Background image not found.', 'fluent-boards'));
583 + }
584 +
585 + $background['id'] = (int) $attachment->id;
586 + $background['image_url'] = (new CommentService())->createPublicUrl($attachment, $boardId);
441 587 $background['is_image'] = true;
442 588 $background['color'] = null;
589 + } elseif (isset($backgroundData['color'])) {
590 + $background['id'] = $backgroundData['id'];
591 + $background['color'] = $backgroundData['color'];
592 + $background['image_url'] = null;
593 + $background['is_image'] = false;
443 594 }
444 - $background['id'] = $backgroundData['id'];
445 595
446 596 $board->background = $background;
447 597 $board->save();
448 - do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
598 + do_action('fluent_boards/board_background_updated', $boardId, $oldBackground);
449 599
450 600 return $board->background;
451 601 }
452 602
@@ -659,9 +809,15 @@
659 809
660 810 $this->updateRecentBoardCheckMeta();
661 811 }
662 812
663 - return Board::whereIn('id', $recentBoardIds)->withCount('completedTasks')->with(['stages', 'users'])->get();
813 + return Board::whereIn('id', $recentBoardIds)
814 + ->whereNull('archived_at')
815 + ->excludeTemplates()
816 + ->availableInCurrentInstall()
817 + ->withCount('completedTasks')
818 + ->with(['stages', 'users'])
819 + ->get();
664 820 }
665 821
666 822 public function getRecentBoardCheckMeta($userId = null){
667 823 if (!$userId) {
@@ -759,10 +915,15 @@
759 915
760 916 $contactOfBoard->delete();
761 917 }
762 918
763 - public function sendInvitationToBoard($boardId, $email)
919 + public function sendInvitationToBoard($boardId, $email, $role = 'member')
764 920 {
921 + $role = sanitize_text_field($role);
922 + if (!in_array($role, ['manager', 'member', 'viewer'], true)) {
923 + $role = 'member';
924 + }
925 +
765 926 $user = User::query()->where('user_email', $email)->first();
766 927
767 928 if ($user) {
768 929 return $user;
@@ -769,9 +930,9 @@
769 930 }
770 931
771 932 $current_user_id = get_current_user_id();
772 933
773 - do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id);
934 + do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id, $role);
774 935
775 936 return;
776 937
777 938 }
@@ -783,11 +944,35 @@
783 944 ->where('key', Constant::BOARD_INVITATION)
784 945 ->get();
785 946 }
786 947
787 - public function deleteInvitation($invitationId)
948 + /**
949 + * Delete an invitation only when it belongs to the supplied board.
950 + *
951 + * The optional second argument lets older Pro releases receive a controlled
952 + * error instead of reporting a successful deletion that never happened.
953 + */
954 + public function deleteInvitation($boardId, $invitationId = null)
788 955 {
789 - Meta::findOrFail($invitationId)->delete();
956 + if ($invitationId === null) {
957 + throw new \Exception(
958 + __('A board ID is required to delete an invitation.', 'fluent-boards')
959 + );
960 + }
961 +
962 + $boardId = intval($boardId);
963 + $invitationId = intval($invitationId);
964 +
965 + if ($boardId <= 0 || $invitationId <= 0) {
966 + return false;
967 + }
968 +
969 + return (bool) Meta::query()
970 + ->where('id', $invitationId)
971 + ->where('object_id', $boardId)
972 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
973 + ->where('key', Constant::BOARD_INVITATION)
974 + ->delete();
790 975 }
791 976
792 977 public function hasDataChanged($boardId, $includeArchived = false, $since = null)
793 978 {
@@ -955,8 +1140,15 @@
955 1140
956 1141 return (string) $timestamp;
957 1142 }
958 1143
1144 + /**
1145 + * Get CRM-associated boards that the current user can access.
1146 + *
1147 + * @param int $associatedId CRM contact/subscriber id.
1148 + * @param int|null $userId WordPress user id used for board access checks.
1149 + * @return \FluentBoards\Framework\Database\Orm\Collection|array
1150 + */
959 1151 public function getAssociatedBoards($associatedId, $userId = null)
960 1152 {
961 1153 $associatedId = absint($associatedId);
962 1154 $userId = $userId ?: get_current_user_id();
@@ -966,16 +1158,28 @@
966 1158 }
967 1159
968 1160 $boardIds = Meta::query()->where('value', $associatedId)
969 1161 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
970 - ->where('key', Constant::BOARD_ASSOCIATED_CRM_CONTACT)
1162 + ->whereIn('key', [
1163 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1164 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1165 + ])
971 1166 ->pluck('object_id');
972 1167
973 - return Board::query()
974 - ->whereIn('id', $boardIds)
1168 + $boards = Board::query()
1169 + ->whereIn('id', array_values(array_unique(array_map('intval', $boardIds->toArray()))))
1170 + ->whereNull('archived_at')
975 1171 ->byAccessUser($userId)
976 - ->with('stages', 'users')
1172 + ->withCount('completedTasks')
1173 + ->with(['stages', 'users'])
1174 + ->orderBy('created_at', 'DESC')
977 1175 ->get();
1176 +
1177 + foreach ($boards as $board) {
1178 + $board->users = Helper::sanitizeUserCollections($board->users);
1179 + }
1180 +
1181 + return $boards;
978 1182 }
979 1183
980 1184 private function deleteBoardMeta($boardId)
981 1185 {
@@ -987,8 +1191,11 @@
987 1191 public function copyBoard($boardData)
988 1192 {
989 1193 $sourceBoard = Board::findOrFail($boardData['source_board_id']);
990 1194 $boardData['background'] = $sourceBoard->background;
1195 + if (isset($boardData['description'])) {
1196 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
1197 + }
991 1198 $boardData = apply_filters('fluent_boards/before_create_board', $boardData);
992 1199
993 1200 $board = Board::create($boardData);
994 1201
@@ -996,162 +1203,8 @@
996 1203
997 1204 return $board;
998 1205 }
999 1206
1000 - public function getBoardReports($board_id)
1001 - {
1002 - $board = Board::findOrFail($board_id);
1003 - $taskQuery = Task::where('board_id', $board_id)
1004 - ->whereNull('parent_id')
1005 - ->whereNull('archived_at');
1006 -
1007 - if($board->type == 'roadmap') {
1008 - $pendingStage = $this->getNewIdeaStage($board->id);
1009 - return $this->getIdeaReports($taskQuery, $pendingStage);
1010 - } else {
1011 - return $this->getTaskReports($taskQuery);
1012 - }
1013 - }
1014 -
1015 - private function getNewIdeaStage($boardId)
1016 - {
1017 - return Stage::where('board_id', $boardId)
1018 - ->where('type', 'stage')
1019 - ->where('archived_at', null)
1020 - ->orderBy('position', 'ASC')
1021 - ->first();
1022 - }
1023 -
1024 - public function getAllBoardReports(){
1025 - $userId = get_current_user_id();
1026 -
1027 - $taskQuery = Task::whereNull('parent_id')
1028 - ->whereNull('archived_at')
1029 - ->whereHas('board', function ($query) {
1030 - $query->where('type', 'to-do');
1031 - });
1032 -
1033 - if (!PermissionManager::isAdmin($userId))
1034 - {
1035 - $currentUser = User::find($userId);
1036 - $relatedBoardIds = $currentUser->whichBoards->where('type', 'to-do')->pluck('id');
1037 - $taskQuery->whereIn('board_id', $relatedBoardIds);
1038 - }
1039 -
1040 - return $this->getTaskReports($taskQuery);
1041 - }
1042 -
1043 - private function getTaskReports($taskQuery)
1044 - {
1045 - $totalTasksQuery = clone $taskQuery;
1046 - $completedTaskQuery = clone $taskQuery;
1047 - $openTaskQuery = clone $taskQuery;
1048 - $overDueTaskQuery = clone $taskQuery;
1049 -
1050 - $completedTaskCount = $completedTaskQuery->where('status', 'closed')->count();
1051 - $openTaskCount = $openTaskQuery->where('status', 'open')->count();
1052 - $overDueTasks = $overDueTaskQuery->overdue(true)->count();
1053 - $totalTasks = $totalTasksQuery->count();
1054 -
1055 - $taskQuery->where('status', 'open');
1056 -
1057 - $highQuery = clone $taskQuery;
1058 - $mediumQuery = clone $taskQuery;
1059 - $lowQuery = clone $taskQuery;
1060 -
1061 - $high = $highQuery->where('priority', 'high')->count();
1062 - $low = $mediumQuery->where('priority', 'low')->count();
1063 - $medium = $lowQuery->where('priority', 'medium')->count();
1064 -
1065 - $reportData = [
1066 - 'completion' => [
1067 - 'completed' => $completedTaskCount,
1068 - 'incomplete' => $openTaskCount,
1069 - 'overdue' => $overDueTasks,
1070 - 'total' => $totalTasks
1071 - ],
1072 - 'priority' => [
1073 - 'high' => $high,
1074 - 'medium' => $medium,
1075 - 'low' => $low
1076 - ]
1077 -
1078 - ];
1079 - return $reportData;
1080 - }
1081 -
1082 - private function getIdeaReports($taskQuery, $pendingStage)
1083 - {
1084 - $pendingIdeaQuery = clone $taskQuery;
1085 - $completedIdeaQuery = clone $taskQuery;
1086 - $openIdeaQueryPage = clone $taskQuery;
1087 - $openIdeaQueryWeb = clone $taskQuery;
1088 -
1089 - $pendingIdeaCount = $pendingIdeaQuery->where('status', 'open')->where('stage_id', $pendingStage->id)->count();
1090 - $completedIdeaCount = $completedIdeaQuery->where('status', 'closed')->count();
1091 - $openIdeaCountPage = $openIdeaQueryPage->where('status', 'open')->where('source', 'page')->count();
1092 - $openIdeaCountWeb = $openIdeaQueryWeb->where('status', 'open')->where('source', 'web')->count();
1093 - $totalIdeas = $openIdeaCountPage + $openIdeaCountWeb;
1094 -
1095 - $taskQuery->where('status', 'open');
1096 -
1097 - $highQuery = clone $taskQuery;
1098 - $mediumQuery = clone $taskQuery;
1099 - $lowQuery = clone $taskQuery;
1100 -
1101 - $high = $highQuery->where('priority', 'high')->count();
1102 - $low = $mediumQuery->where('priority', 'low')->count();
1103 - $medium = $lowQuery->where('priority', 'medium')->count();
1104 -
1105 - $reportData = [
1106 - 'completion' => [
1107 - 'pending' => $pendingIdeaCount,
1108 - 'completed' => $completedIdeaCount,
1109 - 'ideaFromPage' => $openIdeaCountPage,
1110 - 'total' => $totalIdeas
1111 - ],
1112 - 'priority' => [
1113 - 'high' => $high,
1114 - 'medium' => $medium,
1115 - 'low' => $low
1116 - ]
1117 - ];
1118 - return $reportData;
1119 - }
1120 -
1121 - public function getStageWiseBoardReports($board_id)
1122 - {
1123 - $stages = Stage::where('board_id', $board_id)
1124 - ->where('type', 'stage')
1125 - ->whereNull('archived_at')
1126 - ->get();
1127 -
1128 - foreach ($stages as $stage) {
1129 - $completedTaskCount = Task::where('stage_id', $stage->id)
1130 - ->where('status', 'closed')
1131 - ->count();
1132 -
1133 - $openTaskCount = Task::where('stage_id', $stage->id)
1134 - ->whereNull('due_at')
1135 - ->where('status', 'open')
1136 - ->count();
1137 -
1138 - $overDue = Task::where('stage_id', $stage->id)
1139 - ->whereNotNull('due_at')
1140 - ->where('status', 'open')
1141 - ->overdue(true)
1142 - ->count();
1143 -
1144 - $stage->report = [
1145 - 'completed' => $completedTaskCount,
1146 - 'incomplete' => $openTaskCount,
1147 - 'overdue' => $overDue
1148 - ];
1149 - }
1150 -
1151 - return $stages;
1152 - }
1153 -
1154 1207 public function archiveBoard($boardId)
1155 1208 {
1156 1209 $board = Board::findOrFail($boardId);
1157 1210 $board->archived_at = current_time('mysql');
@@ -1220,8 +1273,54 @@
1220 1273
1221 1274 return $pinnedBoardMeta;
1222 1275 }
1223 1276
1277 + /**
1278 + * Sidebar counts cover every board the user can access, so they are counted
1279 + * with their own queries rather than derived from the filtered/paginated list.
1280 + *
1281 + * byAccessUser() re-reads the user's accessible board ids from the database on
1282 + * every call, so the access scope is resolved once and cloned per count.
1283 + *
1284 + * @return array{all: int, pinned: int, archived: int}
1285 + */
1286 + public function getBoardCounts($userId)
1287 + {
1288 + $baseQuery = Board::byAccessUser($userId)
1289 + ->excludeTemplates()
1290 + ->availableInCurrentInstall();
1291 +
1292 + $counts = [
1293 + 'all' => (clone $baseQuery)->whereNull('archived_at')->count(),
1294 + 'pinned' => 0,
1295 + 'archived' => (clone $baseQuery)->whereNotNull('archived_at')->count()
1296 + ];
1297 +
1298 + $pinnedIds = $this->getPinnedBoardIds();
1299 +
1300 + if ($pinnedIds) {
1301 + $counts['pinned'] = (clone $baseQuery)->whereNull('archived_at')
1302 + ->whereIn('id', $pinnedIds)
1303 + ->count();
1304 + }
1305 +
1306 + return $counts;
1307 + }
1308 +
1309 + /**
1310 + * @return array board ids the current user has pinned
1311 + */
1312 + public function getPinnedBoardIds()
1313 + {
1314 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1315 +
1316 + if (!$pinnedBoardMeta) {
1317 + return [];
1318 + }
1319 +
1320 + return array_map('intval', (array) $pinnedBoardMeta->value);
1321 + }
1322 +
1224 1323 public function getPinnedBoards()
1225 1324 {
1226 1325 $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1227 1326
@@ -1319,9 +1418,9 @@
1319 1418 if (!$relation) {
1320 1419 return null;
1321 1420 }
1322 1421
1323 - return Folder::findOrFail($relation->object_id);
1422 + return Folder::find($relation->object_id);
1324 1423 }
1325 1424
1326 1425 public function deleteWebhookData($boardId)
1327 1426 {