@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

Allow installs to customize project icons

Summary:
Ref T10010. Ref T5819. General alignment of the stars:

- There were some hacks in Conduit around stripping `fa-...` off icons when reading and writing that I wanted to get rid of.
- We probably have room for a subtitle in the new heavy nav, and using the icon name is a good starting point (and maybe good enough on its own?)
- The project list was real bad looking with redundant tag/names, now it is very slightly less bad looking with non-redundant types?
- Some installs will want to call Milestones something else, and this gets us a big part of the way there.
- This may slightly help to reinforce "tag" vs "policy" vs "group" stuff?

---

I'm letting installs have enough rope to shoot themselves in the foot (e.g., define 100 icons). It isn't the end of the world if they reuse icons, and is clearly their fault.

I think the cases where 100 icons will break down are:

- Icon selector dialog may get very unwieldy.
- Query UI will be pretty iffy/huge with 100 icons.

We could improve these fairly easily if an install comes up with a reasonable use case for having 100 icons.

---

The UI on the icon itself in the list views is a little iffy -- mostly, it's too saturated/bold.

I'd ideally like to try either:

- rendering a "shade" version (i.e. lighter, less-saturated color); or
- rendering a "shade" tag with just the icon in it.

However, there didn't seem to be a way to do the first one right now (`fa-example sh-blue` doesn't work) and the second one had weird margins/padding, so I left it like this for now. I figure we can clean it up once we build the thick nav, since that will probably also want an identical element.

(I don't want to render a full tag with the icon + name since I think that's confusing -- it looks like a project/object tag, but is not.)

Test Plan:
{F1049905}

{F1049906}

Reviewers: chad

Reviewed By: chad

Subscribers: 20after4, Luke081515.2

Maniphest Tasks: T5819, T10010

Differential Revision: https://secure.phabricator.com/D14918

