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 +81 -41 2.0.12 → 2.2.0 View file →
@@ -189,9 +189,15 @@
189 189 {
190 190 $taskId = $comment->task_id;
191 191 $newComment = $comment->settings['raw_description'] ?? $comment->description;
192 192
193 - $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));
194 200 }
195 201 public function logCommentDeleteActivity($comment)
196 202 {
197 203 $commentDescription = wp_strip_all_tags($comment->settings['raw_description'] ?? $comment->description );
@@ -249,14 +255,14 @@
249 255 $oldStage = $stageContext['source'] ?? null;
250 256 $newStage = $stageContext['target'] ?? null;
251 257
252 258 if (!$oldStage instanceof Stage || !$newStage instanceof Stage) {
253 - $oldStage = Stage::findOrFail($oldStageId);
259 + $oldStage = Stage::find($oldStageId);
254 260 $newStage = Stage::findOrFail($task->stage_id);
255 261 }
256 262
257 - if($oldStage && $newStage){
258 - $this->createLogActivity($task->id, 'changed', 'stage', $oldStage->title, $newStage->title);
263 + if ($newStage) {
264 + $this->createLogActivity($task->id, 'changed', 'stage', $oldStage ? $oldStage->title : null, $newStage->title);
259 265 }
260 266 }
261 267
262 268 public function associateUserAddChangeRemoveActivity($oldAssociateId, $newAssociateId, $taskId)
@@ -365,8 +371,9 @@
365 371 $this->createLogActivity($task->id, 'archived', 'task', $task->title);
366 372 }
367 373 }
368 374
375 + /** Record both recurrence links, retaining the full generated title in the source entry's settings. */
369 376 public function logRepeatTaskCreatedActivity($newTask, $parentTask)
370 377 {
371 378 $this->createLogActivity(
372 379 $newTask->id,
@@ -376,8 +383,23 @@
376 383 $parentTask->title,
377 384 null,
378 385 $parentTask
379 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 + );
380 402 }
381 403
382 404 public function logRepeatTaskSet($newTask)
383 405 {
@@ -404,62 +426,80 @@
404 426 {
405 427 $this->createLogActivity($task->id, 'moved', 'board', $oldBoard->title, $newBoard->title);
406 428 }
407 429
430 + /** Log meaningful task value additions, changes and removals, preserving zero and false. */
408 431 public function logCustomFieldActivity($taskId, $customField, $oldValue, $newValue, $isNewCustomField)
409 432 {
410 - // Format values for display in activity
411 - $displayNewValue = $this->formatValueForDisplay($newValue, $customField->settings['custom_field_type']);
412 -
413 - $action = 'changed';
414 - $column = $customField->title;
415 -
416 - // Ensure we have a value to display
417 - if (empty($displayNewValue)) {
418 - $displayNewValue = 'empty';
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;
419 439 }
420 440
421 - // Handle old value display logic
422 - $displayOldValue = null;
423 - if (!$isNewCustomField && $oldValue !== null && $oldValue !== '') {
424 - // Format old value for display if it exists
425 - $displayOldValue = $this->formatValueForDisplay($oldValue, $customField->settings['custom_field_type']);
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;
426 446 }
427 447
428 448 $settings = [
429 449 'custom_field_id' => $customField->id,
430 450 'custom_field_title' => $customField->title,
431 - 'custom_field_type' => $customField->settings['custom_field_type']
451 + 'custom_field_type' => $fieldType
432 452 ];
433 453
434 - $this->createLogActivity($taskId, $action, $column, $displayOldValue, $displayNewValue, null, $settings);
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 + );
435 464 }
436 465
437 - /**
438 - * Format custom field value for display in activity logs
439 - */
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. */
440 487 private function formatValueForDisplay($value, $fieldType)
441 488 {
442 - if ($value === null || $value === '') {
443 - return 'empty';
489 + if ($value === null) {
490 + return null;
444 491 }
445 -
446 - switch ($fieldType) {
447 - case 'checkbox':
448 - return $value ? 'checked' : 'unchecked';
449 - case 'date':
450 - // If it's already formatted as Y-m-d H:i:s, convert to readable format
451 - if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value)) {
452 - return gmdate('M j, Y g:i A', strtotime($value));
453 - }
454 - return $value;
455 - case 'select':
456 - case 'text':
457 - case 'textarea':
458 - case 'number':
459 - default:
460 - return (string) $value;
492 + if ($fieldType === 'checkbox') {
493 + return $value ? __('checked', 'fluent-boards') : __('unchecked', 'fluent-boards');
461 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;
462 502 }
463 503 public function taskCloned($originalTask, $clonedTask)
464 504 {
465 505 $this->createLogActivity(