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/StageService.php +174 -28 1.95.3 → 2.1.0 View file →
@@ -6,8 +6,9 @@
6 6 use FluentBoards\App\Models\Board;
7 7 use FluentBoards\App\Models\Stage;
8 8 use FluentBoards\App\Models\TaskMeta;
9 9 use FluentBoards\App\Models\Relation;
10 +use FluentBoards\App\Models\User;
10 11 use FluentBoards\App\App;
11 12 use FluentBoards\Framework\Http\Request\Request;
12 13 use FluentBoards\Framework\Support\Arr;
13 14 use FluentBoards\App\Services\Constant;
@@ -60,11 +61,21 @@
60 61 ]
61 62 ]
62 63 ];
63 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 + */
64 74 public function updateStageProperty($col, $value, $stageId)
65 75 {
66 76 $stage = Stage::findOrFail($stageId);
77 + $stageBeforeUpdate = clone $stage;
67 78
68 79 if ('title' == $col) {
69 80 $stage = $this->updateTitle($value, $stage);
70 81 } elseif ('status' == $col) {
@@ -75,8 +86,16 @@
75 86 $stage = $this->updateBackgroundColor($value, $stage);
76 87 } elseif ('archived_at' == $col) {
77 88 $stage = $this->updateArchivedAt($value, $stage);
78 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 +
79 98 return $stage;
80 99 }
81 100
82 101 public function getLastOneMinuteUpdatedStages($boardId, $lastUpdated = null, $includeArchived = true)
@@ -95,8 +114,83 @@
95 114
96 115 return $stagesQuery->get();
97 116 }
98 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 +
99 193 private function updateTitle($value, $stage)
100 194 {
101 195 $stage->title = $value;
102 196 $stage->save();
@@ -174,31 +268,59 @@
174 268 $stage->save();
175 269 }
176 270 }
177 271
178 - public function copyStagesOfBoard($board, $fromBoardId, $isWithTemplates='no')
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)
179 282 {
180 - $stages = Stage::where('board_id', $fromBoardId)->where('type', 'stage')->whereNull('archived_at')->orderBy('position', 'asc')->get();
181 - $stageMapForCopyingTask = array();
182 - foreach($stages as $key => $stage)
183 - {
184 - $stageToSave = array();
185 - $stageToSave['title'] = $stage['title'];
186 - $stageToSave['board_id'] = $board->id;
187 - $stageToSave['slug'] = str_replace(' ', '-', strtolower($stage['title']));
188 - $stageToSave['type'] = 'stage';
189 - $stageToSave['position'] = $key + 1;
190 - $stageToSave['bg_color'] = $stage['bg_color'];
191 - $stageToSave['settings'] = [
192 - '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,
193 316 ];
194 - if (!empty($stage->settings['is_template']) && $isWithTemplates == 'yes') {
195 - $stageToSave['settings']['is_template'] = $stage->settings['is_template'];
196 - }
197 - $newStage = Stage::create($stageToSave);
198 - $stageMapForCopyingTask[$stage['id']] = $newStage->id;
317 +
318 + $newStage = Stage::create($stageData);
319 + $stageMap[$stage->id] = $newStage->id;
199 320 }
200 - return $stageMapForCopyingTask;
321 +
322 + return $stageMap;
201 323 }
202 324
203 325 public function importStagesFromBoard($board_id, $selectedStages, $position = null)
204 326 {
@@ -386,14 +508,21 @@
386 508
387 509 public function moveAllTasks($oldStageId, $newStageId)
388 510 {
389 511 $tasks = Task::where('stage_id', $oldStageId)->whereNull('parent_id')->whereNull('archived_at')->get();
512 + $sourceStage = Stage::findOrFail($oldStageId);
513 + $targetStage = Stage::findOrFail($newStageId);
390 514
515 + // Load shared watcher data once; stage data is injected per task below.
516 + $tasks->load('watchers');
517 +
391 518 // get the last position available of that stage
392 519 $position = (new TaskService())->getLastPositionOfTasks($newStageId);
393 520
394 521 // update tasks stage and position
395 522 foreach ($tasks as $key => $task) {
523 + $taskOldStageId = $task->stage_id;
524 +
396 525 // Clean up archived_by_stage meta when moving to different stage
397 526 TaskMeta::where('task_id', $task->id)
398 527 ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE)
399 528 ->delete();
@@ -399,10 +528,29 @@
399 528 ->delete();
400 529
401 530 $task->stage_id = $newStageId;
402 531 $task->position = $position + $key;
532 + $task->setRelation('stage', $targetStage);
403 533 $task->save();
534 +
535 + if ((int) $taskOldStageId !== (int) $newStageId) {
536 + do_action('fluent_boards/task_stage_updated', $task, $taskOldStageId, [
537 + 'source' => $sourceStage,
538 + 'target' => $targetStage,
539 + ]);
540 + }
404 541 }
542 +
543 + if ($tasks->count() && (int) $sourceStage->id !== (int) $targetStage->id) {
544 + do_action(
545 + 'fluent_boards/tasks_moved_between_stages',
546 + $sourceStage->board_id,
547 + $sourceStage,
548 + $targetStage,
549 + $tasks->count()
550 + );
551 + }
552 +
405 553 return $tasks;
406 554 }
407 555 public function archiveAllTasksInStage($stage_id, $boardId = null)
408 556 {
@@ -460,16 +608,14 @@
460 608 $stageToPush['title'] = $stage['title'];
461 609 $stageToPush['board_id'] = $board->id;
462 610 $stageToPush['position'] = $index + 1;
463 611 $stageToPush['slug'] = $this->createSlug($stage['title']);
612 + $closedStageTitles = ['completed', 'done'];
613 + $stageToPush['settings'] = [
614 + 'default_task_status' => in_array(strtolower(Arr::get($stage, 'title')), $closedStageTitles) ? 'closed' : 'open',
615 + 'is_template' => false
616 + ];
464 617
465 - if (Arr::get($stage, 'title') == 'Completed') {
466 - $stageToPush['settings'] = [
467 - 'default_task_status' => 'closed',
468 - 'is_template' => false
469 - ];
470 - }
471 -
472 618 $stage = Stage::create($stageToPush);
473 619 if($index == 0){
474 620 $firstStage = $stage;
475 621 }
@@ -506,9 +652,9 @@
506 652
507 653 // Apply ordering based on the specified order and orderBy
508 654 switch ($order) {
509 655 case 'priority':
510 - $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
656 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$orderBy}");
511 657 break;
512 658
513 659 case 'due_at':
514 660 if ($orderBy === 'ASC') {