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 +199 -18 1.40 → 2.2.0 View file →
@@ -78,17 +78,18 @@
78 78 }
79 79
80 80 public function logTaskContentUpdatedActivity($task, $col, $oldTask = null)
81 81 {
82 + // Description autosaves are intentionally excluded to avoid flooding the activity feed.
83 + if ($col === 'description') {
84 + return;
85 + }
86 +
82 87 $action = 'updated';
83 - if($col == 'description'){
84 - $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 );
85 90 }else{
86 - if($task->parent_id){
87 - $this->createLogActivity( $task->parent_id, $action, 'subtask', $oldTask->title, $task->title );
88 - }else{
89 - $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title );
90 - }
91 + $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title );
91 92 }
92 93 }
93 94
94 95 public function taskLabelActivity($task, $label, $action)
@@ -133,8 +134,12 @@
133 134 }
134 135
135 136 $this->createLogActivity($task->id, $action, $column, $old, $new);
136 137 }
138 + public function logDueDateRemoveActivity($task)
139 + {
140 + $this->createLogActivity( $task->id, 'removed', 'Due Date', null);
141 + }
137 142
138 143 public function logStartDateActivity($task, $oldDate)
139 144 {
140 145 $column = 'Start Date';
@@ -175,9 +180,9 @@
175 180 $parent = Comment::findOrFail($comment->parent_id);
176 181 $parentDescription = $parent->description;
177 182 $this->createLogActivity($comment->task_id, 'added', 'a reply');
178 183 }else{
179 - $commentPlainText = strip_tags($comment->description);
184 + $commentPlainText = wp_strip_all_tags($comment->description);
180 185 $this->createLogActivity($comment->task_id, 'added', 'comment', null, $commentPlainText, null);
181 186 }
182 187 }
183 188 public function logCommentUpdateActivity($comment, $oldComment)
@@ -184,13 +189,19 @@
184 189 {
185 190 $taskId = $comment->task_id;
186 191 $newComment = $comment->settings['raw_description'] ?? $comment->description;
187 192
188 - $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));
189 200 }
190 201 public function logCommentDeleteActivity($comment)
191 202 {
192 - $commentDescription = strip_tags($comment->settings['raw_description'] ?? $comment->description );
203 + $commentDescription = wp_strip_all_tags($comment->settings['raw_description'] ?? $comment->description );
193 204 $taskId = $comment->task_id;
194 205
195 206 $this->createLogActivity($taskId, 'removed', 'comment', null, $commentDescription, null);
196 207 }
@@ -199,20 +210,37 @@
199 210 {
200 211 $this->createLogActivity($parentTask->id, 'added', 'subtask', null, $subTask->title);
201 212 }
202 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 +
203 223 public function logSubtaskDeletedActivity($id, $subTaskTitle)
204 224 {
205 225 $this->createLogActivity($id, 'removed', 'subtask', null, $subTaskTitle);
206 226 }
207 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 + }
208 236 public function logTaskCompletedOrReopenActivity($task, $status)
209 237 {
210 - if($status == 'completed'){
238 + if($status == 'closed'){
211 239 if($task->parent_id){
212 - $this->createLogActivity($task->parent_id, 'completed', 'subtask', $task->title);
240 + $this->createLogActivity($task->parent_id, 'closed', 'subtask', $task->title);
213 241 }else{
214 - $this->createLogActivity($task->id, 'completed', 'task', $task->title);
242 + $this->createLogActivity($task->id, 'closed', 'task', $task->title);
215 243 }
216 244 }else{
217 245 if($task->parent_id){
218 246 $this->createLogActivity($task->parent_id, 'reopened', 'subtask', $task->title);
@@ -221,15 +249,21 @@
221 249 }
222 250 }
223 251 }
224 252
225 - public function logTaskStageUpdatedActivity($task, $oldStageId)
253 + public function logTaskStageUpdatedActivity($task, $oldStageId, $stageContext = null)
226 254 {
227 - $oldStage = Stage::findOrFail($oldStageId);
228 - $newStage = Stage::findOrFail($task->stage_id);
229 - if($oldStage && $newStage){
230 - $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);
231 261 }
262 +
263 + if ($newStage) {
264 + $this->createLogActivity($task->id, 'changed', 'stage', $oldStage ? $oldStage->title : null, $newStage->title);
265 + }
232 266 }
233 267
234 268 public function associateUserAddChangeRemoveActivity($oldAssociateId, $newAssociateId, $taskId)
235 269 {
@@ -326,6 +360,153 @@
326 360 }
327 361 public function taskAttachmentDeleted($attachment)
328 362 {
329 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 + );
330 511 }
331 512 }