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 +699 -274 1.352.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,9 +43,16 @@
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();
51 +
52 + // delete all activities
53 + Activity::whereIn('object_id', $allTaskIdsInBoard)->where('object_type', Constant::ACTIVITY_TASK)->delete();
54 + $board->activities()->delete();
39 55
40 56 //removing all Board Settings
41 57 $board->boardUserEmailNotificationSettings()->detach();
42 58 $board->boardUserNotificationSettings()->detach();
@@ -48,15 +64,25 @@
48 64
49 65 //removing add board labels
50 66 $board->labels()->delete();
51 67
52 - //removing add board comment
53 - $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 + }
54 78
79 +
55 80 foreach ($board->notifications as $notification) {
56 81 $notification->users()->detach();
57 82 }
58 83 $board->notifications()->delete();
84 + $board->removeBoardFromFolder();
59 85
60 86 //delete board related meta
61 87 $this->deleteBoardMeta($boardId);
62 88
@@ -62,13 +88,13 @@
62 88
63 89 //delete from recently viewed
64 90 $this->deleteFromRecentlyViewed($boardId);
65 91
92 + //delete webhook data
93 + $this->deleteWebhookData($boardId);
94 +
66 95 $board->delete();
67 - if(!defined('FLUENT_BOARDS_PRO')){
68 - FileSystem::deleteDir('board_'.$boardId);
69 - }
70 -// do_action('fluent_boards/board_deleted', $board);
96 + FileSystem::deleteDir('board_'.$boardId);
71 97 }
72 98
73 99 public function fetchBoardMeta($boardId)
74 100 {
@@ -124,9 +150,9 @@
124 150 {
125 151 $boardData = [
126 152 'title' => $boardData['title'],
127 153 'type' => $boardData['type'] ? $boardData['type'] : 'to-do',
128 - 'description' => $boardData['description'],
154 + 'description' => DescriptionMarkdownConverter::normalize($boardData['description'] ?? ''),
129 155 'currency' => isset($boardData['currency']) ? $boardData['currency'] : 'USD',
130 156 'background' => isset($boardData['background']) ? $boardData['background'] : '',
131 157 'created_by' => isset($boardData['created_by']) ? $boardData['created_by'] : get_current_user_id()
132 158 ];
@@ -139,12 +165,23 @@
139 165
140 166 return $board;
141 167 }
142 168
143 - 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)
144 176 {
177 + $creatorId = absint($board->created_by);
178 + if (!$creatorId) {
179 + return;
180 + }
181 +
145 182 $board->users()->attach(
146 - $board->created_by,
183 + $creatorId,
147 184 [
148 185 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
149 186 'settings' => maybe_serialize([
150 187 Constant::IS_BOARD_ADMIN => true
@@ -192,9 +229,18 @@
192 229 ->first();
193 230 if ($recentlyOpened) {
194 231 $recentBoardIds = $recentlyOpened->value;
195 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 +
196 238 $index = array_search($boardId, $recentBoardIds);
239 + if ($index === false) {
240 + return;
241 + }
242 +
197 243 array_splice($recentBoardIds, $index, 1);
198 244
199 245 $recentlyOpened->value = $recentBoardIds;
200 246 $recentlyOpened->save();
@@ -206,12 +252,12 @@
206 252 {
207 253 if ($data['title']) {
208 254 $data['title'] = $data['title'];
209 255 } else {
210 - throw new \Exception('Title cannot be empty');
256 + throw new \Exception(esc_html__('Title cannot be empty', 'fluent-boards'));
211 257 }
212 258 if (isset($data['description'])) {
213 - $data['description'] = $data['description'];
259 + $data['description'] = DescriptionMarkdownConverter::normalize($data['description']);
214 260 }
215 261 $board->fill($data);
216 262 $board->save();
217 263 // do_action('fluent_boards/board_updated', $board);
@@ -237,50 +283,10 @@
237 283
238 284 return serialize($this->processStages($stages));
239 285 }
240 286
241 - public function changePositionOfStage($boardId, $changeData)
242 - {
243 - $allStages = Stage::where('board_id', $boardId)
244 - ->where('is_archived', 0)
245 - ->where('type', 'stage')
246 - ->orderBy('position', 'asc')
247 - ->get();
248 287
249 - $changedStage = null;
250 -
251 - foreach ($allStages as $stage) {
252 - if ($changeData['fromPosition'] < $changeData['toPosition']) {
253 - if ($stage['position'] == $changeData['fromPosition']) {
254 - $stage['position'] = (int)$changeData['toPosition'];
255 - $stage->save();
256 - $changedStage = $stage;
257 - } elseif (
258 - $stage['position'] > $changeData['fromPosition'] &&
259 - $stage['position'] <= $changeData['toPosition']
260 - ) {
261 - $stage['position'] = $stage->position - 1;
262 - $stage->save();
263 - }
264 - } else {
265 - if (
266 - $stage['position'] >= $changeData['toPosition'] &&
267 - $stage['position'] < $changeData['fromPosition']
268 - ) {
269 - $stage['position'] = $stage->position + 1;
270 - $stage->save();
271 - } elseif ($stage['position'] == $changeData['fromPosition']) {
272 - $stage['position'] = (int)$changeData['toPosition'];
273 - $stage->save();
274 - $changedStage = $stage;
275 - }
276 - }
277 - }
278 -
279 - do_action('fluent_boards/board_stage_dragged', $changedStage);
280 - }
281 -
282 - public function rePositionStages($boardId, $incomingList)
288 + public function repositionStages($boardId, $incomingList)
283 289 {
284 290 $oldList = Stage::where('board_id', $boardId)->where('type', 'stage')->whereNull('archived_at')->orderBy('position')->pluck('id');
285 291
286 292 foreach ($incomingList as $key => $stage_id) {
@@ -308,26 +314,42 @@
308 314 }
309 315 return $processedStages;
310 316 }
311 317
318 + /**
319 + * Archive a stage and persist the user who archived it for future archive-list metadata.
320 + */
312 321 public function archiveStage($boardId, $stage)
313 322 {
323 + $settings = $stage->settings ?: [];
324 + $settings['archived_by_id'] = absint(get_current_user_id()) ?: null;
325 +
314 326 $stage->archived_at = current_time('mysql');
315 327 $stage->position = 0;
328 + $stage->settings = $settings;
316 329 $stage->save();
317 330
318 - 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
319 333 return $stage;
320 334 }
321 335
336 + /**
337 + * Restore an archived stage and clear stale archived-by metadata.
338 + */
322 339 public function restoreStage($boardId, $stage)
323 340 {
324 341 $stageService = new StageService();
325 342 $lastStagePosition = $stageService->getLastPositionOfStagesOfBoard($stage->board_id);
343 + $settings = $stage->settings ?: [];
344 + $settings['archived_by_id'] = null;
345 +
326 346 $stage->archived_at = null;
327 347 $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
348 + $stage->settings = $settings;
328 349 $stage->save();
329 - 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
330 352 return $stage;
331 353 }
332 354
333 355 public function getActivities($id, $data)
@@ -333,11 +355,15 @@
333 355 public function getActivities($id, $data)
334 356 {
335 357 $per_page = isset($data['per_page']) ? $data['per_page'] : 40;
336 358 $page = isset($data['page']) ? $data['page'] : 1;
337 - 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'])
338 360 ->orderBy('id', 'DESC')
339 361 ->paginate($per_page, ['*'], 'page', $page);
362 +
363 + Helper::translateActivities($activities);
364 +
365 + return $activities;
340 366 }
341 367
342 368 public function isAlreadyMember($boardId, $memberId)
343 369 {
@@ -347,13 +373,32 @@
347 373
348 374 return $isAlreadyMember ?? false;
349 375 }
350 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 + */
351 383 public function addMembersInBoard($boardId, $memberId, $isViewerOnly = null)
352 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 +
353 393 $board = Board::find($boardId);
394 + $boardMember = User::find($memberId);
354 395
355 - if (!$board) {
396 + if (!$board || !$boardMember) {
397 + return null;
398 + }
399 + $isAlreadyMember = $this->isAlreadyMember($boardId, $memberId);
400 + if($isAlreadyMember) {
356 401 return false;
357 402 }
358 403 $settings = Constant::BOARD_USER_SETTINGS;
359 404
@@ -360,8 +405,10 @@
360 405 if($isViewerOnly === 'yes') {
361 406 $settings = Constant::BOARD_USER_VIEWER_ONLY_SETTINGS;
362 407 }
363 408
409 +
410 +
364 411 $board->users()->attach(
365 412 $memberId,
366 413 [
367 414 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
@@ -368,13 +415,12 @@
368 415 'settings' => maybe_serialize($settings),
369 416 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
370 417 ]
371 418 );
372 - $boardMember = User::find($memberId);
373 419 if(!$isViewerOnly) {
420 + do_action('fluent_boards/board_member_added', $boardId, $boardMember);
421 + } else {
374 422 do_action('fluent_boards/board_viewer_added', $boardId, $boardMember);
375 - } else {
376 - do_action('fluent_boards/board_member_added', $boardId, $boardMember);
377 423 }
378 424 return $boardMember;
379 425 }
380 426
@@ -412,8 +458,75 @@
412 458 $user['is_board_admin'] = false;
413 459 return $user;
414 460 }
415 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 +
416 529 public function getUsersOfBoards()
417 530 {
418 531 $userBoards = Relation::whereNotNull('board_id')
419 532 ->where('user_id', get_current_user_id())
@@ -422,35 +535,68 @@
422 535 return $userBoards;
423 536 }
424 537
425 538 /**
426 - * change board background
427 - * @param mixed $backgroundData
428 - * @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
429 548 */
430 -
431 549 public function setBoardBackground($backgroundData, $board_id)
432 550 {
433 - $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 +
434 568 $background = $board->background;
435 -
436 - // if board background has color
437 - if (isset($backgroundData['color'])) {
438 - $background['color'] = $backgroundData['color'];
439 - $background['image_url'] = null;
440 - $background['is_image'] = false;
569 + if (!is_array($background)) {
570 + $background = [];
441 571 }
442 572
443 - // if board background has image
573 + // Resolve image metadata from the board-owned attachment, never from the client URL.
444 574 if (isset($backgroundData['image_url'])) {
445 - $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);
446 587 $background['is_image'] = true;
447 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;
448 594 }
449 - $background['id'] = $backgroundData['id'];
450 595
451 596 $board->background = $background;
452 597 $board->save();
598 + do_action('fluent_boards/board_background_updated', $boardId, $oldBackground);
453 599
454 600 return $board->background;
455 601 }
456 602
@@ -460,25 +606,85 @@
460 606 * @param mixed $board_id
461 607 * @param mixed $stage_slug
462 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
463 609 */
464 - public function getStageTaskAvailablePositions($board_id, $stage_id)
610 + public function getStageTaskAvailablePositions($board_id, $stage_id, $task_id = null)
465 611 {
466 - $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()
467 619 ->where('board_id', $board_id)
468 620 ->where('parent_id', null)
469 621 ->where('stage_id', $stage_id)
470 622 ->whereNull('archived_at')
471 623 ->orderBy('position', 'asc')
472 - ->get()
473 - ->pluck('position')->toArray();
624 + ->get(['id', 'position']);
474 625
475 - $totalPosition = count($availablePositions);
476 - $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 + }
477 631
478 - 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 + ];
479 666 }
480 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 +
481 687 public function getAssigneesByBoard($board_id, $search = '')
482 688 {
483 689 $assignees = [];
484 690 $boardUsers = [];
@@ -538,10 +744,10 @@
538 744 }
539 745
540 746 // Check if the board is already in the list
541 747 if (!in_array($boardId, $recentBoardIds)) {
542 - // If there are already 3 boards, remove the last one
543 - if (count($recentBoardIds) >= 3) {
748 + // Keep the 4 most recently opened boards for the dashboard view.
749 + if (count($recentBoardIds) >= 4) {
544 750 array_pop($recentBoardIds);
545 751 }
546 752 } else {
547 753 // Remove the existing board id to move it to the front
@@ -577,8 +783,12 @@
577 783 if (!$recentBoardIds) {
578 784 return [];
579 785 }
580 786
787 + if (!is_array($recentBoardIds)) {
788 + $recentBoardIds = [];
789 + }
790 +
581 791 $currentUser = User::find($userId);
582 792
583 793 if (!PermissionManager::isAdmin($userId)){
584 794 $recentBoardIds = array_intersect($recentBoardIds, $currentUser->whichBoards->pluck('id')->toArray());
@@ -583,8 +793,10 @@
583 793 if (!PermissionManager::isAdmin($userId)){
584 794 $recentBoardIds = array_intersect($recentBoardIds, $currentUser->whichBoards->pluck('id')->toArray());
585 795 }
586 796
797 + $recentBoardIds = array_values(array_slice($recentBoardIds, 0, 4));
798 +
587 799 // This is for checking if that board is exists
588 800 // TODO: we will remove this code in future version
589 801 if (!$this->recentBoardBackwardCompatibilityCheck()) {
590 802 foreach ($recentBoardIds as $index => $boardId) {
@@ -597,9 +809,15 @@
597 809
598 810 $this->updateRecentBoardCheckMeta();
599 811 }
600 812
601 - 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();
602 820 }
603 821
604 822 public function getRecentBoardCheckMeta($userId = null){
605 823 if (!$userId) {
@@ -697,10 +915,15 @@
697 915
698 916 $contactOfBoard->delete();
699 917 }
700 918
701 - public function sendInvitationToBoard($boardId, $email)
919 + public function sendInvitationToBoard($boardId, $email, $role = 'member')
702 920 {
921 + $role = sanitize_text_field($role);
922 + if (!in_array($role, ['manager', 'member', 'viewer'], true)) {
923 + $role = 'member';
924 + }
925 +
703 926 $user = User::query()->where('user_email', $email)->first();
704 927
705 928 if ($user) {
706 929 return $user;
@@ -707,9 +930,9 @@
707 930 }
708 931
709 932 $current_user_id = get_current_user_id();
710 933
711 - do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id);
934 + do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id, $role);
712 935
713 936 return;
714 937
715 938 }
@@ -721,57 +944,124 @@
721 944 ->where('key', Constant::BOARD_INVITATION)
722 945 ->get();
723 946 }
724 947
725 - 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)
726 955 {
727 - 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();
728 975 }
729 976
730 - public function hasDataChanged($boardId)
977 + public function hasDataChanged($boardId, $includeArchived = false, $since = null)
731 978 {
732 979 $stages = [];
733 980 $labels = [];
734 981 $tasks = [];
735 - $labels = [];
736 - $taskDeleted = false;
737 - $stageDeleted = false;
738 - $labelDeleted = false;
739 - $oneMinuteAgoTimestamp = current_time('timestamp') - 60; // Get the current timestamp and subtract 60 seconds
740 - $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;
741 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 +
742 994 $board = Board::find($boardId);
743 995 if (!$board) {
744 - throw new \Exception("Board doesn't exists");
996 + throw new \Exception(esc_html__("Board doesn't exists", 'fluent-boards'));
745 997 }
746 - // if stage in this board has been deleted
747 - $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;
748 1000
749 - if ($stageDeleted) {
750 - $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();
751 1017 } else {
752 - $stages = (new StageService())->getLastOneMinuteUpdatedStages($boardId);
1018 + $stages = (new StageService())->getLastOneMinuteUpdatedStages($boardId, $lastUpdated, $includeArchived);
753 1019 }
754 1020
755 - $labelDeleted = Activity::where('object_id', $boardId)->where('updated_at', '>=', $oneMinuteAgo)->where('action', 'deleted')->where('column', 'label')->exists();
756 - if ($labelDeleted) {
757 - $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();
758 1033 } else {
759 - $labels = (new LabelService())->getLastOneMinuteUpdatedLabels($boardId);
1034 + $labels = (new LabelService())->getLastOneMinuteUpdatedLabels($boardId, $lastUpdated, $includeArchived);
760 1035 }
761 1036
762 - // if task in this board has been deleted
763 - $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();
764 1040
765 - 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) {
766 1053 $tasksQuery = Task::query()
767 1054 ->where([
768 1055 'board_id' => $boardId,
769 1056 'parent_id' => null,
770 - 'archived_at' => null
771 1057 ])
772 1058 ->with(['assignees', 'labels', 'watchers']);
773 1059
1060 + if (!$includeArchived) {
1061 + $tasksQuery->whereNull('archived_at');
1062 + }
1063 +
774 1064 if (!!defined('FLUENT_BOARDS_PRO_VERSION')) {
775 1065 $tasksQuery->with('customFields');
776 1066 }
777 1067
@@ -776,14 +1066,18 @@
776 1066 }
777 1067
778 1068 $tasks = $tasksQuery->orderBy('due_at', 'ASC')->get();
779 1069 } else {
780 - $tasks = (new TaskService())->getLastOneMinuteUpdatedTasks($boardId);
1070 + $tasks = (new TaskService())->getLastOneMinuteUpdatedTasks($boardId, $lastUpdated, $includeArchived);
781 1071 }
782 1072
783 1073 foreach ($tasks as $task) {
784 1074 $task->isOverdue = $task->isOverdue();
785 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);
786 1080 }
787 1081
788 1082 $board->background = \maybe_unserialize($board->background);
789 1083 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
@@ -789,201 +1083,128 @@
789 1083 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
790 1084 $board->custom_fields = $board->customFields;
791 1085 }
792 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 +
793 1096 return [
794 - 'board' => $board,
795 - 'stages' => $stages,
796 - 'labels' => $labels,
797 - 'tasks' => $tasks,
798 - 'taskDeleted' => $taskDeletedOrMovedFormBoard,
799 - '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,
800 1110 ];
801 1111 }
802 1112
803 - public function getAssociatedBoards($associatedId)
1113 + private function normalizeSyncCursor($since, $syncStartedAt)
804 1114 {
1115 + if (!is_string($since)) {
1116 + return null;
1117 + }
805 1118
806 - $boardIds = Meta::query()->where('value', $associatedId)
807 - ->where('object_type', Constant::OBJECT_TYPE_BOARD)
808 - ->where('key', Constant::BOARD_ASSOCIATED_CRM_CONTACT)
809 - ->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 + }
810 1123
811 - return Board::query()->whereIn('id', $boardIds)->with('stages', 'users')->get();
812 - }
1124 + if ($since > $syncStartedAt) {
1125 + return null;
1126 + }
813 1127
814 - private function deleteBoardMeta($boardId)
815 - {
816 - Meta::where('object_id', $boardId)
817 - ->where('object_type', Constant::OBJECT_TYPE_BOARD)
818 - ->delete();
1128 + if (strtotime($since) < strtotime('-24 hours', strtotime($syncStartedAt))) {
1129 + return null;
1130 + }
1131 +
1132 + return $since;
819 1133 }
820 1134
821 - public function copyBoard($boardData)
1135 + private function formatSyncTimestamp($timestamp)
822 1136 {
823 - $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 + }
824 1140
825 - $board = Board::create($boardData);
826 -
827 - $this->setCurrentUserPreferencesOnBoardCreate($board);
828 -
829 - return $board;
1141 + return (string) $timestamp;
830 1142 }
831 1143
832 - 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)
833 1152 {
834 - $board = Board::findOrFail($board_id);
835 - $taskQuery = Task::where('board_id', $board_id)
836 - ->whereNull('parent_id')
837 - ->whereNull('archived_at');
1153 + $associatedId = absint($associatedId);
1154 + $userId = $userId ?: get_current_user_id();
838 1155
839 - if($board->type == 'roadmap') {
840 - $pendingStage = $this->getNewIdeaStage($board->id);
841 - return $this->getIdeaReports($taskQuery, $pendingStage);
842 - } else {
843 - return $this->getTaskReports($taskQuery);
1156 + if (!$associatedId || !$userId) {
1157 + return [];
844 1158 }
845 - }
846 1159
847 - private function getNewIdeaStage($boardId)
848 - {
849 - return Stage::where('board_id', $boardId)
850 - ->where('type', 'stage')
851 - ->where('archived_at', null)
852 - ->orderBy('position', 'ASC')
853 - ->first();
854 - }
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');
855 1167
856 - public function getAllBoardReports(){
857 - $userId = get_current_user_id();
858 -
859 - $taskQuery = Task::whereNull('parent_id')
1168 + $boards = Board::query()
1169 + ->whereIn('id', array_values(array_unique(array_map('intval', $boardIds->toArray()))))
860 1170 ->whereNull('archived_at')
861 - ->whereHas('board', function ($query) {
862 - $query->where('type', 'to-do');
863 - });
1171 + ->byAccessUser($userId)
1172 + ->withCount('completedTasks')
1173 + ->with(['stages', 'users'])
1174 + ->orderBy('created_at', 'DESC')
1175 + ->get();
864 1176
865 - if (!PermissionManager::isAdmin($userId))
866 - {
867 - $currentUser = User::find($userId);
868 - $relatedBoardIds = $currentUser->whichBoards->where('type', 'to-do')->pluck('id');
869 - $taskQuery->whereIn('board_id', $relatedBoardIds);
1177 + foreach ($boards as $board) {
1178 + $board->users = Helper::sanitizeUserCollections($board->users);
870 1179 }
871 1180
872 - return $this->getTaskReports($taskQuery);
1181 + return $boards;
873 1182 }
874 1183
875 - private function getTaskReports($taskQuery)
1184 + private function deleteBoardMeta($boardId)
876 1185 {
877 - $totalTasksQuery = clone $taskQuery;
878 - $completedTaskQuery = clone $taskQuery;
879 - $openTaskQuery = clone $taskQuery;
880 - $overDueTaskQuery = clone $taskQuery;
881 -
882 - $completedTaskCount = $completedTaskQuery->where('status', 'closed')->count();
883 - $openTaskCount = $openTaskQuery->where('status', 'open')->count();
884 - $overDueTasks = $overDueTaskQuery->overdue(true)->count();
885 - $totalTasks = $totalTasksQuery->count();
886 -
887 - $taskQuery->where('status', 'open');
888 -
889 - $highQuery = clone $taskQuery;
890 - $mediumQuery = clone $taskQuery;
891 - $lowQuery = clone $taskQuery;
892 -
893 - $high = $highQuery->where('priority', 'high')->count();
894 - $low = $mediumQuery->where('priority', 'low')->count();
895 - $medium = $lowQuery->where('priority', 'medium')->count();
896 -
897 - $reportData = [
898 - 'completion' => [
899 - 'completed' => $completedTaskCount,
900 - 'incomplete' => $openTaskCount,
901 - 'overdue' => $overDueTasks,
902 - 'total' => $totalTasks
903 - ],
904 - 'priority' => [
905 - 'high' => $high,
906 - 'medium' => $medium,
907 - 'low' => $low
908 - ]
909 -
910 - ];
911 - return $reportData;
1186 + Meta::where('object_id', $boardId)
1187 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1188 + ->delete();
912 1189 }
913 1190
914 - private function getIdeaReports($taskQuery, $pendingStage)
1191 + public function copyBoard($boardData)
915 1192 {
916 - $pendingIdeaQuery = clone $taskQuery;
917 - $completedIdeaQuery = clone $taskQuery;
918 - $openIdeaQueryPage = clone $taskQuery;
919 - $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);
920 1199
921 - $pendingIdeaCount = $pendingIdeaQuery->where('status', 'open')->where('stage_id', $pendingStage->id)->count();
922 - $completedIdeaCount = $completedIdeaQuery->where('status', 'closed')->count();
923 - $openIdeaCountPage = $openIdeaQueryPage->where('status', 'open')->where('source', 'page')->count();
924 - $openIdeaCountWeb = $openIdeaQueryWeb->where('status', 'open')->where('source', 'web')->count();
925 - $totalIdeas = $openIdeaCountPage + $openIdeaCountWeb;
1200 + $board = Board::create($boardData);
926 1201
927 - $taskQuery->where('status', 'open');
1202 + $this->setCurrentUserPreferencesOnBoardCreate($board);
928 1203
929 - $highQuery = clone $taskQuery;
930 - $mediumQuery = clone $taskQuery;
931 - $lowQuery = clone $taskQuery;
932 -
933 - $high = $highQuery->where('priority', 'high')->count();
934 - $low = $mediumQuery->where('priority', 'low')->count();
935 - $medium = $lowQuery->where('priority', 'medium')->count();
936 -
937 - $reportData = [
938 - 'completion' => [
939 - 'pending' => $pendingIdeaCount,
940 - 'completed' => $completedIdeaCount,
941 - 'ideaFromPage' => $openIdeaCountPage,
942 - 'total' => $totalIdeas
943 - ],
944 - 'priority' => [
945 - 'high' => $high,
946 - 'medium' => $medium,
947 - 'low' => $low
948 - ]
949 - ];
950 - return $reportData;
1204 + return $board;
951 1205 }
952 1206
953 - public function getStageWiseBoardReports($board_id)
954 - {
955 - $stages = Stage::where('board_id', $board_id)
956 - ->where('type', 'stage')
957 - ->whereNull('archived_at')
958 - ->get();
959 -
960 - foreach ($stages as $stage) {
961 - $completedTaskCount = Task::where('stage_id', $stage->id)
962 - ->where('status', 'closed')
963 - ->count();
964 -
965 - $openTaskCount = Task::where('stage_id', $stage->id)
966 - ->whereNull('due_at')
967 - ->where('status', 'open')
968 - ->count();
969 -
970 - $overDue = Task::where('stage_id', $stage->id)
971 - ->whereNotNull('due_at')
972 - ->where('status', 'open')
973 - ->overdue(true)
974 - ->count();
975 -
976 - $stage->report = [
977 - 'completed' => $completedTaskCount,
978 - 'incomplete' => $openTaskCount,
979 - 'overdue' => $overDue
980 - ];
981 - }
982 -
983 - return $stages;
984 - }
985 -
986 1207 public function archiveBoard($boardId)
987 1208 {
988 1209 $board = Board::findOrFail($boardId);
989 1210 $board->archived_at = current_time('mysql');
@@ -1040,5 +1261,209 @@
1040 1261 $user['is_board_admin'] = false;
1041 1262 return $user;
1042 1263 }
1043 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 + }
1044 1469 }