+451 -95
+10 -12
resources/celerity/map.php
··· 37 37 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', 38 38 'rsrc/css/application/base/phui-theme.css' => '6b451f24', 39 39 'rsrc/css/application/base/standard-page-view.css' => '3c99cdf4', 40 - 'rsrc/css/application/calendar/calendar-icon.css' => 'c69aa59f', 41 40 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 42 41 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 43 42 'rsrc/css/application/config/config-options.css' => '0ede4c9b', ··· 94 93 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 95 94 'rsrc/css/application/policy/policy.css' => '957ea14c', 96 95 'rsrc/css/application/ponder/ponder-view.css' => '7b0df4da', 97 - 'rsrc/css/application/projects/project-icon.css' => '4e3eaa5a', 98 96 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 99 97 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 100 98 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', ··· 135 133 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 136 134 'rsrc/css/phui/phui-form.css' => '0b98e572', 137 135 'rsrc/css/phui/phui-header-view.css' => '55bb32dd', 136 + 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', 138 137 'rsrc/css/phui/phui-icon.css' => 'b0a6b1b6', 139 138 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', 140 139 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', ··· 465 464 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 466 465 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 467 466 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 468 - 'rsrc/js/core/behavior-choose-control.js' => '8fee767e', 467 + 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', 469 468 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 470 469 'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae', 471 470 'rsrc/js/core/behavior-device.js' => 'a205cf28', ··· 524 523 'aphront-typeahead-control-css' => '0e403212', 525 524 'auth-css' => '0877ed6e', 526 525 'bulk-job-css' => 'df9c1d4a', 527 - 'calendar-icon-css' => 'c69aa59f', 528 526 'changeset-view-manager' => '58562350', 529 527 'conduit-api-css' => '7bc725c4', 530 528 'config-options-css' => '0ede4c9b', ··· 571 569 'javelin-behavior-audio-source' => '59b251eb', 572 570 'javelin-behavior-audit-preview' => 'd835b03a', 573 571 'javelin-behavior-bulk-job-reload' => 'edf8a145', 574 - 'javelin-behavior-choose-control' => '8fee767e', 572 + 'javelin-behavior-choose-control' => '327a00d1', 575 573 'javelin-behavior-comment-actions' => 'b65559c0', 576 574 'javelin-behavior-config-reorder-fields' => 'b6993408', 577 575 'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a', ··· 809 807 'phui-form-css' => '0b98e572', 810 808 'phui-form-view-css' => '4a1a0f5e', 811 809 'phui-header-view-css' => '55bb32dd', 810 + 'phui-icon-set-selector-css' => '1ab67aad', 812 811 'phui-icon-view-css' => 'b0a6b1b6', 813 812 'phui-image-mask-css' => '5a8b09c8', 814 813 'phui-info-panel-css' => '27ea50a1', ··· 839 838 'policy-edit-css' => '815c66f7', 840 839 'policy-transaction-detail-css' => '82100a43', 841 840 'ponder-view-css' => '7b0df4da', 842 - 'project-icon-css' => '4e3eaa5a', 843 841 'raphael-core' => '51ee6b43', 844 842 'raphael-g' => '40dde778', 845 843 'raphael-g-line' => '40da039e', ··· 1044 1042 '2f670a96' => array( 1045 1043 'phui-theme-css', 1046 1044 ), 1045 + '327a00d1' => array( 1046 + 'javelin-behavior', 1047 + 'javelin-stratcom', 1048 + 'javelin-dom', 1049 + 'javelin-workflow', 1050 + ), 1047 1051 '331b1611' => array( 1048 1052 'javelin-install', 1049 1053 ), ··· 1521 1525 '8fba1997' => array( 1522 1526 'javelin-install', 1523 1527 'javelin-dom', 1524 - ), 1525 - '8fee767e' => array( 1526 - 'javelin-behavior', 1527 - 'javelin-stratcom', 1528 - 'javelin-dom', 1529 - 'javelin-workflow', 1530 1528 ), 1531 1529 '901935ef' => array( 1532 1530 'javelin-behavior',
+34
resources/sql/autopatches/20151231.proj.01.icon.php
··· 1 + <?php 2 + 3 + $icon_map = array( 4 + 'fa-briefcase' => 'project', 5 + 'fa-tags' => 'tag', 6 + 'fa-lock' => 'policy', 7 + 'fa-users' => 'group', 8 + 9 + 'fa-folder' => 'folder', 10 + 'fa-calendar' => 'timeline', 11 + 'fa-flag-checkered' => 'goal', 12 + 'fa-truck' => 'release', 13 + 14 + 'fa-bug' => 'bugs', 15 + 'fa-trash-o' => 'cleanup', 16 + 'fa-umbrella' => 'umbrella', 17 + 'fa-envelope' => 'communication', 18 + 19 + 'fa-building' => 'organization', 20 + 'fa-cloud' => 'infrastructure', 21 + 'fa-credit-card' => 'account', 22 + 'fa-flask' => 'experimental', 23 + ); 24 + 25 + $table = new PhabricatorProject(); 26 + $conn_w = $table->establishConnection('w'); 27 + foreach ($icon_map as $old_icon => $new_key) { 28 + queryfx( 29 + $conn_w, 30 + 'UPDATE %T SET icon = %s WHERE icon = %s', 31 + $table->getTableName(), 32 + $new_key, 33 + $old_icon); 34 + }
+2
src/__phutil_library_map__.php
··· 2902 2902 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 2903 2903 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 2904 2904 'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php', 2905 + 'PhabricatorProjectTypeConfigOptionType' => 'applications/project/config/PhabricatorProjectTypeConfigOptionType.php', 2905 2906 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 2906 2907 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 2907 2908 'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php', ··· 7268 7269 'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction', 7269 7270 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 7270 7271 'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 7272 + 'PhabricatorProjectTypeConfigOptionType' => 'PhabricatorConfigJSONOptionType', 7271 7273 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 7272 7274 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 7273 7275 'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
+1 -2
src/applications/config/custom/PhabricatorConfigOptionType.php
··· 24 24 $value) { 25 25 26 26 if (is_array($value)) { 27 - $json = new PhutilJSON(); 28 - return $json->encodeFormatted($value); 27 + return PhabricatorConfigJSON::prettyPrintJSON($value); 29 28 } else { 30 29 return $value; 31 30 }
+15 -8
src/applications/files/controller/PhabricatorFileIconSetSelectController.php
··· 26 26 } 27 27 } 28 28 29 - require_celerity_resource('project-icon-css'); 29 + require_celerity_resource('phui-icon-set-selector-css'); 30 30 Javelin::initBehavior('phabricator-tooltips'); 31 31 32 32 $ii = 0; ··· 37 37 $view = id(new PHUIIconView()) 38 38 ->setIconFont($icon->getIcon()); 39 39 40 + $classes = array(); 41 + $classes[] = 'icon-button'; 42 + 43 + $is_selected = ($icon->getKey() == $v_icon); 44 + 45 + if ($is_selected) { 46 + $classes[] = 'selected'; 47 + } 48 + 49 + $is_disabled = $icon->getIsDisabled(); 50 + if ($is_disabled && !$is_selected) { 51 + continue; 52 + } 53 + 40 54 $aural = javelin_tag( 41 55 'span', 42 56 array( 43 57 'aural' => true, 44 58 ), 45 59 pht('Choose "%s" Icon', $label)); 46 - 47 - $classes = array(); 48 - $classes[] = 'icon-button'; 49 - 50 - if ($icon->getKey() == $v_icon) { 51 - $classes[] = 'selected'; 52 - } 53 60 54 61 $buttons[] = javelin_tag( 55 62 'button',
+10
src/applications/files/iconset/PhabricatorIconSetIcon.php
··· 6 6 private $key; 7 7 private $icon; 8 8 private $label; 9 + private $isDisabled; 9 10 10 11 public function setKey($key) { 11 12 $this->key = $key; ··· 26 27 return $this->getKey(); 27 28 } 28 29 return $this->icon; 30 + } 31 + 32 + public function setIsDisabled($is_disabled) { 33 + $this->isDisabled = $is_disabled; 34 + return $this; 35 + } 36 + 37 + public function getIsDisabled() { 38 + return $this->isDisabled; 29 39 } 30 40 31 41 public function setLabel($label) {
+1 -1
src/applications/project/conduit/ProjectConduitAPIMethod.php
··· 26 26 $project_slugs = $project->getSlugs(); 27 27 $project_slugs = array_values(mpull($project_slugs, 'getSlug')); 28 28 29 - $project_icon = substr($project->getIcon(), 3); 29 + $project_icon = $project->getDisplayIconKey(); 30 30 31 31 $result[$project->getPHID()] = array( 32 32 'id' => $project->getID(),
-5
src/applications/project/conduit/ProjectQueryConduitAPIMethod.php
··· 76 76 $request->getValue('icons'); 77 77 if ($request->getValue('icons')) { 78 78 $icons = array(); 79 - // the internal 'fa-' prefix is a detail hidden from api clients 80 - // but needs to pre prepended to the values in the icons array: 81 - foreach ($request->getValue('icons') as $value) { 82 - $icons[] = 'fa-'.$value; 83 - } 84 79 $query->withIcons($icons); 85 80 } 86 81
+31
src/applications/project/config/PhabricatorProjectConfigOptions.php
··· 20 20 } 21 21 22 22 public function getOptions() { 23 + $default_icons = PhabricatorProjectIconSet::getDefaultConfiguration(); 24 + $icons_type = 'custom:PhabricatorProjectTypeConfigOptionType'; 25 + 26 + $icons_description = $this->deformat(pht(<<<EOTEXT 27 + Allows you to change and customize the available project icons. 28 + 29 + You can find a list of available icons in {nav UIExamples > Icons and Images}. 30 + 31 + Configure a list of icon specifications. Each icon specification should be 32 + a dictionary, which may contain these keys: 33 + 34 + - `key` //Required string.// Internal key identifying the icon. 35 + - `name` //Required string.// Human-readable icon name. 36 + - `icon` //Required string.// Specifies which actual icon image to use. 37 + - `default` //Optional bool.// Selects a default icon. Exactly one icon must 38 + be selected as the default. 39 + - `disabled` //Optional bool.// If true, this icon will no longer be 40 + available for selection when creating or editing projects. 41 + - `special` //Optional string.// Marks an icon as a special icon: 42 + - `milestone` This is the icon for milestones. Exactly one icon must be 43 + selected as the milestone icon. 44 + 45 + You can look at the default configuration below for an example of a valid 46 + configuration. 47 + EOTEXT 48 + )); 49 + 50 + 23 51 $default_fields = array( 24 52 'std:project:internal:description' => true, 25 53 ); ··· 45 73 $this->newOption('projects.fields', $custom_field_type, $default_fields) 46 74 ->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass()) 47 75 ->setDescription(pht('Select and reorder project fields.')), 76 + $this->newOption('projects.icons', $icons_type, $default_icons) 77 + ->setSummary(pht('Adjust project icons.')) 78 + ->setDescription($icons_description), 48 79 ); 49 80 } 50 81
+10
src/applications/project/config/PhabricatorProjectTypeConfigOptionType.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTypeConfigOptionType 4 + extends PhabricatorConfigJSONOptionType { 5 + 6 + public function validateOption(PhabricatorConfigOption $option, $value) { 7 + PhabricatorProjectIconSet::validateConfiguration($value); 8 + } 9 + 10 + }
+4 -4
src/applications/project/controller/PhabricatorProjectController.php
··· 156 156 $subprojects_icon = 'fa-sitemap grey'; 157 157 } 158 158 159 - if ($project->supportsMilestones()) { 160 - $milestones_icon = 'fa-map-marker'; 161 - } else { 162 - $milestones_icon = 'fa-map-marker grey'; 159 + $key = PhabricatorProjectIconSet::getMilestoneIconKey(); 160 + $milestones_icon = PhabricatorProjectIconSet::getIconIcon($key); 161 + if (!$project->supportsMilestones()) { 162 + $milestones_icon = "{$milestones_icon} grey"; 163 163 } 164 164 165 165 $nav->addIcon(
+285 -23
src/applications/project/icon/PhabricatorProjectIconSet.php
··· 5 5 6 6 const ICONSETKEY = 'projects'; 7 7 8 + const SPECIAL_MILESTONE = 'milestone'; 9 + 8 10 public function getSelectIconTitleText() { 9 11 return pht('Choose Project Icon'); 10 12 } 11 13 12 - protected function newIcons() { 13 - $map = array( 14 - 'fa-briefcase' => pht('Briefcase'), 15 - 'fa-tags' => pht('Tag'), 16 - 'fa-folder' => pht('Folder'), 17 - 'fa-users' => pht('Team'), 18 - 19 - 'fa-bug' => pht('Bug'), 20 - 'fa-trash-o' => pht('Garbage'), 21 - 'fa-calendar' => pht('Deadline'), 22 - 'fa-flag-checkered' => pht('Goal'), 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 + ), 22 + array( 23 + 'key' => 'tag', 24 + 'icon' => 'fa-tags', 25 + 'name' => pht('Tag'), 26 + ), 27 + array( 28 + 'key' => 'policy', 29 + 'icon' => 'fa-lock', 30 + 'name' => pht('Policy'), 31 + ), 32 + array( 33 + 'key' => 'group', 34 + 'icon' => 'fa-users', 35 + 'name' => pht('Group'), 36 + ), 37 + array( 38 + 'key' => 'folder', 39 + 'icon' => 'fa-folder', 40 + 'name' => pht('Folder'), 41 + ), 42 + array( 43 + 'key' => 'timeline', 44 + 'icon' => 'fa-calendar', 45 + 'name' => pht('Timeline'), 46 + ), 47 + array( 48 + 'key' => 'goal', 49 + 'icon' => 'fa-flag-checkered', 50 + 'name' => pht('Goal'), 51 + ), 52 + array( 53 + 'key' => 'release', 54 + 'icon' => 'fa-truck', 55 + 'name' => pht('Release'), 56 + ), 57 + array( 58 + 'key' => 'bugs', 59 + 'icon' => 'fa-bug', 60 + 'name' => pht('Bugs'), 61 + ), 62 + array( 63 + 'key' => 'cleanup', 64 + 'icon' => 'fa-trash-o', 65 + 'name' => pht('Cleanup'), 66 + ), 67 + array( 68 + 'key' => 'umbrella', 69 + 'icon' => 'fa-umbrella', 70 + 'name' => pht('Umbrella'), 71 + ), 72 + array( 73 + 'key' => 'communication', 74 + 'icon' => 'fa-envelope', 75 + 'name' => pht('Communication'), 76 + ), 77 + array( 78 + 'key' => 'organization', 79 + 'icon' => 'fa-building', 80 + 'name' => pht('Organization'), 81 + ), 82 + array( 83 + 'key' => 'infrastructure', 84 + 'icon' => 'fa-cloud', 85 + 'name' => pht('Infrastructure'), 86 + ), 87 + array( 88 + 'key' => 'account', 89 + 'icon' => 'fa-credit-card', 90 + 'name' => pht('Account'), 91 + ), 92 + array( 93 + 'key' => 'experimental', 94 + 'icon' => 'fa-flask', 95 + 'name' => pht('Experimental'), 96 + ), 97 + array( 98 + 'key' => 'milestone', 99 + 'icon' => 'fa-map-marker', 100 + 'name' => pht('Milestone'), 101 + 'special' => self::SPECIAL_MILESTONE, 102 + ), 103 + ); 104 + } 23 105 24 - 'fa-envelope' => pht('Communication'), 25 - 'fa-truck' => pht('Release'), 26 - 'fa-lock' => pht('Policy'), 27 - 'fa-umbrella' => pht('An Umbrella'), 28 106 29 - 'fa-cloud' => pht('The Cloud'), 30 - 'fa-building' => pht('Company'), 31 - 'fa-credit-card' => pht('Accounting'), 32 - 'fa-flask' => pht('Experimental'), 33 - ); 107 + protected function newIcons() { 108 + $map = self::getIconSpecifications(); 34 109 35 110 $icons = array(); 36 - foreach ($map as $key => $label) { 111 + foreach ($map as $spec) { 112 + $special = idx($spec, 'special'); 113 + 114 + if ($special === self::SPECIAL_MILESTONE) { 115 + continue; 116 + } 117 + 37 118 $icons[] = id(new PhabricatorIconSetIcon()) 38 - ->setKey($key) 39 - ->setLabel($label); 119 + ->setKey($spec['key']) 120 + ->setIsDisabled(idx($spec, 'disabled')) 121 + ->setIcon($spec['icon']) 122 + ->setLabel($spec['name']); 40 123 } 41 124 42 125 return $icons; ··· 50 133 unset($shades[PHUITagView::COLOR_DISABLED]); 51 134 52 135 return $shades; 136 + } 137 + 138 + private static function getIconSpecifications() { 139 + return PhabricatorEnv::getEnvConfig('projects.icons'); 140 + } 141 + 142 + public static function getDefaultIconKey() { 143 + $icons = self::getIconSpecifications(); 144 + foreach ($icons as $icon) { 145 + if (idx($icon, 'default')) { 146 + return $icon['key']; 147 + } 148 + } 149 + return null; 150 + } 151 + 152 + public static function getIconIcon($key) { 153 + $spec = self::getIconSpec($key); 154 + return idx($spec, 'icon', null); 155 + } 156 + 157 + public static function getIconName($key) { 158 + $spec = self::getIconSpec($key); 159 + return idx($spec, 'name', null); 160 + } 161 + 162 + private static function getIconSpec($key) { 163 + $icons = self::getIconSpecifications(); 164 + foreach ($icons as $icon) { 165 + if (idx($icon, 'key') === $key) { 166 + return $icon; 167 + } 168 + } 169 + 170 + return array(); 171 + } 172 + 173 + public static function getMilestoneIconKey() { 174 + $icons = self::getIconSpecifications(); 175 + foreach ($icons as $icon) { 176 + if (idx($icon, 'special') === self::SPECIAL_MILESTONE) { 177 + return idx($icon, 'key'); 178 + } 179 + } 180 + return null; 181 + } 182 + 183 + public static function validateConfiguration($config) { 184 + if (!is_array($config)) { 185 + throw new Exception( 186 + pht('Configuration must be a list of project icon specifications.')); 187 + } 188 + 189 + foreach ($config as $idx => $value) { 190 + if (!is_array($value)) { 191 + throw new Exception( 192 + pht( 193 + 'Value for index "%s" should be a dictionary.', 194 + $idx)); 195 + } 196 + 197 + PhutilTypeSpec::checkMap( 198 + $value, 199 + array( 200 + 'key' => 'string', 201 + 'name' => 'string', 202 + 'icon' => 'string', 203 + 'special' => 'optional string', 204 + 'disabled' => 'optional bool', 205 + 'default' => 'optional bool', 206 + )); 207 + 208 + if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) { 209 + throw new Exception( 210 + pht( 211 + 'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '. 212 + 'characters long and contain only lowercase letters. For example, '. 213 + '"%s" and "%s" are reasonable keys.', 214 + 'tag', 215 + 'group')); 216 + } 217 + 218 + $special = idx($value, 'special'); 219 + $valid = array( 220 + self::SPECIAL_MILESTONE => true, 221 + ); 222 + 223 + if ($special !== null) { 224 + if (empty($valid[$special])) { 225 + throw new Exception( 226 + pht( 227 + 'Icon special attribute "%s" is not valid. Recognized special '. 228 + 'attributes are: %s.', 229 + $special, 230 + implode(', ', array_keys($valid)))); 231 + } 232 + } 233 + } 234 + 235 + $default = null; 236 + $milestone = null; 237 + $keys = array(); 238 + foreach ($config as $idx => $value) { 239 + $key = $value['key']; 240 + if (isset($keys[$key])) { 241 + throw new Exception( 242 + pht( 243 + 'Project icons must have unique keys, but two icons share the '. 244 + 'same key ("%s").', 245 + $key)); 246 + } else { 247 + $keys[$key] = true; 248 + } 249 + 250 + $is_disabled = idx($value, 'disabled'); 251 + 252 + if (idx($value, 'default')) { 253 + if ($default === null) { 254 + if ($is_disabled) { 255 + throw new Exception( 256 + pht( 257 + 'The project icon marked as the default icon ("%s") must not '. 258 + 'be disabled.', 259 + $key)); 260 + } 261 + $default = $value; 262 + } else { 263 + $original_key = $default['key']; 264 + throw new Exception( 265 + pht( 266 + 'Two different icons ("%s", "%s") are marked as the default '. 267 + 'icon. Only one icon may be marked as the default.', 268 + $key, 269 + $original_key)); 270 + } 271 + } 272 + 273 + $special = idx($value, 'special'); 274 + if ($special === self::SPECIAL_MILESTONE) { 275 + if ($milestone === null) { 276 + if ($is_disabled) { 277 + throw new Exception( 278 + pht( 279 + 'The project icon ("%s") with special attribute "%s" must '. 280 + 'not be disabled', 281 + $key, 282 + self::SPECIAL_MIILESTONE)); 283 + } 284 + $milestone = $value; 285 + } else { 286 + $original_key = $milestone['key']; 287 + throw new Exception( 288 + pht( 289 + 'Two different icons ("%s", "%s") are marked with special '. 290 + 'attribute "%s". Only one icon may be marked with this '. 291 + 'attribute.', 292 + $key, 293 + $original_key, 294 + self::SPECIAL_MILESTONE)); 295 + } 296 + } 297 + } 298 + 299 + if ($default === null) { 300 + throw new Exception( 301 + pht( 302 + 'Project icons must include one icon marked as the "%s" icon, '. 303 + 'but no such icon exists.', 304 + 'default')); 305 + } 306 + 307 + if ($milestone === null) { 308 + throw new Exception( 309 + pht( 310 + 'Project icons must include one icon marked with special attribute '. 311 + '"%s", but no such icon exists.', 312 + self::SPECIAL_MILESTONE)); 313 + } 314 + 53 315 } 54 316 55 317 }
+1 -1
src/applications/project/phid/PhabricatorProjectProjectPHIDType.php
··· 51 51 } 52 52 53 53 $handle->setImageURI($project->getProfileImageURI()); 54 - $handle->setIcon($project->getDisplayIcon()); 54 + $handle->setIcon($project->getDisplayIconIcon()); 55 55 $handle->setTagColor($project->getDisplayColor()); 56 56 57 57 if ($project->isArchived()) {
+4
src/applications/project/query/PhabricatorProjectSearchEngine.php
··· 131 131 132 132 $set = new PhabricatorProjectIconSet(); 133 133 foreach ($set->getIcons() as $icon) { 134 + if ($icon->getIsDisabled()) { 135 + continue; 136 + } 137 + 134 138 $options[$icon->getKey()] = array( 135 139 id(new PHUIIconView()) 136 140 ->setIconFont($icon->getIcon()),
+27 -5
src/applications/project/storage/PhabricatorProject.php
··· 45 45 private $slugs = self::ATTACHABLE; 46 46 private $parentProject = self::ATTACHABLE; 47 47 48 - const DEFAULT_ICON = 'fa-briefcase'; 49 48 const DEFAULT_COLOR = 'blue'; 50 49 51 50 const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken'; ··· 63 62 $join_policy = $app->getPolicy( 64 63 ProjectDefaultJoinCapability::CAPABILITY); 65 64 65 + $default_icon = PhabricatorProjectIconSet::getDefaultIconKey(); 66 + 66 67 return id(new PhabricatorProject()) 67 68 ->setAuthorPHID($actor->getPHID()) 68 - ->setIcon(self::DEFAULT_ICON) 69 + ->setIcon($default_icon) 69 70 ->setColor(self::DEFAULT_COLOR) 70 71 ->setViewPolicy($view_policy) 71 72 ->setEditPolicy($edit_policy) ··· 484 485 return $number; 485 486 } 486 487 487 - public function getDisplayIcon() { 488 + public function getDisplayIconKey() { 488 489 if ($this->isMilestone()) { 489 - return 'fa-map-marker'; 490 + $key = PhabricatorProjectIconSet::getMilestoneIconKey(); 491 + } else { 492 + $key = $this->getIcon(); 490 493 } 491 494 492 - return $this->getIcon(); 495 + return $key; 496 + } 497 + 498 + public function getDisplayIconIcon() { 499 + $key = $this->getDisplayIconKey(); 500 + return PhabricatorProjectIconSet::getIconIcon($key); 501 + } 502 + 503 + public function getDisplayIconName() { 504 + $key = $this->getDisplayIconKey(); 505 + return PhabricatorProjectIconSet::getIconName($key); 493 506 } 494 507 495 508 public function getDisplayColor() { ··· 608 621 ->setKey('slug') 609 622 ->setType('string') 610 623 ->setDescription(pht('Primary slug/hashtag.')), 624 + id(new PhabricatorConduitSearchFieldSpecification()) 625 + ->setKey('icon') 626 + ->setType('map<string, wild>') 627 + ->setDescription(pht('Information about the project icon.')), 611 628 ); 612 629 } 613 630 ··· 615 632 return array( 616 633 'name' => $this->getName(), 617 634 'slug' => $this->getPrimarySlug(), 635 + 'icon' => array( 636 + 'key' => $this->getDisplayIconKey(), 637 + 'name' => $this->getDisplayIconName(), 638 + 'icon' => $this->getDisplayIconIcon(), 639 + ), 618 640 ); 619 641 } 620 642
+13 -4
src/applications/project/view/PhabricatorProjectListView.php
··· 25 25 foreach ($projects as $key => $project) { 26 26 $id = $project->getID(); 27 27 28 - $tag_list = id(new PHUIHandleTagListView()) 29 - ->setSlim(true) 30 - ->setHandles(array($handles[$project->getPHID()])); 28 + $icon = $project->getDisplayIconIcon(); 29 + $color = $project->getColor(); 30 + 31 + $icon_icon = id(new PHUIIconView()) 32 + ->setIconFont("{$icon} {$color}"); 33 + 34 + $icon_name = $project->getDisplayIconName(); 31 35 32 36 $item = id(new PHUIObjectItemView()) 33 37 ->setHeader($project->getName()) 34 38 ->setHref("/project/view/{$id}/") 35 39 ->setImageURI($project->getProfileImageURI()) 36 - ->addAttribute($tag_list); 40 + ->addAttribute( 41 + array( 42 + $icon_icon, 43 + ' ', 44 + $icon_name, 45 + )); 37 46 38 47 if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { 39 48 $item->addIcon('delete-grey', pht('Archived'));
-28
webroot/rsrc/css/application/calendar/calendar-icon.css
··· 1 - /** 2 - * @provides calendar-icon-css 3 - */ 4 - 5 - button.icon-button { 6 - background: {$lightgreybackground}; 7 - border: 1px solid {$lightblueborder}; 8 - position: relative; 9 - width: 16px; 10 - height: 16px; 11 - padding: 12px; 12 - margin: 4px; 13 - text-shadow: none; 14 - box-shadow: none; 15 - box-sizing: content-box; 16 - } 17 - 18 - .icon-grid { 19 - text-align: center; 20 - } 21 - 22 - .icon-icon + .icon-icon { 23 - margin-left: 4px; 24 - } 25 - 26 - button.icon-button.selected { 27 - background: {$bluebackground}; 28 - }
+2 -1
webroot/rsrc/css/application/projects/project-icon.css webroot/rsrc/css/phui/phui-icon-set-selector.css
··· 1 1 /** 2 - * @provides project-icon-css 2 + * @provides phui-icon-set-selector-css 3 3 */ 4 4 5 5 button.icon-button { ··· 25 25 26 26 button.icon-button.selected { 27 27 background: {$bluebackground}; 28 + border: 1px solid {$blueborder}; 28 29 }
+1 -1
webroot/rsrc/js/core/behavior-choose-control.js
··· 22 22 } 23 23 24 24 var params = { 25 - value: input.value 25 + icon: input.value 26 26 }; 27 27 28 28 new JX.Workflow(data.uri, params)