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