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/Hooks/Handlers/ActivityHandler.php +223 -20 1.30 → 2.2.0 View file →
@@ -3,8 +3,9 @@
3 3 namespace FluentBoards\App\Hooks\Handlers;
4 4
5 5 use FluentBoards\App\Models\Board;
6 6 use FluentBoards\App\Models\Stage;
7 +use FluentBoards\App\Models\Task;
7 8 use FluentBoards\App\Models\User;
8 9 use FluentBoards\App\Services\Constant;
9 10 use FluentCrm\App\Models\Subscriber;
10 11 use FluentBoards\App\Models\Comment;
@@ -23,8 +24,13 @@
23 24 'new_value' => $newValue,
24 25 'description' => $description,
25 26 'settings' => $settings
26 27 ];
28 + $userId = get_current_user_id();
29 + if($userId == 0){
30 + $task = Task::find($taskId);
31 + $data['created_by'] = $task->created_by;
32 + }
27 33
28 34 Helper::createActivity($data);
29 35 }
30 36 public function logMoveTaskToAnotherBoardActivity($task, $oldTask)
@@ -35,10 +41,18 @@
35 41 }
36 42
37 43 public function logAssigneeAddedActivity($task, $newAssigneeId)
38 44 {
45 + $currentUserId = get_current_user_id();
46 + if($newAssigneeId == $currentUserId) {
47 + $this->taskAssigneeJoin($task->id);
48 + return;
49 + }
39 50 $user = User::findOrFail($newAssigneeId);
40 - if($user) {
51 + $currentUserId = get_current_user_id();
52 + if($currentUserId === $user->ID){
53 + $this->taskAssigneeJoin($task->id);
54 + } elseif ($user) {
41 55 $this->createLogActivity($task->id, 'added', 'assignee', null, $user->display_name);
42 56 }
43 57 }
44 58
@@ -43,10 +57,18 @@
43 57 }
44 58
45 59 public function logAssigneeRemovedActivity($task, $newAssigneeId)
46 60 {
61 + $currentUserId = get_current_user_id();
62 + if($newAssigneeId == $currentUserId) {
63 + $this->taskAssigneeLeave($task->id);
64 + return;
65 + }
47 66 $user = User::findOrFail($newAssigneeId);
48 - if($user) {
67 + $currentUserId = get_current_user_id();
68 + if($currentUserId === $user->ID){
69 + $this->taskAssigneeLeave($task->id);
70 + } else if($user) {
49 71 $this->createLogActivity($task->id, 'removed', 'assignee', null, $user->display_name);
50 72 }
51 73 }
52 74
@@ -56,17 +78,18 @@
56 78 }
57 79
58 80 public function logTaskContentUpdatedActivity($task, $col, $oldTask = null)
59 81 {
82 + // Description autosaves are intentionally excluded to avoid flooding the activity feed.
83 + if ($col === 'description') {
84 + return;
85 + }
86 +
60 87 $action = 'updated';
61 - if($col == 'description'){
62 - $this->createLogActivity( $task->id, $action, $col, $oldTask->description, $task->description );
88 + if($task->parent_id){
89 + $this->createLogActivity( $task->parent_id, $action, 'subtask', $oldTask->title, $task->title );
63 90 }else{
64 - if($task->parent_id){
65 - $this->createLogActivity( $task->parent_id, $action, 'subtask', $oldTask->title, $task->title );
66 - }else{
67 - $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title );
68 - }
91 + $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title );
69 92 }
70 93 }
71 94
72 95 public function taskLabelActivity($task, $label, $action)
@@ -111,8 +134,12 @@
111 134 }
112 135
113 136 $this->createLogActivity($task->id, $action, $column, $old, $new);
114 137 }
138 + public function logDueDateRemoveActivity($task)
139 + {
140 + $this->createLogActivity( $task->id, 'removed', 'Due Date', null);
141 + }
115 142
116 143 public function logStartDateActivity($task, $oldDate)
117 144 {
118 145 $column = 'Start Date';
@@ -153,9 +180,9 @@
153 180 $parent = Comment::findOrFail($comment->parent_id);
154 181 $parentDescription = $parent->description;
155 182 $this->createLogActivity($comment->task_id, 'added', 'a reply');
156 183 }else{
157 - $commentPlainText = strip_tags($comment->description);
184 + $commentPlainText = wp_strip_all_tags($comment->description);
158 185 $this->createLogActivity($comment->task_id, 'added', 'comment', null, $commentPlainText, null);
159 186 }
160 187 }
161 188 public function logCommentUpdateActivity($comment, $oldComment)
@@ -162,13 +189,19 @@
162 189 {
163 190 $taskId = $comment->task_id;
164 191 $newComment = $comment->settings['raw_description'] ?? $comment->description;
165 192
166 - $this->createLogActivity($taskId, 'updated', 'comment', $oldComment, $newComment);
193 + // Activity previews are plain text, while comment drafts now contain inline HTML.
194 + $plainText = static function ($content) {
195 + $content = preg_replace('~<br\s*/?>|</p\s*>~i', "\n", $content);
196 + return trim(html_entity_decode(wp_strip_all_tags($content), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
197 + };
198 +
199 + $this->createLogActivity($taskId, 'updated', 'comment', $plainText($oldComment), $plainText($newComment));
167 200 }
168 201 public function logCommentDeleteActivity($comment)
169 202 {
170 - $commentDescription = strip_tags($comment->settings['raw_description'] ?? $comment->description );
203 + $commentDescription = wp_strip_all_tags($comment->settings['raw_description'] ?? $comment->description );
171 204 $taskId = $comment->task_id;
172 205
173 206 $this->createLogActivity($taskId, 'removed', 'comment', null, $commentDescription, null);
174 207 }
@@ -177,20 +210,37 @@
177 210 {
178 211 $this->createLogActivity($parentTask->id, 'added', 'subtask', null, $subTask->title);
179 212 }
180 213
214 + public function logSubtaskCloneActivity($parentTask, $subTask)
215 + {
216 + $this->createLogActivity($parentTask->id, 'cloned', 'subtask', null, $subTask->title);
217 + }
218 + public function logSubtaskGroupAddedActivity($task_id, $subTaskGroup)
219 + {
220 + $this->createLogActivity($task_id, 'added', 'subtask group', null, $subTaskGroup->value);
221 + }
222 +
181 223 public function logSubtaskDeletedActivity($id, $subTaskTitle)
182 224 {
183 225 $this->createLogActivity($id, 'removed', 'subtask', null, $subTaskTitle);
184 226 }
185 227
228 + public function logSubtaskGroupDeletedActivity($task_id, $subTaskGroup)
229 + {
230 + $this->createLogActivity($task_id, 'removed', 'subtask group', null, $subTaskGroup->value);
231 + }
232 + public function logSubtaskGroupTitleUpdatedActivity($oldTitle, $group)
233 + {
234 + $this->createLogActivity($group->task_id, 'changed', 'subtask group title', $oldTitle, $group->value);
235 + }
186 236 public function logTaskCompletedOrReopenActivity($task, $status)
187 237 {
188 - if($status == 'completed'){
238 + if($status == 'closed'){
189 239 if($task->parent_id){
190 - $this->createLogActivity($task->parent_id, 'completed', 'subtask', $task->title);
240 + $this->createLogActivity($task->parent_id, 'closed', 'subtask', $task->title);
191 241 }else{
192 - $this->createLogActivity($task->id, 'completed', 'task', $task->title);
242 + $this->createLogActivity($task->id, 'closed', 'task', $task->title);
193 243 }
194 244 }else{
195 245 if($task->parent_id){
196 246 $this->createLogActivity($task->parent_id, 'reopened', 'subtask', $task->title);
@@ -199,15 +249,21 @@
199 249 }
200 250 }
201 251 }
202 252
203 - public function logTaskStageUpdatedActivity($task, $oldStageId)
253 + public function logTaskStageUpdatedActivity($task, $oldStageId, $stageContext = null)
204 254 {
205 - $oldStage = Stage::findOrFail($oldStageId);
206 - $newStage = Stage::findOrFail($task->stage_id);
207 - if($oldStage && $newStage){
208 - $this->createLogActivity($task->id, 'changed', 'stage', $oldStage->title, $newStage->title);
255 + $oldStage = $stageContext['source'] ?? null;
256 + $newStage = $stageContext['target'] ?? null;
257 +
258 + if (!$oldStage instanceof Stage || !$newStage instanceof Stage) {
259 + $oldStage = Stage::find($oldStageId);
260 + $newStage = Stage::findOrFail($task->stage_id);
209 261 }
262 +
263 + if ($newStage) {
264 + $this->createLogActivity($task->id, 'changed', 'stage', $oldStage ? $oldStage->title : null, $newStage->title);
265 + }
210 266 }
211 267
212 268 public function associateUserAddChangeRemoveActivity($oldAssociateId, $newAssociateId, $taskId)
213 269 {
@@ -304,6 +360,153 @@
304 360 }
305 361 public function taskAttachmentDeleted($attachment)
306 362 {
307 363 $this->createLogActivity($attachment->object_id , 'deleted', 'attachment', null, strlen($attachment->title) > 0 ? $attachment->title : $attachment->full_url);
364 + }
365 +
366 + public function taskArchived($task)
367 + {
368 + if(!$task->archived_at){
369 + $this->createLogActivity($task->id, 'restored', 'task', $task->title);
370 + }else{
371 + $this->createLogActivity($task->id, 'archived', 'task', $task->title);
372 + }
373 + }
374 +
375 + /** Record both recurrence links, retaining the full generated title in the source entry's settings. */
376 + public function logRepeatTaskCreatedActivity($newTask, $parentTask)
377 + {
378 + $this->createLogActivity(
379 + $newTask->id,
380 + 'created',
381 + 'repeat task',
382 + $newTask->title,
383 + $parentTask->title,
384 + null,
385 + $parentTask
386 + );
387 +
388 + // new_value is VARCHAR(50); keep a Unicode-safe preview and render settings.title.
389 + $this->createLogActivity(
390 + $parentTask->id,
391 + 'created',
392 + 'repeat task',
393 + null,
394 + mb_substr($newTask->title, 0, 50, 'UTF-8'),
395 + null,
396 + [
397 + 'id' => $newTask->id,
398 + 'board_id' => $newTask->board_id,
399 + 'title' => $newTask->title,
400 + ]
401 + );
402 + }
403 +
404 + public function logRepeatTaskSet($newTask)
405 + {
406 + $this->createLogActivity(
407 + $newTask->id,
408 + 'set',
409 + 'Repeat Task',
410 + null,
411 + null,
412 + );
413 + }
414 +
415 + public function logRepeatTaskUpdated($newTask)
416 + {
417 + $this->createLogActivity(
418 + $newTask->id,
419 + 'updated',
420 + 'Repeat Task',
421 + null,
422 + null
423 + );
424 + }
425 + public function taskMovedFromBoard($task, $oldBoard, $newBoard)
426 + {
427 + $this->createLogActivity($task->id, 'moved', 'board', $oldBoard->title, $newBoard->title);
428 + }
429 +
430 + /** Log meaningful task value additions, changes and removals, preserving zero and false. */
431 + public function logCustomFieldActivity($taskId, $customField, $oldValue, $newValue, $isNewCustomField)
432 + {
433 + $fieldType = $customField->settings['custom_field_type'];
434 + $oldValue = $this->normalizeCustomFieldActivityValue($isNewCustomField ? null : $oldValue, $fieldType);
435 + $newValue = $this->normalizeCustomFieldActivityValue($newValue, $fieldType);
436 +
437 + if ($oldValue === $newValue) {
438 + return;
439 + }
440 +
441 + $action = Constant::ACTIVITY_ACTION_CHANGED;
442 + if ($oldValue === null) {
443 + $action = Constant::ACTIVITY_ACTION_ADDED;
444 + } elseif ($newValue === null) {
445 + $action = Constant::ACTIVITY_ACTION_REMOVED;
446 + }
447 +
448 + $settings = [
449 + 'custom_field_id' => $customField->id,
450 + 'custom_field_title' => $customField->title,
451 + 'custom_field_type' => $fieldType
452 + ];
453 +
454 + $this->createLogActivity(
455 + $taskId,
456 + $action,
457 + /* translators: %s is the custom field title */
458 + sprintf(__('custom field %s', 'fluent-boards'), $customField->title),
459 + $this->formatValueForDisplay($oldValue, $fieldType),
460 + $this->formatValueForDisplay($newValue, $fieldType),
461 + null,
462 + $settings
463 + );
464 + }
465 +
466 + /** Compare logical values independently of transport types and selection order. */
467 + private function normalizeCustomFieldActivityValue($value, $fieldType)
468 + {
469 + if ($value === null || $value === '' || $value === []) {
470 + return null;
471 + }
472 + if ($fieldType === 'checkbox') {
473 + return rest_sanitize_boolean($value);
474 + }
475 + if ($fieldType === 'multi-select') {
476 + $value = array_values(array_unique(array_map('strval', (array) $value)));
477 + sort($value, SORT_STRING);
478 + return $value;
479 + }
480 + if ($fieldType === 'number' && is_numeric($value)) {
481 + return (string) (0 + $value);
482 + }
483 + return (string) $value;
484 + }
485 +
486 + /** Format custom-field values without treating zero or an unchecked box as empty. */
487 + private function formatValueForDisplay($value, $fieldType)
488 + {
489 + if ($value === null) {
490 + return null;
491 + }
492 + if ($fieldType === 'checkbox') {
493 + return $value ? __('checked', 'fluent-boards') : __('unchecked', 'fluent-boards');
494 + }
495 + if ($fieldType === 'multi-select') {
496 + return implode(', ', $value);
497 + }
498 + if ($fieldType === 'date' && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value)) {
499 + return mysql2date('M j, Y g:i A', $value);
500 + }
501 + return (string) $value;
502 + }
503 + public function taskCloned($originalTask, $clonedTask)
504 + {
505 + $this->createLogActivity(
506 + $clonedTask->id,
507 + 'cloned',
508 + 'task',
509 + 'from ' . $originalTask->title,
510 + );
308 511 }
309 512 }