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/Services/Intergrations/FluentCRM/Automations/TaskCreateAction.php +239 -33 1.21 → 2.2.0 View file →
@@ -2,13 +2,17 @@
2 2
3 3 namespace FluentBoards\App\Services\Intergrations\FluentCRM\Automations;
4 4
5 5 use FluentBoards\App\Models\Task;
6 +use FluentBoards\App\Models\Stage;
6 7 use FluentBoards\App\Services\TaskService;
8 +use FluentBoards\App\Services\PermissionManager;
9 +use FluentCrm\App\Models\Funnel;
7 10 use FluentCrm\App\Services\Funnel\BaseAction;
8 11 use FluentCrm\App\Services\Funnel\FunnelHelper;
9 12 use FluentCrm\Framework\Support\Arr;
10 13 use FluentBoards\App\Services\Helper;
14 +use FluentBoardsPro\App\Services\ProTaskService;
11 15
12 16 class TaskCreateAction extends BaseAction
13 17 {
14 18 public function __construct()
@@ -26,9 +30,12 @@
26 30 'description' => __('Create Task to the selected Board', 'fluent-boards'),
27 31 'icon' => 'fc-icon-apply_list',
28 32 'settings' => [
29 33 'stage' => [],
30 - 'title' => 'Task from automation of {{contact.email}}'
34 + 'create_task_type' => 'new',
35 + 'title' => 'Task from automation of {{contact.email}}',
36 + 'created_by' => get_current_user_id(),
37 + 'assignees' => [],
31 38 ]
32 39 ];
33 40 }
34 41
@@ -44,19 +51,88 @@
44 51 'label' => __('Select Board & It\'s stage', 'fluent-boards'),
45 52 'placeholder' => __('Select Board & It\'s stage', 'fluent-boards'),
46 53 'options' => Helper::getStagesByBoardGroup()
47 54 ],
55 +
56 + 'created_by' => [
57 + 'type' => 'rest_selector',
58 + 'option_key' => 'board_members',
59 + 'is_multiple' => false,
60 + 'label' => __('Created by', 'fluent-boards'),
61 + 'placeholder' => __('Select a task creator', 'fluent-boards'),
62 + ],
63 +
64 + 'assignees' => [
65 + 'type' => 'rest_selector',
66 + 'option_key' => 'board_members',
67 + 'is_multiple' => true,
68 + 'label' => __('Assign to', 'fluent-boards'),
69 + 'placeholder' => __('Select task assignees', 'fluent-boards'),
70 + ],
71 +
72 + 'create_task_type' => [
73 + 'type' => 'radio',
74 + 'wrapper_class' => 'fc_half_field',
75 + 'label' => __('Task Create Type', 'fluent-boards'),
76 + 'options' => [
77 + [
78 + 'id' => 'new',
79 + 'title' => __('Create from stratch', 'fluent-boards')
80 + ],
81 + [
82 + 'id' => 'template',
83 + 'title' => __('Create from template', 'fluent-boards')
84 + ]
85 + ]
86 + ],
87 +
88 + 'task_template' => [
89 + 'type' => 'rest_selector',
90 + 'option_key' => 'task_templates',
91 + 'is_multiple' => false,
92 + 'label' => __('Select Task Template', 'fluent-boards'),
93 + 'placeholder' => __('Select Template', 'fluent-boards'),
94 + 'dependency' => [
95 + 'depends_on' => 'create_task_type',
96 + 'operator' => '=',
97 + 'value' => 'template'
98 + ]
99 + ],
100 +
101 + 'title_template' => [
102 + 'type' => 'input-text-popper',
103 + 'field_type' => 'text',
104 + 'label' => __('Task Title', 'fluent-boards'),
105 + 'placeholder' => __('Task Title', 'fluent-boards'),
106 + 'inline_help' => __('Leaving it blank will make the title the same as the template task title.', 'fluent-boards'),
107 + 'dependency' => [
108 + 'depends_on' => 'create_task_type',
109 + 'operator' => '=',
110 + 'value' => 'template'
111 + ]
112 + ],
113 +
48 114 'title' => [
49 115 'type' => 'input-text-popper',
50 116 'field_type' => 'text',
51 117 'label' => __('Task Title', 'fluent-boards'),
52 - 'placeholder' => __('Task Title', 'fluent-boards')
118 + 'placeholder' => __('Task Title', 'fluent-boards'),
119 + 'dependency' => [
120 + 'depends_on' => 'create_task_type',
121 + 'operator' => '=',
122 + 'value' => 'new'
123 + ]
53 124 ],
54 125
55 126 'due_day' => [
56 127 'label' => __('Due Date', 'fluent-boards'),
57 128 'type' => 'input-number',
58 - 'inline_help' => __('Set to your_input days after task creation, values less than zero will set due date to null', 'fluent-boards')
129 + 'inline_help' => __('Set to your_input days after task creation, values less than zero will set due date to null', 'fluent-boards'),
130 + 'dependency' => [
131 + 'depends_on' => 'create_task_type',
132 + 'operator' => '=',
133 + 'value' => 'new'
134 + ]
59 135 ],
60 136
61 137 'description' => [
62 138 'type' => 'html_editor',
@@ -61,9 +137,14 @@
61 137 'description' => [
62 138 'type' => 'html_editor',
63 139 'smart_codes' => 'yes',
64 140 'context_codes' => 'yes',
65 - 'label' => __('Description', 'fluent-boards')
141 + 'label' => __('Description', 'fluent-boards'),
142 + 'dependency' => [
143 + 'depends_on' => 'create_task_type',
144 + 'operator' => '=',
145 + 'value' => 'new'
146 + ]
66 147 ],
67 148
68 149 'priority' => [
69 150 'type' => 'select',
@@ -68,58 +149,183 @@
68 149 'priority' => [
69 150 'type' => 'select',
70 151 'label' => __('Select Priority', 'fluent-boards'),
71 152 'options' => Helper::getPriorityOptions(),
72 - 'inline_help' => __('Keeping it blank will select priority to low', 'fluent-boards')
153 + 'inline_help' => __('Keeping it blank will create the task with no priority', 'fluent-boards'),
154 + 'dependency' => [
155 + 'depends_on' => 'create_task_type',
156 + 'operator' => '=',
157 + 'value' => 'new'
158 + ]
73 159 ]
74 160 ]
75 161 ];
76 162 }
77 163
164 + /** Create a task for the triggering contact, using the shared template copier. */
78 165 public function handle( $subscriber, $sequence, $funnelSubscriberId, $funnelMetric )
79 166 {
80 167 $data = $sequence->settings;
81 168
169 + $createType = Arr::get($data, 'create_task_type');
82 170 $stage = Arr::get($data, 'stage'); // this is a string of the pipeline stage id
83 - $title = Arr::get( $data, 'title');
84 - $priority = Arr::get( $data, 'priority');
85 - $due_day = Arr::get( $data, 'due_day');
171 + $creatorId = $this->resolveCreatorId($data, $sequence);
172 + $configuredAssignees = Arr::get($data, 'assignees', []);
173 + $hasConfiguredAssignees = !empty($configuredAssignees);
174 + $assigneeIds = $this->resolveAssigneeIds($configuredAssignees);
86 175
87 - if(isset($due_day)) {
88 - $due_date = Helper::dueDateConversion($due_day, 'day');
89 - }
90 -
91 - $description = Arr::get( $data, 'description');
92 -
93 176 if ( empty($stage) ) {
94 177 FunnelHelper::changeFunnelSubSequenceStatus( $funnelSubscriberId, $sequence->id, 'skipped' );
95 178 return;
96 179 }
97 - $stageId = intval( $stage );
98 - $board = Helper::getBoardByStageId( $stageId );
180 +
181 + $stageId = (int) $stage;
182 + if ($stageId <= 0) {
183 + return;
184 + }
99 185
100 - if ( !$stage ) {
186 + $destinationStage = Stage::find($stageId);
187 + $board = $destinationStage ? $destinationStage->board : null;
188 +
189 + if (!$board) {
190 + FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
101 191 return;
102 192 }
103 193
104 - $description = apply_filters('fluent_crm/parse_campaign_email_text', $description, $subscriber);
105 - $title = apply_filters('fluent_crm/parse_campaign_email_text', $title, $subscriber);
194 + if ($createType === 'template') {
195 + $templateTaskId = absint(Arr::get($data, 'task_template'));
196 + $templateTask = $templateTaskId ? Task::whereHas('taskMeta', function ($query) {
197 + $query->where('key', 'is_template')->where('value', 'yes');
198 + })->find($templateTaskId) : null;
106 199
107 - (new Task())->createTask([
108 - 'title' => $title,
109 - 'board_id' => $board->id,
110 - 'crm_contact_id' => $subscriber->id,
111 - 'type' => 'task',
112 - 'status' => 'open',
113 - 'stage_id' => $stageId,
114 - 'source' => 'funnel',
115 - 'description' => $description,
116 - 'priority' => $priority,
117 - 'due_at' => $due_date ?? null,
118 - 'position' => (new TaskService())->getLastPositionOfTasks($stageId),
119 - ]);
200 + // Authorize the stored automation owner, not the configurable task creator or runner.
201 + $ownerId = Funnel::where('id', absint($sequence->funnel_id))->value('created_by');
202 + $ownerId = $this->getExistingUserIds([$ownerId])[0] ?? 0;
203 + if (!$templateTask || !$ownerId
204 + || !PermissionManager::userHasPermission($templateTask->board_id, $ownerId)
205 + || !PermissionManager::userHasPermission($board->id, $ownerId)) {
206 + FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
207 + return;
208 + }
120 209
210 + $title = Arr::get($data, 'title_template');
211 + if ($title === null || trim($title) === '') {
212 + $title = $templateTask->title;
213 + }
214 + $title = apply_filters('fluent_crm/parse_campaign_email_text', $title, $subscriber);
215 +
216 + $context = [
217 + 'created_by' => $creatorId,
218 + 'crm_contact_id' => absint($subscriber->id),
219 + 'source' => 'funnel',
220 + ];
221 + if ($hasConfiguredAssignees) {
222 + $context['assignees'] = $assigneeIds;
223 + } elseif ((int) $templateTask->board_id !== (int) $board->id) {
224 + // Preserve inherited assignments only for users who can access the destination.
225 + $context['assignees'] = PermissionManager::getUserIdsWithBoardAccess(
226 + $board->id,
227 + $templateTask->assignees()->get()->pluck('ID')->toArray()
228 + );
229 + }
230 +
231 + $taskData = [
232 + 'title' => sanitize_text_field($title),
233 + 'board_id' => $board->id,
234 + 'stage_id' => $stageId,
235 + 'assignee' => !$hasConfiguredAssignees,
236 + 'label' => true,
237 + 'subtask' => true,
238 + 'custom_field' => true,
239 + ];
240 +
241 + // Free and Pro update independently; old Pro must not discard context or skip tasks.
242 + if (class_exists(ProTaskService::class)
243 + && method_exists(ProTaskService::class, 'createFromTemplateForAutomation')) {
244 + (new ProTaskService())->createFromTemplateForAutomation($templateTaskId, $taskData, $context);
245 + } else {
246 + (new TaskService())->createAutomationTaskFromLegacyTemplate($templateTask, $taskData, $context);
247 + }
248 + } else {
249 + $title = Arr::get( $data, 'title');
250 + $priority = Arr::get( $data, 'priority');
251 + $due_day = Arr::get( $data, 'due_day');
252 +
253 + if(isset($due_day)) {
254 + $due_date = Helper::dueDateConversion($due_day, 'day');
255 + }
256 +
257 + $description = Arr::get( $data, 'description');
258 +
259 + $description = apply_filters('fluent_crm/parse_campaign_email_text', $description, $subscriber);
260 + $title = apply_filters('fluent_crm/parse_campaign_email_text', $title, $subscriber);
261 +
262 + (new Task())->createTask([
263 + 'title' => $title,
264 + 'board_id' => $board->id,
265 + 'crm_contact_id' => $subscriber->id,
266 + 'type' => 'task',
267 + 'status' => 'open',
268 + 'stage_id' => $stageId,
269 + 'source' => 'funnel',
270 + 'description' => $description,
271 + 'priority' => $priority ?? '',
272 + 'due_at' => $due_date ?? null,
273 + 'position' => (new TaskService())->getLastPositionOfTasks($stageId),
274 + 'created_by' => $creatorId,
275 + 'assignees' => $assigneeIds,
276 + ]);
277 + }
278 +
121 279 FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'completed');
122 280
123 281 }
124 282
125 -}
283 + /**
284 + * Resolve a valid WordPress user to own tasks created by the automation.
285 + */
286 + private function resolveCreatorId($data, $sequence): int
287 + {
288 + $candidateIds = [
289 + Arr::get($data, 'created_by'),
290 + $sequence->created_by ?? 0,
291 + get_current_user_id(),
292 + ];
293 + $existingIds = $this->getExistingUserIds($candidateIds);
294 +
295 + return $existingIds[0] ?? 0;
296 + }
297 +
298 + /**
299 + * Normalize configured task assignees to existing WordPress user IDs.
300 + */
301 + private function resolveAssigneeIds($assignees): array
302 + {
303 + return $this->getExistingUserIds((array) $assignees);
304 + }
305 +
306 + /**
307 + * Keep positive, unique IDs that still belong to WordPress users.
308 + */
309 + private function getExistingUserIds($userIds): array
310 + {
311 + $userIds = array_values(array_unique(array_filter(array_map('absint', (array) $userIds))));
312 +
313 + if (!$userIds) {
314 + return [];
315 + }
316 +
317 + $users = get_users([
318 + 'include' => $userIds,
319 + 'fields' => 'ID',
320 + ]);
321 + $existingIds = array_map(function ($user) {
322 + return absint(is_object($user) ? $user->ID : $user);
323 + }, $users);
324 + $existingLookup = array_flip($existingIds);
325 +
326 + return array_values(array_filter($userIds, function ($userId) use ($existingLookup) {
327 + return isset($existingLookup[$userId]);
328 + }));
329 + }
330 +
331 +}