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 +209 -18 1.35 → 2.2.0 View file →
@@ -41,8 +41,13 @@
41 41 }
42 42
43 43 public function logAssigneeAddedActivity($task, $newAssigneeId)
44 44 {
45 + $currentUserId = get_current_user_id();
46 + if($newAssigneeId == $currentUserId) {
47 + $this->taskAssigneeJoin($task->id);
48 + return;
49 + }
45 50 $user = User::findOrFail($newAssigneeId);
46 51 $currentUserId = get_current_user_id();
47 52 if($currentUserId === $user->ID){
48 53 $this->taskAssigneeJoin($task->id);
@@ -52,8 +57,13 @@
52 57 }
53 58
54 59 public function logAssigneeRemovedActivity($task, $newAssigneeId)
55 60 {
61 + $currentUserId = get_current_user_id();
62 + if($newAssigneeId == $currentUserId) {
63 + $this->taskAssigneeLeave($task->id);
64 + return;
65 + }
56 66 $user = User::findOrFail($newAssigneeId);
57 67 $currentUserId = get_current_user_id();
58 68 if($currentUserId === $user->ID){
59 69 $this->taskAssigneeLeave($task->id);
@@ -68,17 +78,18 @@
68 78 }
69 79
70 80 public function logTaskContentUpdatedActivity($task, $col, $oldTask = null)
71 81 {
82 + // Description autosaves are intentionally excluded to avoid flooding the activity feed.
83 + if ($col === 'description') {
84 + return;
85 + }
86 +
72 87 $action = 'updated';
73 - if($col == 'description'){
74 - $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 );
75 90 }else{
76 - if($task->parent_id){
77 - $this->createLogActivity( $task->parent_id, $action, 'subtask', $oldTask->title, $task->title );
78 - }else{
79 - $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title );
80 - }
91 + $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title );
81 92 }
82 93 }
83 94
84 95 public function taskLabelActivity($task, $label, $action)
@@ -123,8 +134,12 @@
123 134 }
124 135
125 136 $this->createLogActivity($task->id, $action, $column, $old, $new);
126 137 }
138 + public function logDueDateRemoveActivity($task)
139 + {
140 + $this->createLogActivity( $task->id, 'removed', 'Due Date', null);
141 + }
127 142
128 143 public function logStartDateActivity($task, $oldDate)
129 144 {
130 145 $column = 'Start Date';
@@ -165,9 +180,9 @@
165 180 $parent = Comment::findOrFail($comment->parent_id);
166 181 $parentDescription = $parent->description;
167 182 $this->createLogActivity($comment->task_id, 'added', 'a reply');
168 183 }else{
169 - $commentPlainText = strip_tags($comment->description);
184 + $commentPlainText = wp_strip_all_tags($comment->description);
170 185 $this->createLogActivity($comment->task_id, 'added', 'comment', null, $commentPlainText, null);
171 186 }
172 187 }
173 188 public function logCommentUpdateActivity($comment, $oldComment)
@@ -174,13 +189,19 @@
174 189 {
175 190 $taskId = $comment->task_id;
176 191 $newComment = $comment->settings['raw_description'] ?? $comment->description;
177 192
178 - $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));
179 200 }
180 201 public function logCommentDeleteActivity($comment)
181 202 {
182 - $commentDescription = strip_tags($comment->settings['raw_description'] ?? $comment->description );
203 + $commentDescription = wp_strip_all_tags($comment->settings['raw_description'] ?? $comment->description );
183 204 $taskId = $comment->task_id;
184 205
185 206 $this->createLogActivity($taskId, 'removed', 'comment', null, $commentDescription, null);
186 207 }
@@ -189,20 +210,37 @@
189 210 {
190 211 $this->createLogActivity($parentTask->id, 'added', 'subtask', null, $subTask->title);
191 212 }
192 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 +
193 223 public function logSubtaskDeletedActivity($id, $subTaskTitle)
194 224 {
195 225 $this->createLogActivity($id, 'removed', 'subtask', null, $subTaskTitle);
196 226 }
197 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 + }
198 236 public function logTaskCompletedOrReopenActivity($task, $status)
199 237 {
200 - if($status == 'completed'){
238 + if($status == 'closed'){
201 239 if($task->parent_id){
202 - $this->createLogActivity($task->parent_id, 'completed', 'subtask', $task->title);
240 + $this->createLogActivity($task->parent_id, 'closed', 'subtask', $task->title);
203 241 }else{
204 - $this->createLogActivity($task->id, 'completed', 'task', $task->title);
242 + $this->createLogActivity($task->id, 'closed', 'task', $task->title);
205 243 }
206 244 }else{
207 245 if($task->parent_id){
208 246 $this->createLogActivity($task->parent_id, 'reopened', 'subtask', $task->title);
@@ -211,15 +249,21 @@
211 249 }
212 250 }
213 251 }
214 252
215 - public function logTaskStageUpdatedActivity($task, $oldStageId)
253 + public function logTaskStageUpdatedActivity($task, $oldStageId, $stageContext = null)
216 254 {
217 - $oldStage = Stage::findOrFail($oldStageId);
218 - $newStage = Stage::findOrFail($task->stage_id);
219 - if($oldStage && $newStage){
220 - $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);
221 261 }
262 +
263 + if ($newStage) {
264 + $this->createLogActivity($task->id, 'changed', 'stage', $oldStage ? $oldStage->title : null, $newStage->title);
265 + }
222 266 }
223 267
224 268 public function associateUserAddChangeRemoveActivity($oldAssociateId, $newAssociateId, $taskId)
225 269 {
@@ -316,6 +360,153 @@
316 360 }
317 361 public function taskAttachmentDeleted($attachment)
318 362 {
319 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 + );
320 511 }
321 512 }