PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.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 All 43 releases
← All changes | app/Services/StageService.php +443 -99 1.21 → 2.2.0 View file →
@@ -4,10 +4,17 @@
4 4
5 5 use FluentBoards\App\Models\Task;
6 6 use FluentBoards\App\Models\Board;
7 7 use FluentBoards\App\Models\Stage;
8 +use FluentBoards\App\Models\TaskMeta;
9 +use FluentBoards\App\Models\Relation;
10 +use FluentBoards\App\Models\User;
11 +use FluentBoards\App\App;
8 12 use FluentBoards\Framework\Http\Request\Request;
9 13 use FluentBoards\Framework\Support\Arr;
14 +use FluentBoards\App\Services\Constant;
15 +use FluentBoards\App\Services\AttachmentFileService;
16 +use FluentBoards\App\Services\PermissionManager;
10 17
11 18 class StageService
12 19 {
13 20 public function createDefaultStages($board)
@@ -54,37 +61,141 @@
54 61 ]
55 62 ]
56 63 ];
57 64 }
65 +
66 + /**
67 + * Update one stage property and emit side effects for real title transitions.
68 + *
69 + * @param string $col
70 + * @param mixed $value
71 + * @param int $stageId
72 + * @return Stage
73 + */
58 74 public function updateStageProperty($col, $value, $stageId)
59 75 {
60 76 $stage = Stage::findOrFail($stageId);
77 + $stageBeforeUpdate = clone $stage;
61 78
62 79 if ('title' == $col) {
63 - $this->updateTitle($value, $stage);
80 + $stage = $this->updateTitle($value, $stage);
64 81 } elseif ('status' == $col) {
65 - $this->updateStatus($value, $stage);
82 + $stage = $this->updateStatus($value, $stage);
66 83 } elseif ('color' == $col) {
67 - $this->updateColor($value, $stage);
84 + $stage = $this->updateColor($value, $stage);
68 85 } elseif ('bg_color' == $col) {
69 - $this->updateBackgroundColor($value, $stage);
86 + $stage = $this->updateBackgroundColor($value, $stage);
70 87 } elseif ('archived_at' == $col) {
71 - $this->updateArchivedAt($value, $stage);
88 + $stage = $this->updateArchivedAt($value, $stage);
72 89 }
90 +
91 + if ('title' == $col && $stageBeforeUpdate->title !== $stage->title) {
92 + do_action('fluent_boards/stage_updated', $stage->board_id, [
93 + 'title' => $stage->title,
94 + 'cover_bg' => $stage->bg_color
95 + ], $stageBeforeUpdate);
96 + }
97 +
73 98 return $stage;
74 99 }
75 100
76 - public function getLastOneMinuteUpdatedStages($boardId)
101 + public function getLastOneMinuteUpdatedStages($boardId, $lastUpdated = null, $includeArchived = true)
77 102 {
78 - $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
79 - return Stage::where('board_id', $boardId)
80 - ->where('updated_at', '>=', date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp))
81 - ->get();
103 + if (!$lastUpdated) {
104 + $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
105 + $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp);
106 + }
107 +
108 + $stagesQuery = Stage::where('board_id', $boardId)
109 + ->where('updated_at', '>=', $lastUpdated);
110 +
111 + if (!$includeArchived) {
112 + $stagesQuery->whereNull('archived_at');
113 + }
114 +
115 + return $stagesQuery->get();
82 116 }
83 117
118 + /**
119 + * Get archived stages newest-first with archive actor metadata.
120 + *
121 + * @param array $data
122 + * @param int $boardId
123 + * @return mixed
124 + * @throws \Exception
125 + */
126 + public function getArchivedStages($data, $boardId)
127 + {
128 + if (!$boardId) {
129 + throw new \Exception(esc_html__('Board id is required', 'fluent-boards'));
130 + }
131 +
132 + $noPagination = !empty($data['noPagination']);
133 + $perPage = max(1, min(50, absint($data['per_page'] ?? 30)));
134 + $page = max(1, absint($data['page'] ?? 1));
135 +
136 + $stagesQuery = Stage::where('board_id', $boardId)
137 + ->whereNotNull('archived_at')
138 + ->orderBy('archived_at', 'DESC')
139 + ->orderBy('id', 'DESC');
140 +
141 + if ($noPagination) {
142 + $stages = $stagesQuery->get();
143 + } else {
144 + $stages = $stagesQuery->paginate($perPage, ['*'], 'page', $page);
145 + }
146 +
147 + $this->attachArchivedStageActors($stages);
148 +
149 + return $stages;
150 + }
151 +
152 + /**
153 + * Attach nullable archived-by fields from stage settings without per-stage user queries.
154 + *
155 + * @param mixed $stages
156 + * @return void
157 + */
158 + private function attachArchivedStageActors($stages)
159 + {
160 + $archivedByIds = [];
161 +
162 + foreach ($stages as $stage) {
163 + $stage->archived_by_id = null;
164 + $stage->archived_by = null;
165 +
166 + $settings = is_array($stage->settings) ? $stage->settings : [];
167 + $archivedById = !empty($settings['archived_by_id']) ? absint($settings['archived_by_id']) : 0;
168 +
169 + if ($archivedById) {
170 + $stage->archived_by_id = $archivedById;
171 + $archivedByIds[] = $archivedById;
172 + }
173 + }
174 +
175 + $archivedByIds = array_values(array_unique(array_filter($archivedByIds)));
176 +
177 + if (empty($archivedByIds)) {
178 + return;
179 + }
180 +
181 + $users = User::whereIn('ID', $archivedByIds)->get()->keyBy('ID');
182 +
183 + foreach ($stages as $stage) {
184 + if (!$stage->archived_by_id) {
185 + continue;
186 + }
187 +
188 + $user = $users->get($stage->archived_by_id);
189 + $stage->archived_by = $user ? Helper::sanitizeUserCollections($user) : null;
190 + }
191 + }
192 +
84 193 private function updateTitle($value, $stage)
85 194 {
86 -
195 + $stage->title = $value;
196 + $stage->save();
197 + return $stage;
87 198 }
88 199
89 200 private function updateStatus($value, $stage)
90 201 {
@@ -91,23 +202,30 @@
91 202 $oldSettings = $stage->settings;
92 203 $oldSettings['default_task_status'] = $value;
93 204 $stage->settings = $oldSettings;
94 205 $stage->save();
206 + return $stage;
95 207 }
96 208
97 209 private function updateColor($value, $stage)
98 210 {
99 -
211 + $stage->color = $value;
212 + $stage->save();
213 + return $stage;
100 214 }
101 215
102 216 private function updateBackgroundColor($value, $stage)
103 217 {
104 -
218 + $stage->bg_color = $value;
219 + $stage->save();
220 + return $stage;
105 221 }
106 222
107 223 private function updateArchivedAt($value, $stage)
108 224 {
109 -
225 + $stage->archived_at = $value;
226 + $stage->save();
227 + return $stage;
110 228 }
111 229
112 230 public function createStage($stageData, $boardId)
113 231 {
@@ -114,20 +232,18 @@
114 232 $stage = new Stage();
115 233 $stage->board_id = $boardId;
116 234 $stage->title = $stageData['title'];
117 235 $stage->settings = [
118 - 'default_task_status' => Arr::get($stageData, 'status') ?? 'open'
236 + 'default_task_status' => Arr::get($stageData, 'status') ?? 'open',
237 + 'default_task_assignees' => []
119 238 ];
120 - if (!Arr::get($stageData, 'position')) {
121 - $lastStagePosition = $this->getLastPositionOfStagesOfBoard($boardId);
122 - $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
123 - $stage->save();
124 - } else {
125 - $stage->position = (int)$stageData['position'];
126 - $stage->save();
127 - $this->moveOtherStages($stage);
239 + $providerPosition = Arr::get($stageData, 'position');
240 + $lastStagePosition = $this->getLastPositionOfStagesOfBoard($boardId);
241 + $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
242 + $stage->save();
243 + if($providerPosition){
244 + $stage->moveToNewPosition($providerPosition);
128 245 }
129 -
130 246 return $stage;
131 247 }
132 248
133 249 public function getLastPositionOfStagesOfBoard($boardId)
@@ -152,36 +268,110 @@
152 268 $stage->save();
153 269 }
154 270 }
155 271
156 - public function copyStagesOfBoard($board, $fromBoardId)
272 + /**
273 + * Copy all active stages and their settings to another board in display order.
274 + *
275 + * @param Board $board
276 + * @param int $fromBoardId
277 + * @param string $isWithTemplates
278 + * @param array|null $stageIds
279 + * @return array
280 + */
281 + public function copyStagesOfBoard($board, $fromBoardId, $isWithTemplates = 'no', $stageIds = null)
157 282 {
158 - $stages = Stage::where('board_id', $fromBoardId)->where('type', 'stage')->whereNull('archived_at')->get();
159 - $stageMapForCopyingTask = array();
160 - foreach($stages as $key => $stage)
161 - {
162 - $stageToSave = array();
163 - $stageToSave['title'] = $stage['title'];
164 - $stageToSave['board_id'] = $board->id;
165 - $stageToSave['slug'] = str_replace(' ', '-', strtolower($stage['title']));
166 - $stageToSave['type'] = 'stage';
167 - $stageToSave['position'] = $key + 1;
168 - $stageToSave['settings'] = [
169 - 'default_task_status' => $stage->settings['default_task_status']
283 + $stageQuery = Stage::where('board_id', $fromBoardId)
284 + ->where('type', 'stage')
285 + ->whereNull('archived_at');
286 +
287 + if (is_array($stageIds)) {
288 + $stageIds = array_values(array_filter(array_map('absint', $stageIds)));
289 +
290 + if (!$stageIds) {
291 + return [];
292 + }
293 +
294 + $stageQuery->whereIn('id', $stageIds);
295 + }
296 +
297 + $stages = $stageQuery->orderBy('position', 'asc')->get();
298 + $stageMap = [];
299 +
300 + foreach ($stages as $key => $stage) {
301 + $settings = $stage->settings ?: [];
302 +
303 + // Board copies must not turn their stages into reusable stage templates.
304 + if ($isWithTemplates !== 'yes') {
305 + unset($settings['is_template']);
306 + }
307 +
308 + $stageData = [
309 + 'title' => $stage->title,
310 + 'board_id' => $board->id,
311 + 'slug' => str_replace(' ', '-', strtolower($stage->title)),
312 + 'type' => 'stage',
313 + 'position' => $key + 1,
314 + 'bg_color' => $stage->bg_color,
315 + 'settings' => $settings,
170 316 ];
171 - $newStage = Stage::create($stageToSave);
172 - $stageMapForCopyingTask[$stage['id']] = $newStage->id;
317 +
318 + $newStage = Stage::create($stageData);
319 + $stageMap[$stage->id] = $newStage->id;
173 320 }
174 - return $stageMapForCopyingTask;
321 +
322 + return $stageMap;
175 323 }
176 324
177 - public function importStagesFromBoard($board_id, $selectedStages)
325 + public function importStagesFromBoard($board_id, $selectedStages, $position = null)
178 326 {
179 - $targetStages = Stage::whereIn('id', $selectedStages)->get();
327 + $board_id = absint($board_id);
328 + $selectedStages = array_values(array_unique(array_filter(array_map('absint', (array) $selectedStages))));
329 +
330 + if (!$selectedStages) {
331 + return;
332 + }
333 +
334 + $targetStages = Stage::whereIn('id', $selectedStages)
335 + ->whereNull('archived_at')
336 + ->get();
337 +
338 + if (count($targetStages) !== count($selectedStages)) {
339 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
340 + }
341 +
342 + $sourceBoardIds = array_values(array_unique(array_map('absint', $targetStages->pluck('board_id')->toArray())));
343 +
344 + if (in_array(0, $sourceBoardIds, true)) {
345 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
346 + }
347 +
348 + $userId = get_current_user_id();
349 +
350 + if (!$userId) {
351 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
352 + }
353 +
354 + if (!PermissionManager::isAdmin($userId)) {
355 + $allowedBoardIds = Relation::where('foreign_id', $userId)
356 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
357 + ->whereIn('object_id', $sourceBoardIds)
358 + ->pluck('object_id')
359 + ->toArray();
360 +
361 + $allowedBoardIds = array_map('intval', $allowedBoardIds);
362 + $unauthorizedBoardIds = array_filter($sourceBoardIds, function ($sourceBoardId) use ($allowedBoardIds) {
363 + return !in_array($sourceBoardId, $allowedBoardIds, true);
364 + });
365 +
366 + if ($unauthorizedBoardIds) {
367 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
368 + }
369 + }
370 +
180 371 $stageMapForCopyingTask = array();
181 372 $stageIdsToCopy = array();
182 373
183 - //need to define position of stage
184 374 $numberOfStages = Stage::where('board_id', $board_id)->whereNull('archived_at')->count();
185 375
186 376 foreach($targetStages as $key => $stage)
187 377 {
@@ -194,8 +384,11 @@
194 384 $stageToSave['settings'] = [
195 385 'default_task_status' => $stage->settings['default_task_status']
196 386 ];
197 387 $newStage = Stage::create($stageToSave);
388 + if($position) {
389 + $newStage->moveToNewPosition( (int) $position + $key );
390 + }
198 391 $stageMapForCopyingTask[$stage['id']] = $newStage->id;
199 392 $stageIdsToCopy[] = $stage['id'];
200 393 }
201 394
@@ -203,36 +396,84 @@
203 396 }
204 397
205 398 public function importTasks($boardId, $stageMapper, $stageIds)
206 399 {
207 - $tasksToImport = Task::whereIn('stage_id', $stageIds)->whereNull('archived_at')->get();
400 + $tasksToImport = $this->getAllParentAndSubTasksOfStages($stageIds);
401 + $subtaskGroupChildRelations = [];
402 + $taskIdsToImport = $tasksToImport->pluck('id')->toArray();
208 403
209 - foreach($tasksToImport as $task)
210 - {
211 - $newTask = array();
212 - $newTask['title'] = $task->title;
213 - $newTask['description'] = $task->description;
214 - $newTask['board_id'] = $boardId;
215 - $newTask['stage_id'] = $stageMapper[$task->stage_id];
216 - $newTask['status'] = $task->status;
217 - $newTask['priority'] = $task->priority;
218 - $newTask['position'] = $task->position;
219 - $newTask['due_at'] = $task->due_at;
220 - Task::create($newTask);
404 + if ($taskIdsToImport) {
405 + foreach (TaskMeta::where('key', Constant::SUBTASK_GROUP_CHILD)
406 + ->whereIn('task_id', $taskIdsToImport)
407 + ->get() as $relation) {
408 + $subtaskGroupChildRelations[$relation->task_id] = $relation;
409 + }
221 410 }
411 + $taskMap = [];
412 + $subtaskGroupMap = [];
413 + $attachmentFileService = new AttachmentFileService();
414 + $dbInstance = App::getInstance('db');
222 415
223 - //update task count of board
224 - $totalTasks = sizeof($tasksToImport);
225 - $board = Board::findOrFail($boardId);
226 - $settings = $board->settings ?? [];
416 + $dbInstance->beginTransaction();
227 417
228 - if (isset($settings['tasks_count'])) {
229 - $settings['tasks_count'] += $totalTasks;
230 - } else {
231 - $settings['tasks_count'] = $totalTasks;
418 + try {
419 + foreach($tasksToImport as $task)
420 + {
421 + $newTask = array();
422 + $newTask['title'] = $task->title;
423 + $newTask['parent_id'] = $task->parent_id ? $taskMap[$task->parent_id] : null;
424 + $newTask['description'] = $task->description;
425 + $newTask['board_id'] = $boardId;
426 + $newTask['stage_id'] = $stageMapper[$task->stage_id];
427 + $newTask['status'] = $task->status;
428 + $newTask['priority'] = $task->priority;
429 + $newTask['position'] = $task->position;
430 + $newTask['due_at'] = $task->due_at;
431 + $backgroundColor = $task->settings['cover']['backgroundColor'] ?? '';
432 + $newTask['settings'] = [
433 + 'cover' => [
434 + 'backgroundColor' => $backgroundColor,
435 + ]
436 + ];
437 + $newTask = Task::create($newTask);
438 + $attachmentFileService->cloneTaskFilesToBoard($task, $newTask, $boardId);
439 +
440 + if (!$task->parent_id) {
441 + $taskMap[$task->id] = $newTask->id;
442 + //group mapping
443 + $subtaskGroupMap = (new TaskService())->copySubtaskGroup($task, $newTask, $subtaskGroupMap);
444 + } else {
445 + $groupRelationOfTask = $subtaskGroupChildRelations[$task->id] ?? null;
446 +
447 + if ($groupRelationOfTask && $subtaskGroupMap[$groupRelationOfTask->value]) {
448 + TaskMeta::create([
449 + 'task_id' => $newTask->id,
450 + 'key' => Constant::SUBTASK_GROUP_CHILD,
451 + 'value' => $subtaskGroupMap[$groupRelationOfTask->value]
452 + ]);
453 + }
454 + }
455 + }
456 +
457 + //update task count of board
458 + $totalTasks = sizeof($tasksToImport);
459 + $board = Board::findOrFail($boardId);
460 + $settings = $board->settings ?? [];
461 +
462 + if (isset($settings['tasks_count'])) {
463 + $settings['tasks_count'] += $totalTasks;
464 + } else {
465 + $settings['tasks_count'] = $totalTasks;
466 + }
467 + $board->settings = $settings;
468 + $board->save();
469 +
470 + $dbInstance->commit();
471 + } catch (\Exception $e) {
472 + $dbInstance->rollBack();
473 + $attachmentFileService->rollbackCreatedFiles();
474 + throw $e;
232 475 }
233 - $board->settings = $settings;
234 - $board->save();
235 476 }
236 477
237 478 public function updateStageTemplate($stage_id)
238 479 {
@@ -267,28 +508,98 @@
267 508
268 509 public function moveAllTasks($oldStageId, $newStageId)
269 510 {
270 511 $tasks = Task::where('stage_id', $oldStageId)->whereNull('parent_id')->whereNull('archived_at')->get();
512 + $sourceStage = Stage::findOrFail($oldStageId);
513 + $targetStage = Stage::findOrFail($newStageId);
271 514
515 + // Reuse task-user relations unless default assignments change them.
516 + $tasks->load(['assignees', 'watchers']);
517 +
272 518 // get the last position available of that stage
273 - $position = (new TaskService())->getLastPositionOfTasks($newStageId);
519 + $taskService = new TaskService();
520 + $position = $taskService->getLastPositionOfTasks($newStageId);
521 + $notificationService = new NotificationService();
522 + $currentUserId = get_current_user_id();
523 + // Stage-email eligibility per watcher, resolved in batches and shared across tasks.
524 + $emailEligibility = [];
274 525
275 526 // update tasks stage and position
276 527 foreach ($tasks as $key => $task) {
528 + $taskOldStageId = $task->stage_id;
529 +
530 + // Clean up archived_by_stage meta when moving to different stage
531 + TaskMeta::where('task_id', $task->id)
532 + ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE)
533 + ->delete();
534 +
277 535 $task->stage_id = $newStageId;
278 536 $task->position = $position + $key;
537 + $task->setRelation('stage', $targetStage);
279 538 $task->save();
539 +
540 + if ((int) $taskOldStageId !== (int) $newStageId) {
541 + $assignmentsChanged = $taskService->manageDefaultAssignees($task, $newStageId, $targetStage);
542 + if ($targetStage->defaultTaskStatus() === 'closed' && $task->status !== 'closed') {
543 + $task = $task->close();
544 + }
545 + // Assignment hooks may change assignees and watchers before notification.
546 + if ($assignmentsChanged) {
547 + $task->load(['assignees', 'watchers']);
548 + }
549 +
550 + do_action('fluent_boards/task_stage_updated', $task, $taskOldStageId, [
551 + 'source' => $sourceStage,
552 + 'target' => $targetStage,
553 + ]);
554 +
555 + $watcherIds = array_diff(array_map('intval', $task->watchers->pluck('ID')->toArray()), [$currentUserId]);
556 + $unknownIds = array_diff($watcherIds, array_keys($emailEligibility));
557 + if ($unknownIds) {
558 + $emailEligibility += array_fill_keys($unknownIds, false);
559 + $enabledIds = $notificationService->getEmailEnabledUserIds($task->board_id, $unknownIds, Constant::BOARD_EMAIL_STAGE_CHANGE);
560 + foreach ($enabledIds as $enabledId) {
561 + $emailEligibility[$enabledId] = true;
562 + }
563 + }
564 + $recipients = [];
565 + foreach ($task->watchers as $watcher) {
566 + if (!empty($emailEligibility[(int) $watcher->ID])) {
567 + $recipients[] = $watcher->user_email;
568 + }
569 + }
570 + $taskService->sendMailAfterTaskModify('stage_change', $recipients, $task->id);
571 + }
280 572 }
573 +
574 + if ($tasks->count() && (int) $sourceStage->id !== (int) $targetStage->id) {
575 + do_action(
576 + 'fluent_boards/tasks_moved_between_stages',
577 + $sourceStage->board_id,
578 + $sourceStage,
579 + $targetStage,
580 + $tasks->count()
581 + );
582 + }
583 +
281 584 return $tasks;
282 585 }
283 - public function archiveAllTasksInStage($stage_id)
586 + public function archiveAllTasksInStage($stage_id, $boardId = null)
284 587 {
285 - $tasks = Task::where('stage_id', $stage_id)->whereNull('parent_id')->whereNull('archived_at')->get();
588 + $tasksQuery = Task::where('stage_id', absint($stage_id))
589 + ->whereNull('parent_id')
590 + ->whereNull('archived_at');
591 +
592 + if ($boardId) {
593 + $tasksQuery->where('board_id', absint($boardId));
594 + }
595 +
596 + $tasks = $tasksQuery->get();
286 597 foreach ($tasks as $task) {
287 598 $task->position = 0;
288 599 $task->archived_at = current_time('mysql');
289 600 $task->save();
290 - do_action('fluent_boards/board_task_archived', $task);
601 + do_action('fluent_boards/task_archived', $task);
291 602 }
292 603 return $tasks;
293 604 }
294 605
@@ -328,16 +639,14 @@
328 639 $stageToPush['title'] = $stage['title'];
329 640 $stageToPush['board_id'] = $board->id;
330 641 $stageToPush['position'] = $index + 1;
331 642 $stageToPush['slug'] = $this->createSlug($stage['title']);
643 + $closedStageTitles = ['completed', 'done'];
644 + $stageToPush['settings'] = [
645 + 'default_task_status' => in_array(strtolower(Arr::get($stage, 'title')), $closedStageTitles) ? 'closed' : 'open',
646 + 'is_template' => false
647 + ];
332 648
333 - if (Arr::get($stage, 'title') == 'Completed') {
334 - $stageToPush['settings'] = [
335 - 'default_task_status' => 'closed',
336 - 'is_template' => false
337 - ];
338 - }
339 -
340 649 $stage = Stage::create($stageToPush);
341 650 if($index == 0){
342 651 $firstStage = $stage;
343 652 }
@@ -351,9 +660,9 @@
351 660 }
352 661
353 662 public function stagesByBoardId($boardId)
354 663 {
355 - return Stage::where('board_id', $boardId)->whereNull('archived_at')->get();
664 + return Stage::where('board_id', $boardId)->whereNull('archived_at')->orderBy('position', 'asc')->get();
356 665 }
357 666
358 667
359 668
@@ -363,9 +672,9 @@
363 672 $orderOptions = ['ASC', 'DESC'];
364 673
365 674 // Validate order and orderBy parameters
366 675 if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) {
367 - throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
676 + throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
368 677 }
369 678
370 679 $tasksQuery = Task::where('stage_id', $stage_id)
371 680 ->whereNull('parent_id')
@@ -374,9 +683,9 @@
374 683
375 684 // Apply ordering based on the specified order and orderBy
376 685 switch ($order) {
377 686 case 'priority':
378 - $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
687 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$orderBy}");
379 688 break;
380 689
381 690 case 'due_at':
382 691 if ($orderBy === 'ASC') {
@@ -410,31 +719,66 @@
410 719 });
411 720 return $tasks;
412 721 }
413 722
414 - public function checkStageEditable($stages, $toBeUpdatedStage)
723 +
724 + public function updateStage($updatedStage, $board_id, $oldStage)
415 725 {
416 - $stageEditable = true;
417 - if (empty(sanitize_text_field($toBeUpdatedStage['title']))) {
418 - $stageEditable = false;
419 - } else {
420 - foreach ($stages as $stage) { // check if stage label is similar to any other stage label
421 - if (strtolower($stage->title) == strtolower(trim($toBeUpdatedStage['title']))) {
422 - $stageEditable = false;
423 - break;
424 - }
425 - }
726 + $stageBeforeUpdate = clone $oldStage;
727 + $oldStage->title = sanitize_text_field(Arr::get($updatedStage, 'title'));
728 + $oldStage->bg_color = sanitize_text_field(Arr::get($updatedStage, 'cover_bg'));
729 + $oldStage->save();
730 + do_action('fluent_boards/stage_updated', $board_id, $updatedStage, $stageBeforeUpdate);
731 + return $oldStage;
732 + }
733 +
734 + public function setDefaultAssignees($stage_id, $assignees)
735 + {
736 + $stage = Stage::findOrFail($stage_id);
737 + if ($stage) {
738 + $settings = $stage->settings;
739 + $settings['default_task_assignees'] = $assignees;
740 + $stage->settings = $settings;
741 + $stage->save();
742 +
743 + do_action('fluent_boards/default_assignees_updated', $stage, $assignees);
744 +
745 + return $stage;
426 746 }
747 + }
427 748
428 - return $stageEditable;
749 + public function setDefaultWatchers($stage_id, $watchers)
750 + {
751 + $stage = Stage::findOrFail($stage_id);
752 + if ($stage) {
753 + $settings = $stage->settings;
754 + $settings['default_task_watchers'] = $watchers;
755 + $stage->settings = $settings;
756 + $stage->save();
757 +
758 + do_action('fluent_boards/default_watchers_updated', $stage, $watchers);
759 +
760 + return $stage;
761 + }
429 762 }
430 763
431 - public function updateStage($updatedStage, $board_id, $oldStage)
764 + public function getAllParentAndSubTasksOfStages($stageIds)
432 765 {
433 - $oldTitle = $oldStage->title;
434 - $oldStage->title = Arr::get($updatedStage, 'title');
435 - $oldStage->bg_color = Arr::get($updatedStage, 'cover_bg');
436 - $oldStage->save();
437 - do_action('fluent_boards/board_stage_updated', $board_id, $updatedStage['title'], $oldTitle);
438 - return $oldStage;
766 + // Fetch parent task IDs for the given stage_ids
767 + $parentTaskIds = Task::whereIn('stage_id', $stageIds)
768 + ->whereNull('archived_at')
769 + ->whereNull('parent_id')
770 + ->pluck('id');
771 +
772 + // Fetch parent tasks and subtasks in a single query
773 + $tasks = Task::whereIn('stage_id', $stageIds)
774 + ->whereNull('parent_id')
775 + ->whereNull('archived_at')
776 + ->get();
777 +
778 + $subtasks = Task::whereIn('parent_id', $parentTaskIds)
779 + ->get();
780 +
781 + // Combine parent tasks and subtasks into a single collection
782 + return $tasks->merge($subtasks);
439 783 }
440 784 }