PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
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 1.45 All 41 releases
fluent-boards / app / Modules / MCP / Tools / LabelTools.php

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

308 lines 10.2 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 ]);
72
73 if (empty($labelData['label']) && empty($labelData['bg_color'])) {
74 return MCPHelper::error('invalid_param', __('Provide a label title or a background color', 'fluent-boards'));
75 }
76
77 if ($isUpdate) {
78 $label = $service->editLabelofBoard($labelData, $existing->id, $board->id);
79 do_action('fluent_boards/board_label_updated', $label);
80 } else {
81 $label = $service->createLabel([
82 'label' => $labelData['label'] ?? '',
83 'bg_color' => !empty($labelData['bg_color']) ? $labelData['bg_color'] : self::DEFAULT_BG_COLOR,
84 'color' => !empty($labelData['color']) ? $labelData['color'] : self::DEFAULT_TEXT_COLOR,
85 ], $board->id);
86 do_action('fluent_boards/board_label_created', $label);
87 }
88
89 return [
90 'label' => self::formatLabel($label),
91 'message' => $isUpdate
92 ? __('Label has been updated', 'fluent-boards')
93 : __('Label has been created', 'fluent-boards'),
94 ];
95 }
96
97 public static function deleteLabel($params = [])
98 {
99 $board = MCPHelper::resolveBoard($params);
100 if (is_wp_error($board)) {
101 return $board;
102 }
103
104 if (!MCPHelper::canWriteBoard($board->id)) {
105 return MCPHelper::error('forbidden', __('You do not have permission to manage labels on this board', 'fluent-boards'));
106 }
107
108 $service = new LabelService();
109 $label = self::findLabel($service, $params['label_id'] ?? 0, $board->id);
110 if (is_wp_error($label)) {
111 return $label;
112 }
113
114 $service->deleteLabelOfBoard($label->id, $board->id);
115
116 return [
117 'board_id' => (int) $board->id,
118 'label_id' => (int) $label->id,
119 'message' => __('Label has been deleted', 'fluent-boards'),
120 ];
121 }
122
123 public static function addTaskLabel($params = [])
124 {
125 return self::syncTaskLabels($params, 'add');
126 }
127
128 public static function removeTaskLabel($params = [])
129 {
130 return self::syncTaskLabels($params, 'remove');
131 }
132
133 private static function syncTaskLabels($params, $mode)
134 {
135 $task = MCPHelper::resolveTask($params);
136 if (is_wp_error($task)) {
137 return $task;
138 }
139
140 if (!MCPHelper::canWriteBoard($task->board_id)) {
141 return MCPHelper::error('forbidden', __('You do not have permission to update this task', 'fluent-boards'));
142 }
143
144 $service = new LabelService();
145 $labels = self::resolveLabels($service, $params, $task->board_id);
146 if (is_wp_error($labels)) {
147 return $labels;
148 }
149
150 if (!$labels) {
151 return MCPHelper::error('invalid_param', __('Provide label_id, label_ids or label_titles', 'fluent-boards'));
152 }
153
154 $labelIds = array_map(function ($label) {
155 return (int) $label->id;
156 }, $labels);
157
158 $task->load('labels');
159 $currentIds = [];
160 foreach ($task->labels as $label) {
161 $currentIds[] = (int) $label->id;
162 }
163
164 $changedIds = $mode === 'add'
165 ? array_values(array_diff($labelIds, $currentIds))
166 : array_values(array_intersect($labelIds, $currentIds));
167
168 // One pivot write for the batch instead of a service call per label, each of
169 // which re-queried the task and the label.
170 if ($changedIds && $mode === 'add') {
171 $task->labels()->syncWithoutDetaching(array_fill_keys(
172 $changedIds,
173 ['object_type' => Constant::OBJECT_TYPE_TASK_LABEL]
174 ));
175 } elseif ($changedIds) {
176 $task->labels()->detach($changedIds);
177 }
178
179 $task->load('labels');
180
181 // Activity logging and integrations still expect one event per label.
182 $labelsById = [];
183 foreach ($labels as $label) {
184 $labelsById[(int) $label->id] = $label;
185 }
186
187 foreach ($changedIds as $labelId) {
188 if (empty($labelsById[$labelId])) {
189 continue;
190 }
191
192 $label = $labelsById[$labelId];
193 do_action('fluent_boards/task_label', $task, $label, $mode === 'add' ? 'added' : 'removed');
194 }
195
196 return [
197 'task_id' => (int) $task->id,
198 'board_id' => (int) $task->board_id,
199 'labels' => MCPHelper::formatLabelList($task->labels),
200 'message' => $mode === 'add'
201 ? __('Labels have been added to the task', 'fluent-boards')
202 : __('Labels have been removed from the task', 'fluent-boards'),
203 ];
204 }
205
206 /**
207 * Accepts label_id, label_ids and label_titles so an agent can attach labels it only
208 * knows by name without a lookup round-trip. Everything is resolved against the task's
209 * own board in a single query, and the batch is bounded, before anything is written.
210 *
211 * @return array|\WP_Error List of Label models.
212 */
213 private static function resolveLabels($service, $params, $boardId)
214 {
215 $ids = MCPHelper::sanitizeIdArray($params['label_ids'] ?? []);
216
217 if (!empty($params['label_id'])) {
218 $ids[] = absint($params['label_id']);
219 }
220
221 $titles = [];
222 if (!empty($params['label_titles']) && is_array($params['label_titles'])) {
223 foreach ($params['label_titles'] as $title) {
224 $title = sanitize_text_field((string) $title);
225 if ($title !== '') {
226 $titles[] = $title;
227 }
228 }
229 }
230
231 $ids = array_values(array_unique(array_filter($ids)));
232 $titles = array_values(array_unique($titles));
233
234 if (count($ids) + count($titles) > self::MAX_LABELS_PER_CALL) {
235 return MCPHelper::error('invalid_param', __('Too many labels in one call', 'fluent-boards'), [
236 'max' => self::MAX_LABELS_PER_CALL,
237 ]);
238 }
239
240 if (!$ids && !$titles) {
241 return [];
242 }
243
244 // Single board-scoped read covers both id and title resolution.
245 $boardLabels = $service->getLabelsByBoard($boardId);
246 $byId = [];
247 foreach ($boardLabels as $label) {
248 if ($label->type === 'label') {
249 $byId[(int) $label->id] = $label;
250 }
251 }
252
253 $resolved = [];
254
255 foreach ($ids as $id) {
256 if (!isset($byId[$id])) {
257 return MCPHelper::error('not_found', __('Label not found on this board', 'fluent-boards'), [
258 'label_id' => $id,
259 ]);
260 }
261 $resolved[$id] = $byId[$id];
262 }
263
264 foreach ($titles as $title) {
265 $match = null;
266 foreach ($byId as $label) {
267 if (strcasecmp((string) $label->title, $title) === 0) {
268 $match = $label;
269 break;
270 }
271 }
272
273 if (!$match) {
274 return MCPHelper::error('not_found', __('Label not found on this board', 'fluent-boards'), [
275 'title' => $title,
276 ]);
277 }
278
279 $resolved[(int) $match->id] = $match;
280 }
281
282 return array_values($resolved);
283 }
284
285 private static function findLabel($service, $labelId, $boardId)
286 {
287 $labelId = absint($labelId);
288 if (!$labelId) {
289 return MCPHelper::error('invalid_param', __('Provide label_id', 'fluent-boards'));
290 }
291
292 try {
293 return $service->findLabelOnBoard($labelId, $boardId);
294 } catch (\Exception $e) {
295 return MCPHelper::error('not_found', __('Label not found on this board', 'fluent-boards'), [
296 'label_id' => $labelId,
297 ]);
298 }
299 }
300
301 private static function formatLabel($label)
302 {
303 $formatted = MCPHelper::formatLabelList([$label]);
304
305 return $formatted ? $formatted[0] : null;
306 }
307 }
308