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/Models/Task.php +382 -47 1.11 → 2.2.0 View file →
@@ -5,18 +5,21 @@
5 5 use FluentBoards\App\Services\Constant;
6 6 use FluentBoards\App\Services\Helper;
7 7 use FluentBoards\App\Services\TaskService;
8 8 use FluentBoards\Framework\Database\Orm\Builder;
9 +use FluentBoardsPro\App\Models\TaskAttachment;
10 +use FluentBoards\App\Models\CustomField;
11 +use FluentBoardsPro\App\Services\Constant as ProConstant;
9 12 use FluentCrm\App\Models\Subscriber;
10 13
11 14 class Task extends Model
12 15 {
16 +
13 17 protected $table = 'fbs_tasks';
14 18
15 19 protected $guarded = ['id'];
16 20
17 - protected $fillable
18 - = [
21 + protected $fillable = [
19 22 'title',
20 23 'slug',
21 24 'board_id',
22 25 'parent_id',
@@ -42,17 +45,35 @@
42 45 'comments_count',
43 46 'created_by',
44 47 ];
45 48
46 - protected $appends = ['meta'];
49 + protected $appends = ['meta', 'repeat_task_meta', 'is_pinned'];
47 50
51 + protected $nullableTimestampAttributes = [
52 + 'due_at',
53 + 'started_at',
54 + 'last_completed_at',
55 + 'archived_at',
56 + 'remind_at',
57 + ];
58 +
59 + protected static $skipTaskCreatedEvent = false;
60 +
61 + public static function withoutTaskCreatedEvent($callback)
62 + {
63 + static::$skipTaskCreatedEvent = true;
64 + $result = $callback();
65 + static::$skipTaskCreatedEvent = false;
66 + return $result;
67 + }
68 +
48 69 public static function boot()
49 70 {
50 71 parent::boot();
51 72 static::creating(function ($model) {
73 + $board = Board::find($model->board_id);
52 74 $model->created_by = $model->created_by ?: get_current_user_id();
53 - $model->type = $model->type
54 - ?: 'task'; // default task type is task
75 + $model->type = $board->type === 'roadmap' ? 'roadmap' : 'task'; // default task type is task
55 76
56 77 if (empty($model->slug)) {
57 78 $model->slug = sanitize_title($model->title, 'idea-'.time());
58 79 }
@@ -63,22 +84,35 @@
63 84 'backgroundColor' => '',
64 85 ],
65 86 'subtask_count' => 0,
66 87 'attachment_count' => 0,
88 + 'subtask_completed_count' => 0,
67 89 ];
68 90 $model->position = $model->position
69 91 ?: (new TaskService())->getLastPositionOfTasks($model->stage_id);
70 92 });
71 93 static::created(function ($model) {
72 - if ( ! $model->parent_id) {
94 + if (!$model->parent_id && !static::$skipTaskCreatedEvent) {
73 95 do_action('fluent_boards/task_created', $model);
74 96 if ($model->crm_contact_id) {
75 97 do_action('fluent_boards/contact_added_to_task', $model);
76 98 }
99 + } else {
100 + self::adjustSubtaskCount($model->parent_id);
77 101 }
78 102 });
79 103
80 - /* global scope for task type which means only task_type = task will be fetched from everywhere in */
104 + static::updated(function ($model) {
105 + if ($model->isDirty('status') && $model->getOriginal('status') === 'open' && $model->status === 'closed') {
106 + // Use persisted attributes without stale stage/board relations from the move.
107 + $closedTask = $model->fresh();
108 + if ($closedTask && $closedTask->status === 'closed') {
109 + do_action('fluent_boards/task_closed', $closedTask);
110 + }
111 + }
112 + });
113 +
114 + /* global scope for task type which means only type = task will be fetched from everywhere in */
81 115 static::addGlobalScope('type', function (Builder $builder) {
82 116 $builder->where('type', '=', 'task')
83 117 ->orWhere('type', '=', 'roadmap');
84 118 });
@@ -116,9 +150,27 @@
116 150 return $query->where('status', 'open')
117 151 ->where('due_at', '>=', current_time('mysql'));
118 152 }
119 153
154 + /**
155 + * scope of getting open tasks due today
156 + *
157 + * @param $query \FluentBoards\Framework\Database\Query\Builder
158 + *
159 + * @return \FluentBoards\Framework\Database\Query\Builder
160 + */
161 + public function scopeDueToday($query)
162 + {
163 + $todayTimestamp = current_time('timestamp');
164 + $startOfToday = gmdate('Y-m-d 00:00:00', $todayTimestamp);
165 + $endOfToday = gmdate('Y-m-d 23:59:59', $todayTimestamp);
120 166
167 + return $query->whereNull('last_completed_at')
168 + ->where('status', 'open')
169 + ->whereBetween('due_at', [$startOfToday, $endOfToday]);
170 + }
171 +
172 +
121 173 public function setSettingsAttribute($settings)
122 174 {
123 175 $this->attributes['settings'] = \maybe_serialize($settings);
124 176 }
@@ -149,9 +201,11 @@
149 201 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
150 202 */
151 203 public function comments()
152 204 {
153 - return $this->hasMany(Comment::class, 'task_id', 'id');
205 + return $this->hasMany(Comment::class, 'task_id', 'id')
206 + ->where('type', 'comment')
207 + ->where('parent_id', null);
154 208 }
155 209
156 210 /**
157 211 * One2Many: Task has many notifications
@@ -179,8 +233,30 @@
179 233 {
180 234 return $this->belongsTo(Board::class, 'board_id', 'id');
181 235 }
182 236
237 + /**
238 + * Exclude tasks that belong to template boards.
239 + */
240 + public function scopeExcludeTemplateBoards($query)
241 + {
242 + return $query->whereHas('board', function ($boardQuery) {
243 + $boardQuery->excludeTemplates();
244 + });
245 + }
246 +
247 + /**
248 + * Limit tasks to active, non-template boards available in this install.
249 + */
250 + public function scopeOnActiveAvailableBoards($query)
251 + {
252 + return $query->whereHas('board', function ($boardQuery) {
253 + $boardQuery->whereNull('archived_at')
254 + ->excludeTemplates()
255 + ->availableInCurrentInstall();
256 + });
257 + }
258 +
183 259 public function assignees()
184 260 {
185 261 return $this->belongsToMany(
186 262 User::class,
@@ -207,9 +283,14 @@
207 283 }
208 284
209 285 public function attachments() //may not need in future
210 286 {
211 - return $this->hasMany(\FluentBoardsPro\App\Models\Attachment::class,
287 + if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
288 + return $this->hasMany(Attachment::class, 'id', 'id')
289 + ->where('id', 0);
290 + }
291 +
292 + return $this->hasMany(TaskAttachment::class,
212 293 'object_id', 'id')
213 294 ->where('object_type', Constant::OBJECT_TYPE_TASK);
214 295 }
215 296
@@ -225,8 +306,44 @@
225 306 Constant::OBJECT_TYPE_USER_TASK_WATCH)
226 307 ->withTimestamps();
227 308 }
228 309
310 + /**
311 + * Tasks that must finish before this task can start (this task is the successor/blocked).
312 + * In fbs_relations: object_id = predecessor, foreign_id = this task (successor).
313 + *
314 + * @return \FluentBoards\Framework\Database\Orm\Relations\BelongsToMany
315 + */
316 + public function predecessors()
317 + {
318 + return $this->belongsToMany(
319 + Task::class,
320 + 'fbs_relations',
321 + 'foreign_id',
322 + 'object_id'
323 + )->wherePivot('object_type', Constant::OBJECT_TYPE_TASK_DEPENDENCY)
324 + ->withPivot('settings')
325 + ->withTimestamps();
326 + }
327 +
328 + /**
329 + * Tasks that are waiting on this task to finish (this task is the predecessor/blocker).
330 + * In fbs_relations: object_id = this task (predecessor), foreign_id = successor.
331 + *
332 + * @return \FluentBoards\Framework\Database\Orm\Relations\BelongsToMany
333 + */
334 + public function successors()
335 + {
336 + return $this->belongsToMany(
337 + Task::class,
338 + 'fbs_relations',
339 + 'object_id',
340 + 'foreign_id'
341 + )->wherePivot('object_type', Constant::OBJECT_TYPE_TASK_DEPENDENCY)
342 + ->withPivot('settings')
343 + ->withTimestamps();
344 + }
345 +
229 346 public function parentTask($id)
230 347 {
231 348 return self::find($id);
232 349 }
@@ -240,10 +357,25 @@
240 357 {
241 358 return $this->hasMany(TaskMeta::class, 'task_id', 'id');
242 359 }
243 360
361 + public function getPopularCount()
362 + {
363 + $interactions = $this->taskMeta()->get()->pluck('value', 'key');
364 + if($interactions) {
365 + return ($interactions['upvote'] ?? 0) + ($interactions['comments_count'] ?? 0);
366 + } else {
367 + return 0;
368 + }
369 + }
370 +
371 + /** Use batch-loaded metadata when preparing task-list responses. */
244 372 public function getMetaAttribute()
245 373 {
374 + if ($this->relationLoaded('taskMeta')) {
375 + return $this->getRelation('taskMeta')->pluck('value', 'key');
376 + }
377 +
246 378 return $this->taskMeta()->get()->pluck('value', 'key');
247 379 }
248 380
249 381 public function stage()
@@ -279,10 +411,17 @@
279 411 }
280 412
281 413 public function createTask($data)
282 414 {
415 + $data = apply_filters('fluent_boards/before_task_create', $data);
416 +
417 + // Keep newly-created tasks unprioritized unless a priority is explicitly selected.
418 + if (!array_key_exists('priority', $data)) {
419 + $data['priority'] = '';
420 + }
421 +
283 422 $createdTask = Task::create($data);
284 -
423 +
285 424 if ( ! empty($data['assignees'])) {
286 425 $assignees = array_filter(array_map('intval', $data['assignees']));
287 426 if ($assignees) {
288 427 $assigneeData = array_fill_keys($assignees,
@@ -287,8 +426,15 @@
287 426 if ($assignees) {
288 427 $assigneeData = array_fill_keys($assignees,
289 428 ['object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE]);
290 429 $createdTask->assignees()->syncWithoutDetaching($assigneeData);
430 +
431 + // Add assignees as watchers
432 + foreach ($assignees as $assigneeId) {
433 + $createdTask->watchers()->syncWithoutDetaching([
434 + $assigneeId => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]
435 + ]);
436 + }
291 437 }
292 438 }
293 439
294 440 if ( ! empty($data['labels'])) {
@@ -322,12 +468,12 @@
322 468
323 469 return $operation;
324 470 }
325 471
326 - public function getArchivedAttribute()
327 - {
328 - return (bool) $this->attributes['is_archived'];
329 - }
472 + // public function getArchivedAttribute()
473 + // {
474 + // return (bool) $this->attributes['is_archived'];
475 + // }
330 476
331 477 public function setArchivedAttribute($value)
332 478 {
333 479 if (true === $value) {
@@ -393,8 +539,9 @@
393 539
394 540 return $default;
395 541 }
396 542
543 + /** Persist metadata and invalidate any snapshot loaded for serialization. */
397 544 public function updateMeta($key, $value)
398 545 {
399 546 $exist = TaskMeta::where('task_id', $this->id)->where('key', $key)
400 547 ->first();
@@ -409,11 +556,27 @@
409 556 'value' => $value,
410 557 ]);
411 558 }
412 559
560 + $this->unsetRelation('taskMeta');
561 +
413 562 return $exist;
414 563 }
415 564
565 + /**
566 + * Virtual attribute: pinned state from task meta (for API/frontend).
567 + * Tasks without is_pinned meta are treated as unpinned (0).
568 + * Uses the batch-loaded metadata relation when preparing task lists.
569 + *
570 + * @return int 1 if pinned, 0 otherwise
571 + */
572 + public function getIsPinnedAttribute()
573 + {
574 + $meta = $this->meta;
575 +
576 + return (int) ($meta[Constant::IS_TASK_PINNED] ?? 0);
577 + }
578 +
416 579 public function moveToNewPosition($newIndex)
417 580 {
418 581 $newIndex = (int) $newIndex;
419 582 if ($newIndex < 1) {
@@ -494,8 +657,116 @@
494 657
495 658 return $this;
496 659 }
497 660
661 + public function moveBetweenTasks($prevTaskId = null, $nextTaskId = null)
662 + {
663 + $prevTaskId = absint($prevTaskId);
664 + $nextTaskId = absint($nextTaskId);
665 + // Exclude the current task so cross-stage moves can reuse the same
666 + // neighbour lookup logic after the stage_id has already been reassigned.
667 + $taskQuery = $this->getTaskOrderingQuery();
668 +
669 + $prevTask = null;
670 + if ($prevTaskId) {
671 + $prevTask = (clone $taskQuery)
672 + ->where('id', $prevTaskId)
673 + ->first();
674 + }
675 +
676 + $nextTask = null;
677 + if ($nextTaskId) {
678 + $nextTask = (clone $taskQuery)
679 + ->where('id', $nextTaskId)
680 + ->first();
681 + }
682 +
683 + if (!$prevTask && !$nextTask) {
684 + $firstItem = (clone $taskQuery)
685 + ->orderBy('position', 'asc')
686 + ->first();
687 +
688 + if (!$firstItem) {
689 + $this->position = 1;
690 + $this->save();
691 +
692 + return $this;
693 + }
694 +
695 + if ($firstItem->position < 0.02) {
696 + self::reIndexTasksPositions($this->toArray());
697 +
698 + return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
699 + }
700 +
701 + $this->position = round($firstItem->position / 2, 2);
702 + $this->save();
703 +
704 + return $this;
705 + }
706 +
707 + if (!$prevTask && $nextTask) {
708 + // Insert before the first visible neighbour by splitting the leading gap.
709 + if ($nextTask->position < 0.02) {
710 + self::reIndexTasksPositions($this->toArray());
711 +
712 + return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
713 + }
714 +
715 + $this->position = round($nextTask->position / 2, 2);
716 + $this->save();
717 +
718 + return $this;
719 + }
720 +
721 + if ($prevTask && !$nextTask) {
722 + // Insert after the last visible neighbour without touching the rest
723 + // of the stage unless the sparse ordering needs a later reindex.
724 + $this->position = $prevTask->position + 1;
725 + $this->save();
726 +
727 + return $this;
728 + }
729 +
730 + if ($prevTask->position >= $nextTask->position) {
731 + self::reIndexTasksPositions($this->toArray());
732 +
733 + return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
734 + }
735 +
736 + // Middle inserts keep reordering cheap by taking the midpoint between
737 + // the two neighbour positions instead of renumbering the whole stage.
738 + $newPosition = ($prevTask->position + $nextTask->position) / 2;
739 +
740 + $exists = (clone $taskQuery)
741 + ->where('position', $newPosition)
742 + ->first();
743 +
744 + if ($exists) {
745 + self::reIndexTasksPositions($this->toArray());
746 +
747 + return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
748 + }
749 +
750 + $this->position = $newPosition;
751 + $this->save();
752 +
753 + return $this;
754 + }
755 +
756 + private function getTaskOrderingQuery()
757 + {
758 + if (isset($this->parent_id)) {
759 + return self::where('parent_id', $this->parent_id)
760 + ->whereNull('archived_at')
761 + ->where('id', '!=', $this->id);
762 + }
763 +
764 + return self::where('stage_id', $this->stage_id)
765 + ->whereNull('archived_at')
766 + ->where('id', '!=', $this->id);
767 + }
768 +
498 769 public static function reIndexTasksPositions($task)
499 770 {
500 771 if (isset($task['parent_id'])) {
501 772 $tasksQuery = self::where('parent_id', $task['parent_id']);
@@ -516,24 +787,25 @@
516 787 {
517 788 if ( ! $subTaskParentId) {
518 789 return;
519 790 }
520 - $parentTask = Task::findOrFail($subTaskParentId);
521 -
522 - if ($parentTask->settings
523 - && array_key_exists('subtask_count', $parentTask->settings)
524 - ) {
525 - $parentTaskSettings = $parentTask->settings;
526 - $parentTaskSettings['subtask_count']
527 - = $parentTaskSettings['subtask_count'] - 1;
528 - $parentTask->settings = $parentTaskSettings;
529 - $parentTask->save();
791 + $parentTask = Task::find($subTaskParentId);
792 + if( !$parentTask) {
793 + return;
530 794 }
795 + $subtasks = $parentTask->subtasks;
796 + $parentTaskSettings = $parentTask->settings;
797 + $parentTaskSettings['subtask_count'] = $subtasks->count();
798 + $parentTaskSettings['subtask_completed_count'] = $subtasks->filter(function ($subtask) {
799 + return $subtask->status === 'closed';
800 + })->count();
801 + $parentTask->settings = $parentTaskSettings;
802 + $parentTask->save();
531 803 }
532 804
533 805 public function close()
534 806 {
535 - if ($this->status == 'closed') {
807 + if ($this->status == 'closed' && $this->last_completed_at) {
536 808 return $this;
537 809 }
538 810
539 811 $this->status = 'closed';
@@ -538,22 +810,26 @@
538 810
539 811 $this->status = 'closed';
540 812 $this->last_completed_at = current_time('mysql');
541 813 $this->save();
542 -
814 + if ($this->parent_id) {
815 + self::adjustSubtaskCount($this->parent_id);
816 + }
543 817 return $this;
544 818 }
545 819
546 820 public function reopen()
547 821 {
548 - if ($this->status == 'open') {
822 + if ($this->status == 'open' && $this->last_completed_at == null) {
549 823 return $this;
550 824 }
551 825
552 826 $this->status = 'open';
553 - $this->last_completed_at = null;
827 + $this->last_completed_at = NULL;
554 828 $this->save();
555 -
829 + if ($this->parent_id) {
830 + self::adjustSubtaskCount($this->parent_id);
831 + }
556 832 return $this;
557 833 }
558 834
559 835 /*
@@ -565,15 +841,24 @@
565 841 */
566 842 public static function mappables()
567 843 {
568 844 return [
569 - 'title' => __('Title', 'fluent-boards'),
845 + 'task_title' => __('Task Title', 'fluent-boards'),
846 + 'slug' => __('Slug', 'fluent-boards'),
847 + 'board_title' => __('Board Title', 'fluent-boards'),
848 + 'status' => __('Status', 'fluent-boards'),
849 + 'type' => __('Type', 'fluent-boards'),
570 850 'description' => __('Description', 'fluent-boards'),
571 851 'priority' => __('Priority', 'fluent-boards'),
572 852 'due_at' => __('Due Date', 'fluent-boards'),
853 + 'started_at' => __('Start Date', 'fluent-boards'),
854 + 'archived_at' => __('Archive Date', 'fluent-boards'),
573 855 'stage' => __('Stage', 'fluent-boards'),
574 856 'board' => __('Board', 'fluent-boards'),
575 857 'source' => __('Source', 'fluent-boards'),
858 + 'position' => __('Position', 'fluent-boards'),
859 + 'subtasks' => __('Subtasks', 'fluent-boards'),
860 + 'completion' => __('Completion', 'fluent-boards'),
576 861 ];
577 862 }
578 863 public static function mappableFields()
579 864 {
@@ -581,27 +866,27 @@
581 866 'title' => [
582 867 'field' => __('Title', 'fluent-boards'),
583 868 'type' => 'text',
584 869 'rules' => 'required',
585 - 'description' => __('Title of the task', 'fluent-boards'),
870 + 'description' => __('Title of the task.', 'fluent-boards'),
586 871 ],
587 - 'stage_id' => [
872 + 'stage' => [
588 873 'field' => __('Stage', 'fluent-boards'),
589 - 'type' => 'int',
874 + 'type' => 'int|text',
590 875 'rules' => 'optional',
591 - 'description' => __('Stage of the task', 'fluent-boards'),
876 + 'description' => __('The stage of the task, which can be an ID, title, or slug. Example: 1 | "open" | "Open"', 'fluent-boards'),
592 877 ],
593 878 'parent_id' => [
594 879 'field' => __('Parent Task', 'fluent-boards'),
595 - 'type' => 'select',
880 + 'type' => 'int',
596 881 'rules' => 'optional',
597 - 'description' => __('Parent Task of the subtask', 'fluent-boards'),
882 + 'description' => __('Parent Task ID of the subtask. Example: 1', 'fluent-boards'),
598 883 ],
599 884 'status' => [
600 885 'field' => __('Status', 'fluent-boards'),
601 886 'type' => 'text',
602 887 'rules' => 'optional',
603 - 'description' => __('Status of the task (open | closed)', 'fluent-boards'),
888 + 'description' => __('The status of the task (open | closed). Example: "closed"', 'fluent-boards'),
604 889 ],
605 890 'description' => [
606 891 'field' => __('Description', 'fluent-boards'),
607 892 'type' => 'textarea',
@@ -611,45 +896,51 @@
611 896 'priority' => [
612 897 'field' => __('Priority', 'fluent-boards'),
613 898 'type' => 'text',
614 899 'rules' => 'optional',
615 - 'description' => __('Priority of the task (low | medium | high)', 'fluent-boards'),
900 + 'description' => __('Priority of the task (urgent | high | medium | low). Leave empty for no priority. Example: "medium"', 'fluent-boards'),
616 901 ],
617 902 'due_at' => [
618 903 'field' => __('Due Date', 'fluent-boards'),
619 904 'type' => 'date',
620 905 'rules' => 'optional',
621 - 'description' => __('Due Date of the task', 'fluent-boards'),
906 + 'description' => __('The due date of the task in the format YYYY-MM-DD hh:mm. Example: 2099-12-31 23:59:59', 'fluent-boards'),
622 907 ],
623 908 'started_at' => [
624 909 'field' => __('Start Date', 'fluent-boards'),
625 910 'type' => 'date',
626 911 'rules' => 'optional',
627 - 'description' => __('Start Date of the task', 'fluent-boards'),
912 + 'description' => __('The start date of the task in the format YYYY-MM-DD. Example: 2099-12-31', 'fluent-boards'),
628 913 ],
629 - 'source' => [
914 + 'source' => [
630 915 'field' => __('Source', 'fluent-boards'),
631 916 'type' => 'text',
632 917 'rules' => 'optional',
633 - 'description' => __('Source of the task', 'fluent-boards'),
918 + 'description' => __('The source of the task. Example: "jira"', 'fluent-boards'),
634 919 ],
920 + 'source_id' => [
921 + 'field' => __('Source Id', 'fluent-boards'),
922 + 'type' => 'text|int',
923 + 'rules' => 'optional',
924 + 'description' => __('The source Id of the task (if any). Example: "bcy664fh177"', 'fluent-boards'),
925 + ],
635 926 'crm_contact_id' => [
636 927 'field' => __('CRM Contact Id', 'fluent-boards'),
637 928 'type' => 'int',
638 929 'rules' => 'optional',
639 - 'description' => __('Associated CRM Contact Id', 'fluent-boards'),
930 + 'description' => __('The ID of the associated FluentCRM contact. Example: 6465', 'fluent-boards'),
640 931 ],
641 932 'contact_email' => [
642 933 'field' => __('CRM Contact Email', 'fluent-boards'),
643 934 'type' => 'text',
644 935 'rules' => 'optional',
645 - 'description' => __('Associated CRM Contact Email', 'fluent-boards'),
936 + 'description' => __('The email of the associated CRM contact. Example: "[email protected]"', 'fluent-boards'),
646 937 ],
647 938 'contact_first_name' => [
648 939 'field' => __('Contact First Name', 'fluent-boards'),
649 940 'type' => 'text',
650 941 'rules' => 'optional',
651 - 'description' => __('Associated CRM Contact First Name', 'fluent-boards'),
942 + 'description' => __('Associated CRM Contact First Name. Example: "John"', 'fluent-boards'),
652 943 ],
653 944 'contact_last_name' => [
654 945 'field' => __('Contact Last Name', 'fluent-boards'),
655 946 'type' => 'text',
@@ -659,17 +950,61 @@
659 950 'labels' => [
660 951 'field' => __('Labels', 'fluent-boards'),
661 952 'type' => 'text|int',
662 953 'rules' => 'optional',
663 - 'description' => __('Label Array of Id or title', 'fluent-boards'),
954 + 'description' => __('An array of label IDs or titles. Example: [1, "feature", 44]', 'fluent-boards'),
664 955 ],
665 956 'assignees' => [
666 957 'field' => __('Assignees', 'fluent-boards'),
667 958 'type' => 'text|int',
668 959 'rules' => 'optional',
669 - 'description' => __('WP UserId Array', 'fluent-boards'),
960 + 'description' => __('An array of WP User IDs. Example: [1,2,44]', 'fluent-boards'),
670 961 ]
671 962 ];
672 963
673 964 return $fields;
674 965 }
966 +
967 + public function customFields()
968 + {
969 + return $this->belongsToMany(
970 + CustomField::class,
971 + 'fbs_relations',
972 + 'object_id',
973 + 'foreign_id'
974 + )->withPivot('settings')
975 + ->wherePivot('object_type', ProConstant::TASK_CUSTOM_FIELD)
976 + ->withTimestamps();
977 + }
978 +
979 +
980 + public function repeatTaskMeta()
981 + {
982 + return $this->hasOne(Meta::class, 'object_id', 'id')->where('object_type', Constant::REPEAT_TASK_META);
983 + }
984 +
985 + /** Reuse batch-loaded repeat settings, including a known missing relation. */
986 + public function getRepeatTaskMetaAttribute()
987 + {
988 + if ($this->relationLoaded('repeatTaskMeta')) {
989 + return $this->getRelation('repeatTaskMeta');
990 + }
991 +
992 + return $this->repeatTaskMeta()->first();
993 + }
994 + public function taskCustomFields()
995 + {
996 + return $this->hasMany(Relation::class, 'object_id', 'id')
997 + ->where('object_type', Constant::TASK_CUSTOM_FIELD);
998 + }
999 +
1000 + public function subtasks()
1001 + {
1002 + return $this->hasMany(Task::class, 'parent_id', 'id');
1003 + }
1004 +
1005 + public function subtaskGroup()
1006 + {
1007 + return $this->hasMany(TaskMeta::class, 'task_id')->where('key', Constant::SUBTASK_GROUP_NAME);
1008 + }
1009 +
675 1010 }