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 +694 -271 1.412.1.0 View file →
@@ -2,24 +2,33 @@
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 5 use FluentBoards\App\Models\Activity;
6 -use FluentBoards\App\Models\Stage;
7 -use FluentBoards\App\Models\User;
6 +use FluentBoards\App\Models\Attachment;
8 7 use FluentBoards\App\Models\Board;
8 +use FluentBoards\App\Models\Comment;
9 +use FluentBoards\App\Models\Folder;
10 +use FluentBoards\App\Models\Label;
11 +use FluentBoards\App\Models\Meta;
9 12 use FluentBoards\App\Models\Relation;
13 +use FluentBoards\App\Models\Stage;
10 14 use FluentBoards\App\Models\Task;
11 -use FluentBoards\App\Models\Meta;
12 15 use FluentBoards\App\Models\TaskMeta;
16 +use FluentBoards\App\Models\User;
13 17 use FluentBoards\App\Services\Libs\FileSystem;
14 -use FluentBoards\Framework\Support\Arr;
15 -use FluentRoadmap\App\Models\Idea;
18 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
16 19
17 20 class BoardService
18 21 {
22 + private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
23 +
19 24 public function getBoardsByType($type)
20 25 {
21 - return Board::where('type', $type)->whereNull('archived_at')->orderBy('created_at', 'ASC')->get();
26 + return Board::where('type', sanitize_text_field($type))
27 + ->whereNull('archived_at')
28 + ->byAccessUser(get_current_user_id())
29 + ->orderBy('created_at', 'ASC')
30 + ->get();
22 31 }
23 32
24 33 public function deleteBoard($boardId)
25 34 {
@@ -34,8 +43,11 @@
34 43 $taskRelatedRelations = Relation::whereIn('object_id', $allTaskIdsInBoard);
35 44 $taskRelatedRelations->delete();
36 45 TaskMeta::whereIn('task_id', $allTaskIdsInBoard)->delete();
37 46
47 + // Delete time tracking records for all tasks in the board
48 + (new TaskService())->deleteTimeTrackingRecords($allTaskIdsInBoard->toArray());
49 +
38 50 Task::whereIn('id', $allTaskIdsInBoard)->delete();
39 51
40 52 // delete all activities
41 53 Activity::whereIn('object_id', $allTaskIdsInBoard)->where('object_type', Constant::ACTIVITY_TASK)->delete();
@@ -52,15 +64,25 @@
52 64
53 65 //removing add board labels
54 66 $board->labels()->delete();
55 67
56 - //removing add board comment
57 - $board->comments()->delete();
68 + //removing all board comments (delete individually to fire model events and clean up images)
69 + $comments = $board->comments()->get();
70 + foreach ($comments as $comment) {
71 + $comment->delete();
72 + }
73 +
74 + //removing add board custom fields
75 + if (defined('FLUENT_BOARDS_PRO')) {
76 + $board->customFields()->delete();
77 + }
58 78
79 +
59 80 foreach ($board->notifications as $notification) {
60 81 $notification->users()->detach();
61 82 }
62 83 $board->notifications()->delete();
84 + $board->removeBoardFromFolder();
63 85
64 86 //delete board related meta
65 87 $this->deleteBoardMeta($boardId);
66 88
@@ -66,11 +88,13 @@
66 88
67 89 //delete from recently viewed
68 90 $this->deleteFromRecentlyViewed($boardId);
69 91
92 + //delete webhook data
93 + $this->deleteWebhookData($boardId);
94 +
70 95 $board->delete();
71 96 FileSystem::deleteDir('board_'.$boardId);
72 -// do_action('fluent_boards/board_deleted', $board);
73 97 }
74 98
75 99 public function fetchBoardMeta($boardId)
76 100 {
@@ -126,9 +150,9 @@
126 150 {
127 151 $boardData = [
128 152 'title' => $boardData['title'],
129 153 'type' => $boardData['type'] ? $boardData['type'] : 'to-do',
130 - 'description' => $boardData['description'],
154 + 'description' => DescriptionMarkdownConverter::normalize($boardData['description'] ?? ''),
131 155 'currency' => isset($boardData['currency']) ? $boardData['currency'] : 'USD',
132 156 'background' => isset($boardData['background']) ? $boardData['background'] : '',
133 157 'created_by' => isset($boardData['created_by']) ? $boardData['created_by'] : get_current_user_id()
134 158 ];
@@ -141,12 +165,23 @@
141 165
142 166 return $board;
143 167 }
144 168
145 - private function setCurrentUserPreferencesOnBoardCreate($board)
169 + /**
170 + * Attach a user-owned board to its creator with Board Admin preferences.
171 + *
172 + * @param Board $board
173 + * @return void
174 + */
175 + public function setCurrentUserPreferencesOnBoardCreate($board)
146 176 {
177 + $creatorId = absint($board->created_by);
178 + if (!$creatorId) {
179 + return;
180 + }
181 +
147 182 $board->users()->attach(
148 - $board->created_by,
183 + $creatorId,
149 184 [
150 185 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
151 186 'settings' => maybe_serialize([
152 187 Constant::IS_BOARD_ADMIN => true
@@ -194,9 +229,18 @@
194 229 ->first();
195 230 if ($recentlyOpened) {
196 231 $recentBoardIds = $recentlyOpened->value;
197 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 +
198 238 $index = array_search($boardId, $recentBoardIds);
239 + if ($index === false) {
240 + return;
241 + }
242 +
199 243 array_splice($recentBoardIds, $index, 1);
200 244
201 245 $recentlyOpened->value = $recentBoardIds;
202 246 $recentlyOpened->save();
@@ -208,12 +252,12 @@
208 252 {
209 253 if ($data['title']) {
210 254 $data['title'] = $data['title'];
211 255 } else {
212 - throw new \Exception('Title cannot be empty');
256 + throw new \Exception(esc_html__('Title cannot be empty', 'fluent-boards'));
213 257 }
214 258 if (isset($data['description'])) {
215 - $data['description'] = $data['description'];
259 + $data['description'] = DescriptionMarkdownConverter::normalize($data['description']);
216 260 }
217 261 $board->fill($data);
218 262 $board->save();
219 263 // do_action('fluent_boards/board_updated', $board);
@@ -239,50 +283,10 @@
239 283
240 284 return serialize($this->processStages($stages));
241 285 }
242 286
243 - public function changePositionOfStage($boardId, $changeData)
244 - {
245 - $allStages = Stage::where('board_id', $boardId)
246 - ->where('is_archived', 0)
247 - ->where('type', 'stage')
248 - ->orderBy('position', 'asc')
249 - ->get();
250 287
251 - $changedStage = null;
252 -
253 - foreach ($allStages as $stage) {
254 - if ($changeData['fromPosition'] < $changeData['toPosition']) {
255 - if ($stage['position'] == $changeData['fromPosition']) {
256 - $stage['position'] = (int)$changeData['toPosition'];
257 - $stage->save();
258 - $changedStage = $stage;
259 - } elseif (
260 - $stage['position'] > $changeData['fromPosition'] &&
261 - $stage['position'] <= $changeData['toPosition']
262 - ) {
263 - $stage['position'] = $stage->position - 1;
264 - $stage->save();
265 - }
266 - } else {
267 - if (
268 - $stage['position'] >= $changeData['toPosition'] &&
269 - $stage['position'] < $changeData['fromPosition']
270 - ) {
271 - $stage['position'] = $stage->position + 1;
272 - $stage->save();
273 - } elseif ($stage['position'] == $changeData['fromPosition']) {
274 - $stage['position'] = (int)$changeData['toPosition'];
275 - $stage->save();
276 - $changedStage = $stage;
277 - }
278 - }
279 - }
280 -
281 - do_action('fluent_boards/board_stage_dragged', $changedStage);
282 - }
283 -
284 - public function rePositionStages($boardId, $incomingList)
288 + public function repositionStages($boardId, $incomingList)
285 289 {
286 290 $oldList = Stage::where('board_id', $boardId)->where('type', 'stage')->whereNull('archived_at')->orderBy('position')->pluck('id');
287 291
288 292 foreach ($incomingList as $key => $stage_id) {
@@ -310,26 +314,42 @@
310 314 }
311 315 return $processedStages;
312 316 }
313 317
318 + /**
319 + * Archive a stage and persist the user who archived it for future archive-list metadata.
320 + */
314 321 public function archiveStage($boardId, $stage)
315 322 {
323 + $settings = $stage->settings ?: [];
324 + $settings['archived_by_id'] = absint(get_current_user_id()) ?: null;
325 +
316 326 $stage->archived_at = current_time('mysql');
317 327 $stage->position = 0;
328 + $stage->settings = $settings;
318 329 $stage->save();
319 330
320 - do_action('fluent_boards/stage_archived', $boardId, $stage);
331 + do_action('fluent_boards/stage_archived', $boardId, $stage); // Old hook
332 + do_action('fluent_boards/stage_archived_with_tasks', $boardId, $stage); // New hook
321 333 return $stage;
322 334 }
323 335
336 + /**
337 + * Restore an archived stage and clear stale archived-by metadata.
338 + */
324 339 public function restoreStage($boardId, $stage)
325 340 {
326 341 $stageService = new StageService();
327 342 $lastStagePosition = $stageService->getLastPositionOfStagesOfBoard($stage->board_id);
343 + $settings = $stage->settings ?: [];
344 + $settings['archived_by_id'] = null;
345 +
328 346 $stage->archived_at = null;
329 347 $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
348 + $stage->settings = $settings;
330 349 $stage->save();
331 - do_action('fluent_boards/board_stage_restored', $boardId, $stage->title);
350 + do_action('fluent_boards/board_stage_restored', $boardId, $stage->title); // Old hook
351 + do_action('fluent_boards/stage_restored_with_tasks', $boardId, $stage); // New hook
332 352 return $stage;
333 353 }
334 354
335 355 public function getActivities($id, $data)
@@ -335,11 +355,15 @@
335 355 public function getActivities($id, $data)
336 356 {
337 357 $per_page = isset($data['per_page']) ? $data['per_page'] : 40;
338 358 $page = isset($data['page']) ? $data['page'] : 1;
339 - return Activity::where('object_id', $id)->where('object_type', Constant::ACTIVITY_BOARD)->with(['user'])
359 + $activities = Activity::where('object_id', $id)->where('object_type', Constant::ACTIVITY_BOARD)->with(['user'])
340 360 ->orderBy('id', 'DESC')
341 361 ->paginate($per_page, ['*'], 'page', $page);
362 +
363 + Helper::translateActivities($activities);
364 +
365 + return $activities;
342 366 }
343 367
344 368 public function isAlreadyMember($boardId, $memberId)
345 369 {
@@ -349,13 +373,32 @@
349 373
350 374 return $isAlreadyMember ?? false;
351 375 }
352 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 + */
353 383 public function addMembersInBoard($boardId, $memberId, $isViewerOnly = null)
354 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 +
355 393 $board = Board::find($boardId);
394 + $boardMember = User::find($memberId);
356 395
357 - if (!$board) {
396 + if (!$board || !$boardMember) {
397 + return null;
398 + }
399 + $isAlreadyMember = $this->isAlreadyMember($boardId, $memberId);
400 + if($isAlreadyMember) {
358 401 return false;
359 402 }
360 403 $settings = Constant::BOARD_USER_SETTINGS;
361 404
@@ -362,8 +405,10 @@
362 405 if($isViewerOnly === 'yes') {
363 406 $settings = Constant::BOARD_USER_VIEWER_ONLY_SETTINGS;
364 407 }
365 408
409 +
410 +
366 411 $board->users()->attach(
367 412 $memberId,
368 413 [
369 414 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
@@ -370,13 +415,12 @@
370 415 'settings' => maybe_serialize($settings),
371 416 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
372 417 ]
373 418 );
374 - $boardMember = User::find($memberId);
375 419 if(!$isViewerOnly) {
420 + do_action('fluent_boards/board_member_added', $boardId, $boardMember);
421 + } else {
376 422 do_action('fluent_boards/board_viewer_added', $boardId, $boardMember);
377 - } else {
378 - do_action('fluent_boards/board_member_added', $boardId, $boardMember);
379 423 }
380 424 return $boardMember;
381 425 }
382 426
@@ -414,8 +458,75 @@
414 458 $user['is_board_admin'] = false;
415 459 return $user;
416 460 }
417 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 +
418 529 public function getUsersOfBoards()
419 530 {
420 531 $userBoards = Relation::whereNotNull('board_id')
421 532 ->where('user_id', get_current_user_id())
@@ -424,35 +535,68 @@
424 535 return $userBoards;
425 536 }
426 537
427 538 /**
428 - * change board background
429 - * @param mixed $backgroundData
430 - * @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
431 548 */
432 -
433 549 public function setBoardBackground($backgroundData, $board_id)
434 550 {
435 - $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 +
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 +
436 568 $background = $board->background;
437 -
438 - // if board background has color
439 - if (isset($backgroundData['color'])) {
440 - $background['color'] = $backgroundData['color'];
441 - $background['image_url'] = null;
442 - $background['is_image'] = false;
569 + if (!is_array($background)) {
570 + $background = [];
443 571 }
444 572
445 - // if board background has image
573 + // Resolve image metadata from the board-owned attachment, never from the client URL.
446 574 if (isset($backgroundData['image_url'])) {
447 - $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);
448 587 $background['is_image'] = true;
449 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;
450 594 }
451 - $background['id'] = $backgroundData['id'];
452 595
453 596 $board->background = $background;
454 597 $board->save();
598 + do_action('fluent_boards/board_background_updated', $boardId, $oldBackground);
455 599
456 600 return $board->background;
457 601 }
458 602
@@ -462,25 +606,85 @@
462 606 * @param mixed $board_id
463 607 * @param mixed $stage_slug
464 608 * @return array of available positions of the stage with one increased value because if the stage has 10 tasks than it will have 10 position and +1 as last position of the stage
465 609 */
466 - public function getStageTaskAvailablePositions($board_id, $stage_id)
610 + public function getStageTaskAvailablePositions($board_id, $stage_id, $task_id = null)
467 611 {
468 - $availablePositions = Task::query()
612 + $task_id = absint($task_id);
613 + $task = $task_id ? Task::find($task_id) : null;
614 + $isCurrentStage = $task
615 + && (int) $task->board_id === (int) $board_id
616 + && (int) $task->stage_id === (int) $stage_id;
617 +
618 + $stageTasks = Task::query()
469 619 ->where('board_id', $board_id)
470 620 ->where('parent_id', null)
471 621 ->where('stage_id', $stage_id)
472 622 ->whereNull('archived_at')
473 623 ->orderBy('position', 'asc')
474 - ->get()
475 - ->pluck('position')->toArray();
624 + ->get(['id', 'position']);
476 625
477 - $totalPosition = count($availablePositions);
478 - $availablePositions[$totalPosition] = $totalPosition + 1;
626 + if ($isCurrentStage) {
627 + $stageTasks = $stageTasks->filter(function ($stageTask) use ($task_id) {
628 + return (int) $stageTask->id !== $task_id;
629 + })->values();
630 + }
479 631
480 - return $availablePositions;
632 + $availablePositions = [];
633 + $moveTargets = [];
634 + $currentMoveTargetKey = null;
635 + $totalSlots = $stageTasks->count() + 1;
636 + $currentSlot = $this->getCurrentStageSlotIndex($task, $stageTasks, $isCurrentStage);
637 +
638 + for ($slotIndex = 0; $slotIndex < $totalSlots; $slotIndex++) {
639 + $slotNumber = $slotIndex + 1;
640 + // Each slot represents a drop target between two ordered tasks, so the
641 + // modal can send exact neighbour ids instead of a fragile display index.
642 + $prevTask = $slotIndex > 0 ? $stageTasks->get($slotIndex - 1) : null;
643 + $nextTask = $slotIndex < $stageTasks->count() ? $stageTasks->get($slotIndex) : null;
644 + $slotKey = 'slot_' . $slotNumber;
645 +
646 + $availablePositions[] = $slotNumber;
647 + $moveTargets[] = [
648 + 'key' => $slotKey,
649 + 'label' => $slotNumber,
650 + 'prevTaskId' => $prevTask ? (int) $prevTask->id : null,
651 + 'nextTaskId' => $nextTask ? (int) $nextTask->id : null,
652 + 'isCurrent' => $isCurrentStage && $currentSlot === $slotNumber,
653 + ];
654 +
655 + if ($isCurrentStage && $currentSlot === $slotNumber) {
656 + $currentMoveTargetKey = $slotKey;
657 + }
658 + }
659 +
660 + return [
661 + 'availablePositions' => $availablePositions,
662 + 'moveTargets' => $moveTargets,
663 + 'currentMoveTargetKey' => $currentMoveTargetKey,
664 + 'defaultMoveTargetKey' => 'slot_' . $totalSlots,
665 + ];
481 666 }
482 667
668 + private function getCurrentStageSlotIndex($task, $stageTasks, $isCurrentStage)
669 + {
670 + if (!$isCurrentStage || !$task) {
671 + return null;
672 + }
673 +
674 + $slotNumber = 1;
675 + foreach ($stageTasks as $stageTask) {
676 + if ((float) $task->position > (float) $stageTask->position) {
677 + $slotNumber++;
678 + continue;
679 + }
680 +
681 + break;
682 + }
683 +
684 + return $slotNumber;
685 + }
686 +
483 687 public function getAssigneesByBoard($board_id, $search = '')
484 688 {
485 689 $assignees = [];
486 690 $boardUsers = [];
@@ -540,10 +744,10 @@
540 744 }
541 745
542 746 // Check if the board is already in the list
543 747 if (!in_array($boardId, $recentBoardIds)) {
544 - // If there are already 3 boards, remove the last one
545 - if (count($recentBoardIds) >= 3) {
748 + // Keep the 4 most recently opened boards for the dashboard view.
749 + if (count($recentBoardIds) >= 4) {
546 750 array_pop($recentBoardIds);
547 751 }
548 752 } else {
549 753 // Remove the existing board id to move it to the front
@@ -579,8 +783,12 @@
579 783 if (!$recentBoardIds) {
580 784 return [];
581 785 }
582 786
787 + if (!is_array($recentBoardIds)) {
788 + $recentBoardIds = [];
789 + }
790 +
583 791 $currentUser = User::find($userId);
584 792
585 793 if (!PermissionManager::isAdmin($userId)){
586 794 $recentBoardIds = array_intersect($recentBoardIds, $currentUser->whichBoards->pluck('id')->toArray());
@@ -585,8 +793,10 @@
585 793 if (!PermissionManager::isAdmin($userId)){
586 794 $recentBoardIds = array_intersect($recentBoardIds, $currentUser->whichBoards->pluck('id')->toArray());
587 795 }
588 796
797 + $recentBoardIds = array_values(array_slice($recentBoardIds, 0, 4));
798 +
589 799 // This is for checking if that board is exists
590 800 // TODO: we will remove this code in future version
591 801 if (!$this->recentBoardBackwardCompatibilityCheck()) {
592 802 foreach ($recentBoardIds as $index => $boardId) {
@@ -599,9 +809,15 @@
599 809
600 810 $this->updateRecentBoardCheckMeta();
601 811 }
602 812
603 - 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();
604 820 }
605 821
606 822 public function getRecentBoardCheckMeta($userId = null){
607 823 if (!$userId) {
@@ -699,10 +915,15 @@
699 915
700 916 $contactOfBoard->delete();
701 917 }
702 918
703 - public function sendInvitationToBoard($boardId, $email)
919 + public function sendInvitationToBoard($boardId, $email, $role = 'member')
704 920 {
921 + $role = sanitize_text_field($role);
922 + if (!in_array($role, ['manager', 'member', 'viewer'], true)) {
923 + $role = 'member';
924 + }
925 +
705 926 $user = User::query()->where('user_email', $email)->first();
706 927
707 928 if ($user) {
708 929 return $user;
@@ -709,9 +930,9 @@
709 930 }
710 931
711 932 $current_user_id = get_current_user_id();
712 933
713 - do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id);
934 + do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id, $role);
714 935
715 936 return;
716 937
717 938 }
@@ -723,57 +944,124 @@
723 944 ->where('key', Constant::BOARD_INVITATION)
724 945 ->get();
725 946 }
726 947
727 - 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)
728 955 {
729 - 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();
730 975 }
731 976
732 - public function hasDataChanged($boardId)
977 + public function hasDataChanged($boardId, $includeArchived = false, $since = null)
733 978 {
734 979 $stages = [];
735 980 $labels = [];
736 981 $tasks = [];
737 - $labels = [];
738 - $taskDeleted = false;
739 - $stageDeleted = false;
740 - $labelDeleted = false;
741 - $oneMinuteAgoTimestamp = current_time('timestamp') - 60; // Get the current timestamp and subtract 60 seconds
742 - $oneMinuteAgo = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp); // Format the timestamp into the desired format in GMT
982 + $syncStartedAt = current_time('mysql');
983 + $isCursorRequest = !empty($since);
984 + $forceFullSync = false;
743 985
986 + if ($isCursorRequest) {
987 + $lastUpdated = $this->normalizeSyncCursor($since, $syncStartedAt);
988 + $forceFullSync = !$lastUpdated;
989 + } else {
990 + $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
991 + $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp);
992 + }
993 +
744 994 $board = Board::find($boardId);
745 995 if (!$board) {
746 - throw new \Exception("Board doesn't exists");
996 + throw new \Exception(esc_html__("Board doesn't exists", 'fluent-boards'));
747 997 }
748 - // if stage in this board has been deleted
749 - $stageDeleted = Activity::where('object_id', $boardId)->where('updated_at', '>=', $oneMinuteAgo)->where('action', 'deleted')->where('column', 'stage')->exists();
998 + $boardUpdatedAt = $this->formatSyncTimestamp($board->updated_at);
999 + $boardChanged = !$isCursorRequest || $forceFullSync || $boardUpdatedAt >= $lastUpdated;
750 1000
751 - if ($stageDeleted) {
752 - $stages = $board->stages;
1001 + // Reset the local list if a change can remove an item from the user's current view.
1002 + $stageActivityQuery = Activity::where('object_id', $boardId)
1003 + ->where('object_type', Constant::ACTIVITY_BOARD)
1004 + ->where('updated_at', '>=', $lastUpdated)
1005 + ->where('column', 'stage');
1006 +
1007 + $stageResetRequired = $forceFullSync || (clone $stageActivityQuery)
1008 + ->whereIn('action', ['deleted', 'archived', 'restored'])
1009 + ->exists();
1010 +
1011 + if ($stageResetRequired) {
1012 + $stagesQuery = Stage::where('board_id', $boardId)->orderBy('position', 'asc');
1013 + if (!$includeArchived) {
1014 + $stagesQuery->whereNull('archived_at');
1015 + }
1016 + $stages = $stagesQuery->get();
753 1017 } else {
754 - $stages = (new StageService())->getLastOneMinuteUpdatedStages($boardId);
1018 + $stages = (new StageService())->getLastOneMinuteUpdatedStages($boardId, $lastUpdated, $includeArchived);
755 1019 }
756 1020
757 - $labelDeleted = Activity::where('object_id', $boardId)->where('updated_at', '>=', $oneMinuteAgo)->where('action', 'deleted')->where('column', 'label')->exists();
758 - if ($labelDeleted) {
759 - $labels = $board->labels;
1021 + $labelResetRequired = $forceFullSync || Activity::where('object_id', $boardId)
1022 + ->where('object_type', Constant::ACTIVITY_BOARD)
1023 + ->where('updated_at', '>=', $lastUpdated)
1024 + ->where('action', 'deleted')
1025 + ->where('column', 'label')
1026 + ->exists();
1027 + if ($labelResetRequired) {
1028 + $labelsQuery = Label::where('board_id', $boardId)->orderBy('position', 'asc');
1029 + if (!$includeArchived) {
1030 + $labelsQuery->whereNull('archived_at');
1031 + }
1032 + $labels = $labelsQuery->get();
760 1033 } else {
761 - $labels = (new LabelService())->getLastOneMinuteUpdatedLabels($boardId);
1034 + $labels = (new LabelService())->getLastOneMinuteUpdatedLabels($boardId, $lastUpdated, $includeArchived);
762 1035 }
763 1036
764 - // if task in this board has been deleted
765 - $taskDeletedOrMovedFormBoard = Activity::where('object_id', $boardId)->where('updated_at', '>=', $oneMinuteAgo)->where('action', 'deleted')->orWhere('action', 'moved')->where('column', 'task')->exists();
1037 + $stageArchiveRestored = !$forceFullSync && (clone $stageActivityQuery)
1038 + ->whereIn('action', ['archived', 'restored'])
1039 + ->exists();
766 1040
767 - if ($taskDeletedOrMovedFormBoard) {
1041 + $taskResetRequired = $forceFullSync || $stageArchiveRestored || Activity::where('object_id', $boardId)
1042 + ->where('object_type', Constant::ACTIVITY_BOARD)
1043 + ->where('updated_at', '>=', $lastUpdated)
1044 + ->where(function($query) {
1045 + $query->where('action', 'deleted')
1046 + ->orWhere('action', 'moved')
1047 + ->orWhere('action', 'archived')
1048 + ->orWhere('action', 'restored');
1049 + })
1050 + ->where('column', 'task')
1051 + ->exists();
1052 + if ($taskResetRequired) {
768 1053 $tasksQuery = Task::query()
769 1054 ->where([
770 1055 'board_id' => $boardId,
771 1056 'parent_id' => null,
772 - 'archived_at' => null
773 1057 ])
774 1058 ->with(['assignees', 'labels', 'watchers']);
775 1059
1060 + if (!$includeArchived) {
1061 + $tasksQuery->whereNull('archived_at');
1062 + }
1063 +
776 1064 if (!!defined('FLUENT_BOARDS_PRO_VERSION')) {
777 1065 $tasksQuery->with('customFields');
778 1066 }
779 1067
@@ -778,14 +1066,18 @@
778 1066 }
779 1067
780 1068 $tasks = $tasksQuery->orderBy('due_at', 'ASC')->get();
781 1069 } else {
782 - $tasks = (new TaskService())->getLastOneMinuteUpdatedTasks($boardId);
1070 + $tasks = (new TaskService())->getLastOneMinuteUpdatedTasks($boardId, $lastUpdated, $includeArchived);
783 1071 }
784 1072
785 1073 foreach ($tasks as $task) {
786 1074 $task->isOverdue = $task->isOverdue();
787 1075 $task->isUpcoming = $task->upcoming();
1076 + $task->is_watching = $task->isWatching();
1077 + $task->contact = Helper::crm_contact($task->crm_contact_id);
1078 + $task->assignees = Helper::sanitizeUserCollections($task->assignees);
1079 + $task->watchers = Helper::sanitizeUserCollections($task->watchers);
788 1080 }
789 1081
790 1082 $board->background = \maybe_unserialize($board->background);
791 1083 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
@@ -791,201 +1083,128 @@
791 1083 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
792 1084 $board->custom_fields = $board->customFields;
793 1085 }
794 1086
1087 + $boardPayload = $boardChanged ? $board : (object) [];
1088 + $hasChanges = $boardChanged
1089 + || $stageResetRequired
1090 + || $labelResetRequired
1091 + || $taskResetRequired
1092 + || count($stages)
1093 + || count($labels)
1094 + || count($tasks);
1095 +
795 1096 return [
796 - 'board' => $board,
797 - 'stages' => $stages,
798 - 'labels' => $labels,
799 - 'tasks' => $tasks,
800 - 'taskDeleted' => $taskDeletedOrMovedFormBoard,
801 - 'stageDeleted' => $stageDeleted,
1097 + 'board' => $boardPayload,
1098 + 'stages' => $stages,
1099 + 'labels' => $labels,
1100 + 'tasks' => $tasks,
1101 + 'taskDeleted' => $taskResetRequired,
1102 + 'stageDeleted' => $stageResetRequired,
1103 + 'labelDeleted' => $labelResetRequired,
1104 + 'taskResetRequired' => $taskResetRequired,
1105 + 'stageResetRequired' => $stageResetRequired,
1106 + 'labelResetRequired' => $labelResetRequired,
1107 + 'has_changes' => (bool) $hasChanges,
1108 + 'synced_at' => $syncStartedAt,
1109 + 'sync_reset' => $forceFullSync,
802 1110 ];
803 1111 }
804 1112
805 - public function getAssociatedBoards($associatedId)
1113 + private function normalizeSyncCursor($since, $syncStartedAt)
806 1114 {
1115 + if (!is_string($since)) {
1116 + return null;
1117 + }
807 1118
808 - $boardIds = Meta::query()->where('value', $associatedId)
809 - ->where('object_type', Constant::OBJECT_TYPE_BOARD)
810 - ->where('key', Constant::BOARD_ASSOCIATED_CRM_CONTACT)
811 - ->pluck('object_id');
1119 + $since = trim($since);
1120 + if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $since)) {
1121 + return null;
1122 + }
812 1123
813 - return Board::query()->whereIn('id', $boardIds)->with('stages', 'users')->get();
814 - }
1124 + if ($since > $syncStartedAt) {
1125 + return null;
1126 + }
815 1127
816 - private function deleteBoardMeta($boardId)
817 - {
818 - Meta::where('object_id', $boardId)
819 - ->where('object_type', Constant::OBJECT_TYPE_BOARD)
820 - ->delete();
1128 + if (strtotime($since) < strtotime('-24 hours', strtotime($syncStartedAt))) {
1129 + return null;
1130 + }
1131 +
1132 + return $since;
821 1133 }
822 1134
823 - public function copyBoard($boardData)
1135 + private function formatSyncTimestamp($timestamp)
824 1136 {
825 - $boardData = apply_filters('fluent_boards/before_create_board', $boardData);
1137 + if ($timestamp instanceof \DateTimeInterface) {
1138 + return $timestamp->format('Y-m-d H:i:s');
1139 + }
826 1140
827 - $board = Board::create($boardData);
828 -
829 - $this->setCurrentUserPreferencesOnBoardCreate($board);
830 -
831 - return $board;
1141 + return (string) $timestamp;
832 1142 }
833 1143
834 - public function getBoardReports($board_id)
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 + */
1151 + public function getAssociatedBoards($associatedId, $userId = null)
835 1152 {
836 - $board = Board::findOrFail($board_id);
837 - $taskQuery = Task::where('board_id', $board_id)
838 - ->whereNull('parent_id')
839 - ->whereNull('archived_at');
1153 + $associatedId = absint($associatedId);
1154 + $userId = $userId ?: get_current_user_id();
840 1155
841 - if($board->type == 'roadmap') {
842 - $pendingStage = $this->getNewIdeaStage($board->id);
843 - return $this->getIdeaReports($taskQuery, $pendingStage);
844 - } else {
845 - return $this->getTaskReports($taskQuery);
1156 + if (!$associatedId || !$userId) {
1157 + return [];
846 1158 }
847 - }
848 1159
849 - private function getNewIdeaStage($boardId)
850 - {
851 - return Stage::where('board_id', $boardId)
852 - ->where('type', 'stage')
853 - ->where('archived_at', null)
854 - ->orderBy('position', 'ASC')
855 - ->first();
856 - }
1160 + $boardIds = Meta::query()->where('value', $associatedId)
1161 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1162 + ->whereIn('key', [
1163 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1164 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1165 + ])
1166 + ->pluck('object_id');
857 1167
858 - public function getAllBoardReports(){
859 - $userId = get_current_user_id();
860 -
861 - $taskQuery = Task::whereNull('parent_id')
1168 + $boards = Board::query()
1169 + ->whereIn('id', array_values(array_unique(array_map('intval', $boardIds->toArray()))))
862 1170 ->whereNull('archived_at')
863 - ->whereHas('board', function ($query) {
864 - $query->where('type', 'to-do');
865 - });
1171 + ->byAccessUser($userId)
1172 + ->withCount('completedTasks')
1173 + ->with(['stages', 'users'])
1174 + ->orderBy('created_at', 'DESC')
1175 + ->get();
866 1176
867 - if (!PermissionManager::isAdmin($userId))
868 - {
869 - $currentUser = User::find($userId);
870 - $relatedBoardIds = $currentUser->whichBoards->where('type', 'to-do')->pluck('id');
871 - $taskQuery->whereIn('board_id', $relatedBoardIds);
1177 + foreach ($boards as $board) {
1178 + $board->users = Helper::sanitizeUserCollections($board->users);
872 1179 }
873 1180
874 - return $this->getTaskReports($taskQuery);
1181 + return $boards;
875 1182 }
876 1183
877 - private function getTaskReports($taskQuery)
1184 + private function deleteBoardMeta($boardId)
878 1185 {
879 - $totalTasksQuery = clone $taskQuery;
880 - $completedTaskQuery = clone $taskQuery;
881 - $openTaskQuery = clone $taskQuery;
882 - $overDueTaskQuery = clone $taskQuery;
883 -
884 - $completedTaskCount = $completedTaskQuery->where('status', 'closed')->count();
885 - $openTaskCount = $openTaskQuery->where('status', 'open')->count();
886 - $overDueTasks = $overDueTaskQuery->overdue(true)->count();
887 - $totalTasks = $totalTasksQuery->count();
888 -
889 - $taskQuery->where('status', 'open');
890 -
891 - $highQuery = clone $taskQuery;
892 - $mediumQuery = clone $taskQuery;
893 - $lowQuery = clone $taskQuery;
894 -
895 - $high = $highQuery->where('priority', 'high')->count();
896 - $low = $mediumQuery->where('priority', 'low')->count();
897 - $medium = $lowQuery->where('priority', 'medium')->count();
898 -
899 - $reportData = [
900 - 'completion' => [
901 - 'completed' => $completedTaskCount,
902 - 'incomplete' => $openTaskCount,
903 - 'overdue' => $overDueTasks,
904 - 'total' => $totalTasks
905 - ],
906 - 'priority' => [
907 - 'high' => $high,
908 - 'medium' => $medium,
909 - 'low' => $low
910 - ]
911 -
912 - ];
913 - return $reportData;
1186 + Meta::where('object_id', $boardId)
1187 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1188 + ->delete();
914 1189 }
915 1190
916 - private function getIdeaReports($taskQuery, $pendingStage)
1191 + public function copyBoard($boardData)
917 1192 {
918 - $pendingIdeaQuery = clone $taskQuery;
919 - $completedIdeaQuery = clone $taskQuery;
920 - $openIdeaQueryPage = clone $taskQuery;
921 - $openIdeaQueryWeb = clone $taskQuery;
1193 + $sourceBoard = Board::findOrFail($boardData['source_board_id']);
1194 + $boardData['background'] = $sourceBoard->background;
1195 + if (isset($boardData['description'])) {
1196 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
1197 + }
1198 + $boardData = apply_filters('fluent_boards/before_create_board', $boardData);
922 1199
923 - $pendingIdeaCount = $pendingIdeaQuery->where('status', 'open')->where('stage_id', $pendingStage->id)->count();
924 - $completedIdeaCount = $completedIdeaQuery->where('status', 'closed')->count();
925 - $openIdeaCountPage = $openIdeaQueryPage->where('status', 'open')->where('source', 'page')->count();
926 - $openIdeaCountWeb = $openIdeaQueryWeb->where('status', 'open')->where('source', 'web')->count();
927 - $totalIdeas = $openIdeaCountPage + $openIdeaCountWeb;
1200 + $board = Board::create($boardData);
928 1201
929 - $taskQuery->where('status', 'open');
1202 + $this->setCurrentUserPreferencesOnBoardCreate($board);
930 1203
931 - $highQuery = clone $taskQuery;
932 - $mediumQuery = clone $taskQuery;
933 - $lowQuery = clone $taskQuery;
934 -
935 - $high = $highQuery->where('priority', 'high')->count();
936 - $low = $mediumQuery->where('priority', 'low')->count();
937 - $medium = $lowQuery->where('priority', 'medium')->count();
938 -
939 - $reportData = [
940 - 'completion' => [
941 - 'pending' => $pendingIdeaCount,
942 - 'completed' => $completedIdeaCount,
943 - 'ideaFromPage' => $openIdeaCountPage,
944 - 'total' => $totalIdeas
945 - ],
946 - 'priority' => [
947 - 'high' => $high,
948 - 'medium' => $medium,
949 - 'low' => $low
950 - ]
951 - ];
952 - return $reportData;
1204 + return $board;
953 1205 }
954 1206
955 - public function getStageWiseBoardReports($board_id)
956 - {
957 - $stages = Stage::where('board_id', $board_id)
958 - ->where('type', 'stage')
959 - ->whereNull('archived_at')
960 - ->get();
961 -
962 - foreach ($stages as $stage) {
963 - $completedTaskCount = Task::where('stage_id', $stage->id)
964 - ->where('status', 'closed')
965 - ->count();
966 -
967 - $openTaskCount = Task::where('stage_id', $stage->id)
968 - ->whereNull('due_at')
969 - ->where('status', 'open')
970 - ->count();
971 -
972 - $overDue = Task::where('stage_id', $stage->id)
973 - ->whereNotNull('due_at')
974 - ->where('status', 'open')
975 - ->overdue(true)
976 - ->count();
977 -
978 - $stage->report = [
979 - 'completed' => $completedTaskCount,
980 - 'incomplete' => $openTaskCount,
981 - 'overdue' => $overDue
982 - ];
983 - }
984 -
985 - return $stages;
986 - }
987 -
988 1207 public function archiveBoard($boardId)
989 1208 {
990 1209 $board = Board::findOrFail($boardId);
991 1210 $board->archived_at = current_time('mysql');
@@ -1042,5 +1261,209 @@
1042 1261 $user['is_board_admin'] = false;
1043 1262 return $user;
1044 1263 }
1045 1264
1265 + private function getUserWisePinnedBoards()
1266 + {
1267 + $userId = get_current_user_id();
1268 +
1269 + $pinnedBoardMeta = Meta::query()->where('object_id', $userId)
1270 + ->where('object_type', Constant::OBJECT_TYPE_USER)
1271 + ->where('key', Constant::USER_PINNED_BOARDS)
1272 + ->first();
1273 +
1274 + return $pinnedBoardMeta;
1275 + }
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 +
1323 + public function getPinnedBoards()
1324 + {
1325 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1326 +
1327 + if (!$pinnedBoardMeta) {
1328 + return [];
1329 + } else {
1330 + $ids = $pinnedBoardMeta->value;
1331 +
1332 + // Convert to array of integers
1333 + $intIds = array_map('intval', $ids);
1334 +
1335 + return Board::whereIn('id', $intIds)
1336 + ->whereNull('archived_at')
1337 + ->byAccessUser(get_current_user_id())
1338 + ->get();
1339 + }
1340 + }
1341 +
1342 + public function pinBoard($boardId)
1343 + {
1344 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1345 +
1346 + if ($pinnedBoardMeta) {
1347 + $currentPinnedBoards = $pinnedBoardMeta->value;
1348 + if (!in_array($boardId, $currentPinnedBoards)) {
1349 + $currentPinnedBoards[] = $boardId;
1350 + $pinnedBoardMeta->value = $currentPinnedBoards;
1351 + $pinnedBoardMeta->save();
1352 + }
1353 + } else {
1354 + // Create an empty array
1355 + $boardIds = [];
1356 + $boardIds[] = $boardId;
1357 +
1358 + $meta = new Meta();
1359 + $meta->object_id = get_current_user_id();
1360 + $meta->object_type = Constant::OBJECT_TYPE_USER;
1361 + $meta->key = Constant::USER_PINNED_BOARDS;
1362 + $meta->value = $boardIds;
1363 + $meta->save();
1364 + }
1365 + }
1366 +
1367 + /**
1368 + * @param $boardId
1369 + * @return bool
1370 + */
1371 + public function unpinBoard($boardId)
1372 + {
1373 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1374 +
1375 + if (!$pinnedBoardMeta) {
1376 + return false;
1377 + }
1378 +
1379 + $currentPinnedBoards = $pinnedBoardMeta->value;
1380 + if (in_array($boardId, $currentPinnedBoards)) {
1381 + $index = array_search($boardId, $currentPinnedBoards);
1382 + array_splice($currentPinnedBoards, $index, 1);
1383 + $pinnedBoardMeta->value = $currentPinnedBoards;
1384 + $pinnedBoardMeta->save();
1385 + return true;
1386 + }
1387 +
1388 + return false;
1389 + }
1390 +
1391 + /**
1392 + * @param $boardId
1393 + * @return bool
1394 + * If board id is in user's current pinned boards list
1395 + */
1396 + public function isPinned($boardId)
1397 + {
1398 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1399 +
1400 + if (!$pinnedBoardMeta) {
1401 + return false;
1402 + }
1403 +
1404 + $currentPinnedBoards = $pinnedBoardMeta->value;
1405 + if (in_array($boardId, $currentPinnedBoards)) {
1406 + return true;
1407 + }
1408 +
1409 + return false;
1410 + }
1411 +
1412 + public function getBoardFolder($boardId)
1413 + {
1414 + $relation = Relation::where('object_type', Constant::OBJECT_TYPE_FOLDER_BOARD)
1415 + ->where('foreign_id', $boardId)
1416 + ->first();
1417 +
1418 + if (!$relation) {
1419 + return null;
1420 + }
1421 +
1422 + return Folder::find($relation->object_id);
1423 + }
1424 +
1425 + public function deleteWebhookData($boardId)
1426 + {
1427 + $outgoingRelations = Relation::where('object_type', 'outgoing_webhook_board')
1428 + ->where('foreign_id', $boardId)
1429 + ->get();
1430 +
1431 + foreach ($outgoingRelations as $relation) {
1432 + $webhookMetaId = (int) $relation->object_id;
1433 +
1434 + $linkedCount = Relation::where('object_type', 'outgoing_webhook_board')
1435 + ->where('object_id', $webhookMetaId)
1436 + ->count();
1437 +
1438 + if ($linkedCount === 1) {
1439 + Meta::where('id', $webhookMetaId)
1440 + ->where('object_type', 'outgoing_webhook')
1441 + ->delete();
1442 + } else if ($linkedCount > 1) {
1443 + $meta = Meta::find($webhookMetaId);
1444 + if ($meta && $meta->object_type === 'outgoing_webhook') {
1445 + $value = $meta->value;
1446 +
1447 + if (isset($value['board_id'])) {
1448 + $boards = $value['board_id'];
1449 +
1450 + if (is_array($boards)) {
1451 + $boards = array_values(array_filter($boards, function ($id) use ($boardId) {
1452 + return intval($id) !== intval($boardId);
1453 + }));
1454 + $value['board_id'] = $boards;
1455 + } else {
1456 + if ($boards !== null && intval($boards) === intval($boardId)) {
1457 + $value['board_id'] = [];
1458 + }
1459 + }
1460 +
1461 + $meta->value = $value;
1462 + $meta->save();
1463 + }
1464 + }
1465 + }
1466 + $relation->delete();
1467 + }
1468 + }
1046 1469 }