| @@ -16,26 +16,48 @@ | ||
| 16 | 16 | } |
| 17 | 17 | |
| 18 | 18 | public function getLabelsByBoardUsedInTasks($boardId) |
| 19 | 19 | { |
| 20 | - $boardLabel = Label::where('board_id', $boardId)->where('type', 'label')->orderBy('created_at', 'ASC')->get(); | |
| 20 | + $boardLabels = Label::where('board_id', $boardId) | |
| 21 | + ->where('type', 'label') | |
| 22 | + ->orderBy('created_at', 'ASC') | |
| 23 | + ->get(); | |
| 24 | + | |
| 25 | + if ($boardLabels->isEmpty()) { | |
| 26 | + return []; | |
| 27 | + } | |
| 28 | + | |
| 29 | + // One relation query for the whole board rather than an exists() per label. | |
| 30 | + $usedIds = Relation::where('object_type', Constant::OBJECT_TYPE_TASK_LABEL) | |
| 31 | + ->whereIn('foreign_id', $boardLabels->pluck('id')->all()) | |
| 32 | + ->distinct() | |
| 33 | + ->pluck('foreign_id') | |
| 34 | + ->all(); | |
| 35 | + | |
| 36 | + $usedIds = array_map('intval', $usedIds); | |
| 37 | + | |
| 21 | 38 | $usedLabel = []; |
| 22 | - foreach ($boardLabel as $label) { | |
| 23 | - $exist = Relation::where('foreign_id', $label->id)->where('object_type', 'task_label')->exists(); | |
| 24 | - if ($exist) { | |
| 39 | + foreach ($boardLabels as $label) { | |
| 40 | + if (in_array((int) $label->id, $usedIds, true)) { | |
| 25 | 41 | $usedLabel[] = $label; |
| 26 | 42 | } |
| 27 | 43 | } |
| 44 | + | |
| 28 | 45 | return $usedLabel; |
| 29 | 46 | } |
| 30 | 47 | |
| 31 | 48 | public function createLabel($labelData, $boardId) |
| 32 | 49 | { |
| 50 | + $labelData = $this->normalizeLabelColorData($labelData); | |
| 51 | + | |
| 33 | 52 | $label = new Label(); |
| 34 | 53 | $label->board_id = $boardId; |
| 35 | - $label->title = $labelData['label']; | |
| 36 | - $label->bg_color = $labelData['bg_color']; | |
| 37 | - $label->color = $labelData['color']; | |
| 54 | + $label->title = $labelData['label'] ?? ''; | |
| 55 | + $label->bg_color = $labelData['bg_color'] ?? ''; | |
| 56 | + $label->color = $labelData['color'] ?? ''; | |
| 57 | + if (isset($labelData['settings'])) { | |
| 58 | + $label->settings = $labelData['settings']; | |
| 59 | + } | |
| 38 | 60 | $label->save(); |
| 39 | 61 | |
| 40 | 62 | return $label; |
| 41 | 63 | } |
| @@ -41,26 +63,26 @@ | ||
| 41 | 63 | } |
| 42 | 64 | |
| 43 | 65 | public function createDefaultLabel($boardId) |
| 44 | 66 | { |
| 45 | - $defaultColors = [ | |
| 46 | - "green" => "#4bce97", | |
| 47 | - "yellow" => "#f5cd47", | |
| 48 | - "orange" => "#fea362", | |
| 49 | - "red" => "#f87168", | |
| 50 | - "purple" => "#9f8fef" | |
| 51 | - ]; | |
| 67 | + $defaultColors = ['green-bold', 'yellow-bold', 'orange-bold', 'red-bold', 'purple-bold']; | |
| 52 | 68 | |
| 53 | 69 | $data = []; |
| 54 | 70 | |
| 55 | - foreach ($defaultColors as $index => $bg_color) | |
| 71 | + foreach ($defaultColors as $presetId) | |
| 56 | 72 | { |
| 73 | + $preset = Constant::getLabelColorPreset($presetId); | |
| 74 | + $colorName = strtok($presetId, '-'); | |
| 57 | 75 | $data[] = [ |
| 58 | 76 | 'board_id' => $boardId, |
| 59 | - 'slug' => $index, | |
| 77 | + // Titles match the create-board modal defaults, otherwise boards created | |
| 78 | + // outside that modal end up with colour chips carrying no text. | |
| 79 | + 'title' => ucfirst($colorName), | |
| 80 | + 'slug' => $colorName, | |
| 60 | 81 | 'type' => 'label', |
| 61 | - 'bg_color' => $bg_color, | |
| 62 | - 'color' => Constant::TEXT_COLOR_MAP[$index], | |
| 82 | + 'bg_color' => $preset['light_bg_color'], | |
| 83 | + 'color' => $preset['light_text_color'], | |
| 84 | + 'settings' => maybe_serialize([Constant::LABEL_COLOR_PRESET_SETTING => $presetId]), | |
| 63 | 85 | 'created_at' => current_time('mysql'), |
| 64 | 86 | 'updated_at' => current_time('mysql') |
| 65 | 87 | ]; |
| 66 | 88 | } |
| @@ -67,15 +89,16 @@ | ||
| 67 | 89 | |
| 68 | 90 | Label::insert($data); |
| 69 | 91 | } |
| 70 | 92 | |
| 71 | - public function createLabelForTask($labelData) | |
| 93 | + public function createLabelForTask($labelData, $boardId = null) | |
| 72 | 94 | { |
| 73 | - $task = Task::findOrFail($labelData['task_id']); | |
| 95 | + $task = $boardId ? (new TaskService())->findTaskOnBoard($labelData['task_id'], $boardId) : Task::findOrFail($labelData['task_id']); | |
| 96 | + $label = $this->findLabelOnBoard($labelData['board_term_id'], $boardId ?: $task->board_id); | |
| 74 | 97 | |
| 75 | - $task->labels()->syncWithoutDetaching([$labelData['boardTerm_id'] => ['object_type' => Constant::OBJECT_TYPE_TASK_LABEL]]); | |
| 98 | + $task->labels()->syncWithoutDetaching([$labelData['board_term_id'] => ['object_type' => Constant::OBJECT_TYPE_TASK_LABEL]]); | |
| 76 | 99 | |
| 77 | - $label = $task->labels->find($labelData['boardTerm_id']); | |
| 100 | + $label = $task->labels->find($label->id); | |
| 78 | 101 | |
| 79 | 102 | do_action('fluent_boards/task_label',$task, $label, 'added'); |
| 80 | 103 | |
| 81 | 104 | return $label; |
| @@ -80,11 +103,11 @@ | ||
| 80 | 103 | |
| 81 | 104 | return $label; |
| 82 | 105 | } |
| 83 | 106 | |
| 84 | - public function getLabelsByTask($taskId) | |
| 107 | + public function getLabelsByTask($taskId, $boardId = null) | |
| 85 | 108 | { |
| 86 | - $task = Task::findOrFail($taskId); | |
| 109 | + $task = $boardId ? (new TaskService())->findTaskOnBoard($taskId, $boardId) : Task::findOrFail($taskId); | |
| 87 | 110 | return $task->labels; |
| 88 | 111 | } |
| 89 | 112 | |
| 90 | 113 | public function labelsByBoardId($boardId) |
| @@ -91,37 +114,75 @@ | ||
| 91 | 114 | { |
| 92 | 115 | return Label::where('board_id', $boardId)->whereNull('archived_at')->get(); |
| 93 | 116 | } |
| 94 | 117 | |
| 95 | - public function deleteLabelOfTask($taskId, $labelId) | |
| 118 | + public function deleteLabelOfTask($taskId, $labelId, $boardId = null) | |
| 96 | 119 | { |
| 97 | - $task = Task::findOrFail($taskId); | |
| 120 | + $task = $boardId ? (new TaskService())->findTaskOnBoard($taskId, $boardId) : Task::findOrFail($taskId); | |
| 121 | + $label = $this->findLabelOnBoard($labelId, $boardId ?: $task->board_id); | |
| 98 | 122 | $task->labels()->detach($labelId); |
| 99 | - $label = Label::findOrFail($labelId); | |
| 100 | 123 | do_action('fluent_boards/task_label',$task, $label, 'removed'); |
| 101 | 124 | } |
| 102 | 125 | |
| 103 | - public function deleteLabelOfBoard($labelId) | |
| 126 | + public function deleteLabelOfBoard($labelId, $boardId = null) | |
| 104 | 127 | { |
| 105 | - $label = Label::findOrFail($labelId); | |
| 106 | - $boardId = $label->board_id; | |
| 128 | + $label = $boardId ? $this->findLabelOnBoard($labelId, $boardId) : Label::findOrFail($labelId); | |
| 129 | + $label->tasks()->detach(); | |
| 107 | 130 | $label->delete(); |
| 108 | 131 | |
| 109 | 132 | do_action('fluent_boards/board_label_deleted', $label); |
| 110 | 133 | } |
| 111 | 134 | |
| 112 | - public function editLabelofBoard($labelData, $id) | |
| 135 | + public function editLabelofBoard($labelData, $id, $boardId = null) | |
| 113 | 136 | { |
| 114 | - $label = Label::findOrFail($id); | |
| 115 | - $label->title = $labelData['label']; | |
| 116 | - if ($label->bg_color != $labelData['bg_color']) { | |
| 137 | + $label = $boardId ? $this->findLabelOnBoard($id, $boardId) : Label::findOrFail($id); | |
| 138 | + $labelData = $this->normalizeLabelColorData($labelData, $label); | |
| 139 | + $label->title = $labelData['label'] ?? $label->title; | |
| 140 | + | |
| 141 | + // Background and text colour move independently: coupling them dropped a | |
| 142 | + // text-colour-only change on the floor while still reporting success. | |
| 143 | + if (isset($labelData['bg_color']) && $labelData['bg_color'] !== '') { | |
| 117 | 144 | $label->bg_color = $labelData['bg_color']; |
| 145 | + } | |
| 146 | + | |
| 147 | + if (isset($labelData['color']) && $labelData['color'] !== '') { | |
| 118 | 148 | $label->color = $labelData['color']; |
| 119 | 149 | } |
| 150 | + | |
| 151 | + if (array_key_exists('settings', $labelData)) { | |
| 152 | + if (array_key_exists('color_preset', $labelData) && $labelData['color_preset'] === '') { | |
| 153 | + $label->replaceSettings($labelData['settings']); | |
| 154 | + } else { | |
| 155 | + $label->settings = $labelData['settings']; | |
| 156 | + } | |
| 157 | + } | |
| 158 | + | |
| 120 | 159 | $label->save(); |
| 121 | 160 | return $label; |
| 122 | 161 | } |
| 123 | 162 | |
| 163 | + /** | |
| 164 | + * Resolve a label only when it belongs to the requested board. | |
| 165 | + * | |
| 166 | + * @param int $labelId | |
| 167 | + * @param int $boardId | |
| 168 | + * @return Label | |
| 169 | + * @throws \Exception | |
| 170 | + */ | |
| 171 | + public function findLabelOnBoard($labelId, $boardId) | |
| 172 | + { | |
| 173 | + $label = Label::where('id', absint($labelId)) | |
| 174 | + ->where('board_id', absint($boardId)) | |
| 175 | + ->where('type', 'label') | |
| 176 | + ->first(); | |
| 177 | + | |
| 178 | + if (!$label) { | |
| 179 | + throw new \Exception(esc_html__('Label not found', 'fluent-boards')); | |
| 180 | + } | |
| 181 | + | |
| 182 | + return $label; | |
| 183 | + } | |
| 184 | + | |
| 124 | 185 | public function copyLabelsOfBoard($boardId, $board) |
| 125 | 186 | { |
| 126 | 187 | $boardCopyFrom = Board::findOrFail($boardId); |
| 127 | 188 | |
| @@ -136,8 +197,13 @@ | ||
| 136 | 197 | $labelToSave['type'] = 'label'; |
| 137 | 198 | $labelToSave['position'] = 0; |
| 138 | 199 | $labelToSave['color'] = $label->color; |
| 139 | 200 | $labelToSave['bg_color'] = $label->bg_color; |
| 201 | + $settings = (array) $label->settings; | |
| 202 | + $presetId = $settings[Constant::LABEL_COLOR_PRESET_SETTING] ?? ''; | |
| 203 | + if (Constant::getLabelColorPreset($presetId)) { | |
| 204 | + $labelToSave['settings'] = [Constant::LABEL_COLOR_PRESET_SETTING => $presetId]; | |
| 205 | + } | |
| 140 | 206 | $copiedLabel = Label::create($labelToSave); |
| 141 | 207 | |
| 142 | 208 | $labelMap[$label['id']] = $copiedLabel->id; |
| 143 | 209 | } |
| @@ -142,12 +208,64 @@ | ||
| 142 | 208 | $labelMap[$label['id']] = $copiedLabel->id; |
| 143 | 209 | } |
| 144 | 210 | return $labelMap; |
| 145 | 211 | } |
| 146 | - public function getLastOneMinuteUpdatedLabels($boardId) | |
| 212 | + | |
| 213 | + /** | |
| 214 | + * Converts a selected preset into stable light-mode fallback colors. | |
| 215 | + * | |
| 216 | + * @param array $labelData | |
| 217 | + * @param Label|null $label | |
| 218 | + * @return array | |
| 219 | + * @throws \Exception | |
| 220 | + */ | |
| 221 | + private function normalizeLabelColorData($labelData, $label = null) | |
| 147 | 222 | { |
| 148 | - $oneMinuteAgoTimestamp = current_time('timestamp') - 60; | |
| 149 | - return Label::where('board_id', $boardId) | |
| 150 | - ->where('updated_at', '>=', date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp)) | |
| 151 | - ->get(); | |
| 223 | + if (!array_key_exists('color_preset', $labelData)) { | |
| 224 | + return $labelData; | |
| 225 | + } | |
| 226 | + | |
| 227 | + $presetId = $labelData['color_preset']; | |
| 228 | + $settings = $label ? (array) $label->settings : []; | |
| 229 | + | |
| 230 | + // Framework request extraction can represent an omitted optional field | |
| 231 | + // as null. That must preserve an existing preset rather than reject it. | |
| 232 | + if ($presetId === null) { | |
| 233 | + unset($labelData['color_preset']); | |
| 234 | + return $labelData; | |
| 235 | + } | |
| 236 | + | |
| 237 | + if ($presetId === '') { | |
| 238 | + unset($settings[Constant::LABEL_COLOR_PRESET_SETTING]); | |
| 239 | + $labelData['settings'] = $settings; | |
| 240 | + return $labelData; | |
| 241 | + } | |
| 242 | + | |
| 243 | + $preset = Constant::getLabelColorPreset($presetId); | |
| 244 | + if (!$preset) { | |
| 245 | + throw new \Exception(esc_html__('Invalid label color preset', 'fluent-boards')); | |
| 246 | + } | |
| 247 | + | |
| 248 | + $settings[Constant::LABEL_COLOR_PRESET_SETTING] = $preset['id']; | |
| 249 | + $labelData['settings'] = $settings; | |
| 250 | + $labelData['bg_color'] = $preset['light_bg_color']; | |
| 251 | + $labelData['color'] = $preset['light_text_color']; | |
| 252 | + | |
| 253 | + return $labelData; | |
| 152 | 254 | } |
| 153 | -} | |
| 255 | + public function getLastOneMinuteUpdatedLabels($boardId, $lastUpdated = null, $includeArchived = true) | |
| 256 | + { | |
| 257 | + if (!$lastUpdated) { | |
| 258 | + $oneMinuteAgoTimestamp = current_time('timestamp') - 60; | |
| 259 | + $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp); | |
| 260 | + } | |
| 261 | + | |
| 262 | + $labelsQuery = Label::where('board_id', $boardId) | |
| 263 | + ->where('updated_at', '>=', $lastUpdated); | |
| 264 | + | |
| 265 | + if (!$includeArchived) { | |
| 266 | + $labelsQuery->whereNull('archived_at'); | |
| 267 | + } | |
| 268 | + | |
| 269 | + return $labelsQuery->get(); | |
| 270 | + } | |
| 271 | +} | |