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