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