| @@ -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( |