@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorProjectIconSet
4 extends PhabricatorIconSet {
5
6 const ICONSETKEY = 'projects';
7
8 const SPECIAL_MILESTONE = 'milestone';
9
10 public function getSelectIconTitleText() {
11 return pht('Choose Project Icon');
12 }
13
14 public static function getDefaultConfiguration() {
15 return array(
16 array(
17 'key' => 'project',
18 'icon' => 'fa-briefcase',
19 'name' => pht('Project'),
20 'default' => true,
21 'image' => 'v3/briefcase.png',
22 ),
23 array(
24 'key' => 'tag',
25 'icon' => 'fa-tags',
26 'name' => pht('Tag'),
27 'image' => 'v3/tag.png',
28 ),
29 array(
30 'key' => 'policy',
31 'icon' => 'fa-lock',
32 'name' => pht('Policy'),
33 'image' => 'v3/lock.png',
34 ),
35 array(
36 'key' => 'group',
37 'icon' => 'fa-users',
38 'name' => pht('Group'),
39 'image' => 'v3/people.png',
40 ),
41 array(
42 'key' => 'folder',
43 'icon' => 'fa-folder',
44 'name' => pht('Folder'),
45 'image' => 'v3/folder.png',
46 ),
47 array(
48 'key' => 'timeline',
49 'icon' => 'fa-calendar',
50 'name' => pht('Timeline'),
51 'image' => 'v3/calendar.png',
52 ),
53 array(
54 'key' => 'goal',
55 'icon' => 'fa-flag-checkered',
56 'name' => pht('Goal'),
57 'image' => 'v3/flag.png',
58 ),
59 array(
60 'key' => 'release',
61 'icon' => 'fa-truck',
62 'name' => pht('Release'),
63 'image' => 'v3/truck.png',
64 ),
65 array(
66 'key' => 'bugs',
67 'icon' => 'fa-bug',
68 'name' => pht('Bugs'),
69 'image' => 'v3/bug.png',
70 ),
71 array(
72 'key' => 'cleanup',
73 'icon' => 'fa-trash-o',
74 'name' => pht('Cleanup'),
75 'image' => 'v3/trash.png',
76 ),
77 array(
78 'key' => 'umbrella',
79 'icon' => 'fa-umbrella',
80 'name' => pht('Umbrella'),
81 'image' => 'v3/umbrella.png',
82 ),
83 array(
84 'key' => 'communication',
85 'icon' => 'fa-envelope',
86 'name' => pht('Communication'),
87 'image' => 'v3/mail.png',
88 ),
89 array(
90 'key' => 'organization',
91 'icon' => 'fa-building',
92 'name' => pht('Organization'),
93 'image' => 'v3/organization.png',
94 ),
95 array(
96 'key' => 'infrastructure',
97 'icon' => 'fa-cloud',
98 'name' => pht('Infrastructure'),
99 'image' => 'v3/cloud.png',
100 ),
101 array(
102 'key' => 'account',
103 'icon' => 'fa-credit-card',
104 'name' => pht('Account'),
105 'image' => 'v3/creditcard.png',
106 ),
107 array(
108 'key' => 'experimental',
109 'icon' => 'fa-flask',
110 'name' => pht('Experimental'),
111 'image' => 'v3/experimental.png',
112 ),
113 array(
114 'key' => 'milestone',
115 'icon' => 'fa-map-marker',
116 'name' => pht('Milestone'),
117 'special' => self::SPECIAL_MILESTONE,
118 'image' => 'v3/marker.png',
119 ),
120 );
121 }
122
123
124 protected function newIcons() {
125 $map = self::getIconSpecifications();
126
127 $icons = array();
128 foreach ($map as $spec) {
129 $special = idx($spec, 'special');
130
131 if ($special === self::SPECIAL_MILESTONE) {
132 continue;
133 }
134
135 $icons[] = id(new PhabricatorIconSetIcon())
136 ->setKey($spec['key'])
137 ->setIsDisabled(idx($spec, 'disabled'))
138 ->setIcon($spec['icon'])
139 ->setLabel($spec['name']);
140 }
141
142 return $icons;
143 }
144
145 private static function getIconSpecifications() {
146 return PhabricatorEnv::getEnvConfig('projects.icons');
147 }
148
149 public static function getDefaultIconKey() {
150 $icons = self::getIconSpecifications();
151 foreach ($icons as $icon) {
152 if (idx($icon, 'default')) {
153 return $icon['key'];
154 }
155 }
156 return null;
157 }
158
159 public static function getIconIcon($key) {
160 $spec = self::getIconSpec($key);
161 return idx($spec, 'icon', null);
162 }
163
164 public static function getIconName($key) {
165 $spec = self::getIconSpec($key);
166 return idx($spec, 'name', null);
167 }
168
169 public static function getIconImage($key) {
170 $spec = self::getIconSpec($key);
171 return idx($spec, 'image', 'v3/briefcase.png');
172 }
173
174 private static function getIconSpec($key) {
175 $icons = self::getIconSpecifications();
176 foreach ($icons as $icon) {
177 if (idx($icon, 'key') === $key) {
178 return $icon;
179 }
180 }
181
182 return array();
183 }
184
185 public static function getMilestoneIconKey() {
186 $icons = self::getIconSpecifications();
187 foreach ($icons as $icon) {
188 if (idx($icon, 'special') === self::SPECIAL_MILESTONE) {
189 return idx($icon, 'key');
190 }
191 }
192 return null;
193 }
194
195 public static function validateConfiguration($config) {
196 if (!is_array($config)) {
197 throw new Exception(
198 pht('Configuration must be a list of project icon specifications.'));
199 }
200
201 foreach ($config as $idx => $value) {
202 if (!is_array($value)) {
203 throw new Exception(
204 pht(
205 'Value for index "%s" should be a dictionary.',
206 $idx));
207 }
208
209 PhutilTypeSpec::checkMap(
210 $value,
211 array(
212 'key' => 'string',
213 'name' => 'string',
214 'icon' => 'string',
215 'image' => 'optional string',
216 'special' => 'optional string',
217 'disabled' => 'optional bool',
218 'default' => 'optional bool',
219 ));
220
221 if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) {
222 throw new Exception(
223 pht(
224 'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '.
225 'characters long and contain only lowercase letters. For example, '.
226 '"%s" and "%s" are reasonable keys.',
227 $value['key'],
228 'tag',
229 'group'));
230 }
231
232 $special = idx($value, 'special');
233 $valid = array(
234 self::SPECIAL_MILESTONE => true,
235 );
236
237 if ($special !== null) {
238 if (empty($valid[$special])) {
239 throw new Exception(
240 pht(
241 'Icon special attribute "%s" is not valid. Recognized special '.
242 'attributes are: %s.',
243 $special,
244 implode(', ', array_keys($valid))));
245 }
246 }
247 }
248
249 $default = null;
250 $milestone = null;
251 $keys = array();
252 foreach ($config as $idx => $value) {
253 $key = $value['key'];
254 if (isset($keys[$key])) {
255 throw new Exception(
256 pht(
257 'Project icons must have unique keys, but two icons share the '.
258 'same key ("%s").',
259 $key));
260 } else {
261 $keys[$key] = true;
262 }
263
264 $is_disabled = idx($value, 'disabled');
265
266 $image = idx($value, 'image');
267 if ($image !== null) {
268 $builtin = idx($value, 'image');
269 $builtin_map = id(new PhabricatorFilesOnDiskBuiltinFile())
270 ->getProjectBuiltinFiles();
271 $builtin_map = array_flip($builtin_map);
272
273 $root = dirname(phutil_get_library_root('phabricator'));
274 $image = $root.'/resources/builtin/projects/'.$builtin;
275
276 if (!array_key_exists($image, $builtin_map)) {
277 throw new Exception(
278 pht(
279 'The project image ("%s") specified for ("%s") '.
280 'was not found in the folder "resources/builtin/projects/".',
281 $builtin,
282 $key));
283 }
284 }
285
286 if (idx($value, 'default')) {
287 if ($default === null) {
288 if ($is_disabled) {
289 throw new Exception(
290 pht(
291 'The project icon marked as the default icon ("%s") must not '.
292 'be disabled.',
293 $key));
294 }
295 $default = $value;
296 } else {
297 $original_key = $default['key'];
298 throw new Exception(
299 pht(
300 'Two different icons ("%s", "%s") are marked as the default '.
301 'icon. Only one icon may be marked as the default.',
302 $key,
303 $original_key));
304 }
305 }
306
307 $special = idx($value, 'special');
308 if ($special === self::SPECIAL_MILESTONE) {
309 if ($milestone === null) {
310 if ($is_disabled) {
311 throw new Exception(
312 pht(
313 'The project icon ("%s") with special attribute "%s" must '.
314 'not be disabled',
315 $key,
316 self::SPECIAL_MILESTONE));
317 }
318 $milestone = $value;
319 } else {
320 $original_key = $milestone['key'];
321 throw new Exception(
322 pht(
323 'Two different icons ("%s", "%s") are marked with special '.
324 'attribute "%s". Only one icon may be marked with this '.
325 'attribute.',
326 $key,
327 $original_key,
328 self::SPECIAL_MILESTONE));
329 }
330 }
331 }
332
333 if ($default === null) {
334 throw new Exception(
335 pht(
336 'Project icons must include one icon marked as the "%s" icon, '.
337 'but no such icon exists.',
338 'default'));
339 }
340
341 if ($milestone === null) {
342 throw new Exception(
343 pht(
344 'Project icons must include one icon marked with special attribute '.
345 '"%s", but no such icon exists.',
346 self::SPECIAL_MILESTONE));
347 }
348
349 }
350
351 private static function getColorSpecifications() {
352 return PhabricatorEnv::getEnvConfig('projects.colors');
353 }
354
355 public static function getColorMap() {
356 $specifications = self::getColorSpecifications();
357 return ipull($specifications, 'name', 'key');
358 }
359
360 public static function getDefaultColorKey() {
361 $specifications = self::getColorSpecifications();
362
363 foreach ($specifications as $specification) {
364 if (idx($specification, 'default')) {
365 return $specification['key'];
366 }
367 }
368
369 return null;
370 }
371
372 private static function getAvailableColorKeys() {
373 $list = array();
374
375 $specifications = self::getDefaultColorMap();
376 foreach ($specifications as $specification) {
377 $list[] = $specification['key'];
378 }
379
380 return $list;
381 }
382
383 public static function getColorName($color_key) {
384 $map = self::getColorMap();
385 return idx($map, $color_key);
386 }
387
388 /**
389 * Get the default value for the config `project.colors`.
390 *
391 * The result is simple enough to be easily JSON-encoded later.
392 * @return array Array of colors. Each color is an associative array with
393 * these keys: 'key' (color key) and 'name' (translated label).
394 */
395 public static function getDefaultColorMap() {
396 // In the future, generate this list using PHUITagView::getShadeMapCached(),
397 // instead of re-defining these colors also here.
398 // https://we.phorge.it/T16240
399 return array(
400 array(
401 'key' => PHUITagView::COLOR_RED,
402 'name' => pht('Red'),
403 ),
404 array(
405 'key' => PHUITagView::COLOR_ORANGE,
406 'name' => pht('Orange'),
407 ),
408 array(
409 'key' => PHUITagView::COLOR_YELLOW,
410 'name' => pht('Yellow'),
411 ),
412 array(
413 'key' => PHUITagView::COLOR_GREEN,
414 'name' => pht('Green'),
415 ),
416 array(
417 'key' => PHUITagView::COLOR_BLUE,
418 'name' => pht('Blue'),
419 'default' => true,
420 ),
421 array(
422 'key' => PHUITagView::COLOR_INDIGO,
423 'name' => pht('Indigo'),
424 ),
425 array(
426 'key' => PHUITagView::COLOR_VIOLET,
427 'name' => pht('Violet'),
428 ),
429 array(
430 'key' => PHUITagView::COLOR_PINK,
431 'name' => pht('Pink'),
432 ),
433 array(
434 'key' => PHUITagView::COLOR_GREY,
435 'name' => pht('Grey'),
436 ),
437 array(
438 'key' => PHUITagView::COLOR_CHECKERED,
439 'name' => pht('Checkered'),
440 ),
441 );
442 }
443
444 public static function validateColorConfiguration($config) {
445 if (!is_array($config)) {
446 throw new Exception(
447 pht('Configuration must be a list of project color specifications.'));
448 }
449
450 $available_keys = self::getAvailableColorKeys();
451 $available_keys = array_fuse($available_keys);
452
453 foreach ($config as $idx => $value) {
454 if (!is_array($value)) {
455 throw new Exception(
456 pht(
457 'Value for index "%s" should be a dictionary.',
458 $idx));
459 }
460
461 PhutilTypeSpec::checkMap(
462 $value,
463 array(
464 'key' => 'string',
465 'name' => 'string',
466 'default' => 'optional bool',
467 ));
468
469 $key = $value['key'];
470 if (!isset($available_keys[$key])) {
471 throw new Exception(
472 pht(
473 'Color key "%s" is not a valid color key. The supported color '.
474 'keys are: %s.',
475 $key,
476 implode(', ', $available_keys)));
477 }
478 }
479
480 $default = null;
481 $keys = array();
482 foreach ($config as $idx => $value) {
483 $key = $value['key'];
484 if (isset($keys[$key])) {
485 throw new Exception(
486 pht(
487 'Project colors must have unique keys, but two icons share the '.
488 'same key ("%s").',
489 $key));
490 } else {
491 $keys[$key] = true;
492 }
493
494 if (idx($value, 'default')) {
495 if ($default === null) {
496 $default = $value;
497 } else {
498 $original_key = $default['key'];
499 throw new Exception(
500 pht(
501 'Two different colors ("%s", "%s") are marked as the default '.
502 'color. Only one color may be marked as the default.',
503 $key,
504 $original_key));
505 }
506 }
507 }
508
509 if ($default === null) {
510 throw new Exception(
511 pht(
512 'Project colors must include one color marked as the "%s" color, '.
513 'but no such color exists.',
514 'default'));
515 }
516
517 }
518
519}