PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.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 1.41 All 42 releases
fluent-boards / app / Modules / MCP / Tools / LabelTools.php

LabelTools.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at app/Modules/MCP/Tools/LabelTools.php

314 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Modules\MCP\Tools;
4
5 use FluentBoards\App\Modules\MCP\Helpers\MCPHelper;
6 use FluentBoards\App\Services\Constant;
7 use FluentBoards\App\Services\Helper;
8 use FluentBoards\App\Services\LabelService;
9
10 /**
11 * Board label read/write MCP tools and task label assignment.
12 */
13 class LabelTools
14 {
15 const DEFAULT_BG_COLOR = '#f3f4f6';
16 const DEFAULT_TEXT_COLOR = '#1B2533';
17 const MAX_LABELS_PER_CALL = 50;
18
19 public static function listLabels($params = [])
20 {
21 $board = MCPHelper::resolveBoard($params);
22 if (is_wp_error($board)) {
23 return $board;
24 }
25
26 if (!MCPHelper::canReadBoard($board->id)) {
27 return MCPHelper::error('forbidden', __('You do not have access to this board', 'fluent-boards'));
28 }
29
30 $service = new LabelService();
31 $labels = !empty($params['only_used'])
32 ? $service->getLabelsByBoardUsedInTasks($board->id)
33 : $service->getLabelsByBoard($board->id);
34
35 return [
36 'board_id' => (int) $board->id,
37 'labels' => MCPHelper::formatLabelList($labels),
38 ];
39 }
40
41 /**
42 * Create a label, or update it when label_id is given. One tool so an agent shaping a
43 * board's taxonomy does not have to pick between two, mirroring save-stage.
44 */
45 public static function saveLabel($params = [])
46 {
47 $board = MCPHelper::resolveBoard($params);
48 if (is_wp_error($board)) {
49 return $board;
50 }
51
52 if (!MCPHelper::canWriteBoard($board->id)) {
53 return MCPHelper::error('forbidden', __('You do not have permission to manage labels on this board', 'fluent-boards'));
54 }
55
56 $service = new LabelService();
57 $isUpdate = !empty($params['label_id']);
58 $existing = null;
59
60 if ($isUpdate) {
61 $existing = self::findLabel($service, $params['label_id'], $board->id);
62 if (is_wp_error($existing)) {
63 return $existing;
64 }
65 }
66
67 $labelData = Helper::sanitizeLabel([
68 'label' => array_key_exists('title', $params) ? $params['title'] : ($existing ? $existing->title : ''),
69 'bg_color' => !empty($params['bg_color']) ? $params['bg_color'] : ($existing ? $existing->bg_color : ''),
70 'color' => !empty($params['color']) ? $params['color'] : ($existing ? $existing->color : ''),
71 'color_preset' => array_key_exists('color_preset', $params) ? $params['color_preset'] : null,
72 ]);
73
74 if (!array_key_exists('color_preset', $params)) {
75 unset($labelData['color_preset']);
76 }
77
78 if (empty($labelData['label']) && empty($labelData['bg_color'])) {
79 return MCPHelper::error('invalid_param', __('Provide a label title or a background color', 'fluent-boards'));
80 }
81
82 if ($isUpdate) {
83 $label = $service->editLabelofBoard($labelData, $existing->id, $board->id);
84 do_action('fluent_boards/board_label_updated', $label);
85 } else {
86 $label = $service->createLabel([
87 'label' => $labelData['label'] ?? '',
88 'bg_color' => !empty($labelData['bg_color']) ? $labelData['bg_color'] : self::DEFAULT_BG_COLOR,
89 'color' => !empty($labelData['color']) ? $labelData['color'] : self::DEFAULT_TEXT_COLOR,
90 'color_preset' => $labelData['color_preset'] ?? '',
91 ], $board->id);
92 do_action('fluent_boards/board_label_created', $label);
93 }
94
95 return [
96 'label' => self::formatLabel($label),
97 'message' => $isUpdate
98 ? __('Label has been updated', 'fluent-boards')
99 : __('Label has been created', 'fluent-boards'),
100 ];
101 }
102
103 public static function deleteLabel($params = [])
104 {
105 $board = MCPHelper::resolveBoard($params);
106 if (is_wp_error($board)) {
107 return $board;
108 }
109
110 if (!MCPHelper::canWriteBoard($board->id)) {
111 return MCPHelper::error('forbidden', __('You do not have permission to manage labels on this board', 'fluent-boards'));
112 }
113
114 $service = new LabelService();
115 $label = self::findLabel($service, $params['label_id'] ?? 0, $board->id);
116 if (is_wp_error($label)) {
117 return $label;
118 }
119
120 $service->deleteLabelOfBoard($label->id, $board->id);
121
122 return [
123 'board_id' => (int) $board->id,
124 'label_id' => (int) $label->id,
125 'message' => __('Label has been deleted', 'fluent-boards'),
126 ];
127 }
128
129 public static function addTaskLabel($params = [])
130 {
131 return self::syncTaskLabels($params, 'add');
132 }
133
134 public static function removeTaskLabel($params = [])
135 {
136 return self::syncTaskLabels($params, 'remove');
137 }
138
139 private static function syncTaskLabels($params, $mode)
140 {
141 $task = MCPHelper::resolveTask($params);
142 if (is_wp_error($task)) {
143 return $task;
144 }
145
146 if (!MCPHelper::canWriteBoard($task->board_id)) {
147 return MCPHelper::error('forbidden', __('You do not have permission to update this task', 'fluent-boards'));
148 }
149
150 $service = new LabelService();
151 $labels = self::resolveLabels($service, $params, $task->board_id);
152 if (is_wp_error($labels)) {
153 return $labels;
154 }
155
156 if (!$labels) {
157 return MCPHelper::error('invalid_param', __('Provide label_id, label_ids or label_titles', 'fluent-boards'));
158 }
159
160 $labelIds = array_map(function ($label) {
161 return (int) $label->id;
162 }, $labels);
163
164 $task->load('labels');
165 $currentIds = [];
166 foreach ($task->labels as $label) {
167 $currentIds[] = (int) $label->id;
168 }
169
170 $changedIds = $mode === 'add'
171 ? array_values(array_diff($labelIds, $currentIds))
172 : array_values(array_intersect($labelIds, $currentIds));
173
174 // One pivot write for the batch instead of a service call per label, each of
175 // which re-queried the task and the label.
176 if ($changedIds && $mode === 'add') {
177 $task->labels()->syncWithoutDetaching(array_fill_keys(
178 $changedIds,
179 ['object_type' => Constant::OBJECT_TYPE_TASK_LABEL]
180 ));
181 } elseif ($changedIds) {
182 $task->labels()->detach($changedIds);
183 }
184
185 $task->load('labels');
186
187 // Activity logging and integrations still expect one event per label.
188 $labelsById = [];
189 foreach ($labels as $label) {
190 $labelsById[(int) $label->id] = $label;
191 }
192
193 foreach ($changedIds as $labelId) {
194 if (empty($labelsById[$labelId])) {
195 continue;
196 }
197
198 $label = $labelsById[$labelId];
199 do_action('fluent_boards/task_label', $task, $label, $mode === 'add' ? 'added' : 'removed');
200 }
201
202 return [
203 'task_id' => (int) $task->id,
204 'board_id' => (int) $task->board_id,
205 'labels' => MCPHelper::formatLabelList($task->labels),
206 'message' => $mode === 'add'
207 ? __('Labels have been added to the task', 'fluent-boards')
208 : __('Labels have been removed from the task', 'fluent-boards'),
209 ];
210 }
211
212 /**
213 * Accepts label_id, label_ids and label_titles so an agent can attach labels it only
214 * knows by name without a lookup round-trip. Everything is resolved against the task's
215 * own board in a single query, and the batch is bounded, before anything is written.
216 *
217 * @return array|\WP_Error List of Label models.
218 */
219 private static function resolveLabels($service, $params, $boardId)
220 {
221 $ids = MCPHelper::sanitizeIdArray($params['label_ids'] ?? []);
222
223 if (!empty($params['label_id'])) {
224 $ids[] = absint($params['label_id']);
225 }
226
227 $titles = [];
228 if (!empty($params['label_titles']) && is_array($params['label_titles'])) {
229 foreach ($params['label_titles'] as $title) {
230 $title = sanitize_text_field((string) $title);
231 if ($title !== '') {
232 $titles[] = $title;
233 }
234 }
235 }
236
237 $ids = array_values(array_unique(array_filter($ids)));
238 $titles = array_values(array_unique($titles));
239
240 if (count($ids) + count($titles) > self::MAX_LABELS_PER_CALL) {
241 return MCPHelper::error('invalid_param', __('Too many labels in one call', 'fluent-boards'), [
242 'max' => self::MAX_LABELS_PER_CALL,
243 ]);
244 }
245
246 if (!$ids && !$titles) {
247 return [];
248 }
249
250 // Single board-scoped read covers both id and title resolution.
251 $boardLabels = $service->getLabelsByBoard($boardId);
252 $byId = [];
253 foreach ($boardLabels as $label) {
254 if ($label->type === 'label') {
255 $byId[(int) $label->id] = $label;
256 }
257 }
258
259 $resolved = [];
260
261 foreach ($ids as $id) {
262 if (!isset($byId[$id])) {
263 return MCPHelper::error('not_found', __('Label not found on this board', 'fluent-boards'), [
264 'label_id' => $id,
265 ]);
266 }
267 $resolved[$id] = $byId[$id];
268 }
269
270 foreach ($titles as $title) {
271 $match = null;
272 foreach ($byId as $label) {
273 if (strcasecmp((string) $label->title, $title) === 0) {
274 $match = $label;
275 break;
276 }
277 }
278
279 if (!$match) {
280 return MCPHelper::error('not_found', __('Label not found on this board', 'fluent-boards'), [
281 'title' => $title,
282 ]);
283 }
284
285 $resolved[(int) $match->id] = $match;
286 }
287
288 return array_values($resolved);
289 }
290
291 private static function findLabel($service, $labelId, $boardId)
292 {
293 $labelId = absint($labelId);
294 if (!$labelId) {
295 return MCPHelper::error('invalid_param', __('Provide label_id', 'fluent-boards'));
296 }
297
298 try {
299 return $service->findLabelOnBoard($labelId, $boardId);
300 } catch (\Exception $e) {
301 return MCPHelper::error('not_found', __('Label not found on this board', 'fluent-boards'), [
302 'label_id' => $labelId,
303 ]);
304 }
305 }
306
307 private static function formatLabel($label)
308 {
309 $formatted = MCPHelper::formatLabelList([$label]);
310
311 return $formatted ? $formatted[0] : null;
312 }
313 }
314