From 9ab22e21b305bd8767620ed25ce991e27b117388 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 30 Dec 2015 04:36:48 -0800 Subject: [PATCH] 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 --- resources/celerity/map.php | 22 +- .../sql/autopatches/20151231.proj.01.icon.php | 34 ++ src/__phutil_library_map__.php | 2 + .../custom/PhabricatorConfigOptionType.php | 3 +- ...PhabricatorFileIconSetSelectController.php | 23 +- .../files/iconset/PhabricatorIconSetIcon.php | 10 + .../conduit/ProjectConduitAPIMethod.php | 2 +- .../conduit/ProjectQueryConduitAPIMethod.php | 5 - .../PhabricatorProjectConfigOptions.php | 31 ++ ...PhabricatorProjectTypeConfigOptionType.php | 10 + .../PhabricatorProjectController.php | 8 +- .../icon/PhabricatorProjectIconSet.php | 310 ++++++++++++++++-- .../PhabricatorProjectProjectPHIDType.php | 2 +- .../query/PhabricatorProjectSearchEngine.php | 4 + .../project/storage/PhabricatorProject.php | 32 +- .../view/PhabricatorProjectListView.php | 17 +- .../application/calendar/calendar-icon.css | 28 -- .../phui-icon-set-selector.css} | 3 +- .../rsrc/js/core/behavior-choose-control.js | 2 +- 19 files changed, 452 insertions(+), 96 deletions(-) create mode 100644 resources/sql/autopatches/20151231.proj.01.icon.php create mode 100644 src/applications/project/config/PhabricatorProjectTypeConfigOptionType.php delete mode 100644 webroot/rsrc/css/application/calendar/calendar-icon.css rename webroot/rsrc/css/{application/projects/project-icon.css => phui/phui-icon-set-selector.css} (84%) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index aec7734a63..c32a98f1f2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -37,7 +37,6 @@ return array( 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', 'rsrc/css/application/base/phui-theme.css' => '6b451f24', 'rsrc/css/application/base/standard-page-view.css' => '3c99cdf4', - 'rsrc/css/application/calendar/calendar-icon.css' => 'c69aa59f', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '0ede4c9b', @@ -94,7 +93,6 @@ return array( 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/ponder/ponder-view.css' => '7b0df4da', - 'rsrc/css/application/projects/project-icon.css' => '4e3eaa5a', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', @@ -135,6 +133,7 @@ return array( 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 'rsrc/css/phui/phui-form.css' => '0b98e572', 'rsrc/css/phui/phui-header-view.css' => '55bb32dd', + 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', 'rsrc/css/phui/phui-icon.css' => 'b0a6b1b6', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', @@ -465,7 +464,7 @@ return array( 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', - 'rsrc/js/core/behavior-choose-control.js' => '8fee767e', + 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae', 'rsrc/js/core/behavior-device.js' => 'a205cf28', @@ -524,7 +523,6 @@ return array( 'aphront-typeahead-control-css' => '0e403212', 'auth-css' => '0877ed6e', 'bulk-job-css' => 'df9c1d4a', - 'calendar-icon-css' => 'c69aa59f', 'changeset-view-manager' => '58562350', 'conduit-api-css' => '7bc725c4', 'config-options-css' => '0ede4c9b', @@ -571,7 +569,7 @@ return array( 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', 'javelin-behavior-bulk-job-reload' => 'edf8a145', - 'javelin-behavior-choose-control' => '8fee767e', + 'javelin-behavior-choose-control' => '327a00d1', 'javelin-behavior-comment-actions' => 'b65559c0', 'javelin-behavior-config-reorder-fields' => 'b6993408', 'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a', @@ -809,6 +807,7 @@ return array( 'phui-form-css' => '0b98e572', 'phui-form-view-css' => '4a1a0f5e', 'phui-header-view-css' => '55bb32dd', + 'phui-icon-set-selector-css' => '1ab67aad', 'phui-icon-view-css' => 'b0a6b1b6', 'phui-image-mask-css' => '5a8b09c8', 'phui-info-panel-css' => '27ea50a1', @@ -839,7 +838,6 @@ return array( 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', - 'project-icon-css' => '4e3eaa5a', 'raphael-core' => '51ee6b43', 'raphael-g' => '40dde778', 'raphael-g-line' => '40da039e', @@ -1044,6 +1042,12 @@ return array( '2f670a96' => array( 'phui-theme-css', ), + '327a00d1' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-workflow', + ), '331b1611' => array( 'javelin-install', ), @@ -1522,12 +1526,6 @@ return array( 'javelin-install', 'javelin-dom', ), - '8fee767e' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-workflow', - ), '901935ef' => array( 'javelin-behavior', 'javelin-dom', diff --git a/resources/sql/autopatches/20151231.proj.01.icon.php b/resources/sql/autopatches/20151231.proj.01.icon.php new file mode 100644 index 0000000000..501614df3d --- /dev/null +++ b/resources/sql/autopatches/20151231.proj.01.icon.php @@ -0,0 +1,34 @@ + 'project', + 'fa-tags' => 'tag', + 'fa-lock' => 'policy', + 'fa-users' => 'group', + + 'fa-folder' => 'folder', + 'fa-calendar' => 'timeline', + 'fa-flag-checkered' => 'goal', + 'fa-truck' => 'release', + + 'fa-bug' => 'bugs', + 'fa-trash-o' => 'cleanup', + 'fa-umbrella' => 'umbrella', + 'fa-envelope' => 'communication', + + 'fa-building' => 'organization', + 'fa-cloud' => 'infrastructure', + 'fa-credit-card' => 'account', + 'fa-flask' => 'experimental', +); + +$table = new PhabricatorProject(); +$conn_w = $table->establishConnection('w'); +foreach ($icon_map as $old_icon => $new_key) { + queryfx( + $conn_w, + 'UPDATE %T SET icon = %s WHERE icon = %s', + $table->getTableName(), + $new_key, + $old_icon); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c73cec5965..2a0de867aa 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2902,6 +2902,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php', + 'PhabricatorProjectTypeConfigOptionType' => 'applications/project/config/PhabricatorProjectTypeConfigOptionType.php', 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php', @@ -7268,6 +7269,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorProjectTypeConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', diff --git a/src/applications/config/custom/PhabricatorConfigOptionType.php b/src/applications/config/custom/PhabricatorConfigOptionType.php index 3f588452c4..733229e652 100644 --- a/src/applications/config/custom/PhabricatorConfigOptionType.php +++ b/src/applications/config/custom/PhabricatorConfigOptionType.php @@ -24,8 +24,7 @@ abstract class PhabricatorConfigOptionType extends Phobject { $value) { if (is_array($value)) { - $json = new PhutilJSON(); - return $json->encodeFormatted($value); + return PhabricatorConfigJSON::prettyPrintJSON($value); } else { return $value; } diff --git a/src/applications/files/controller/PhabricatorFileIconSetSelectController.php b/src/applications/files/controller/PhabricatorFileIconSetSelectController.php index af46e2d66e..ed7d18a5ee 100644 --- a/src/applications/files/controller/PhabricatorFileIconSetSelectController.php +++ b/src/applications/files/controller/PhabricatorFileIconSetSelectController.php @@ -26,7 +26,7 @@ final class PhabricatorFileIconSetSelectController } } - require_celerity_resource('project-icon-css'); + require_celerity_resource('phui-icon-set-selector-css'); Javelin::initBehavior('phabricator-tooltips'); $ii = 0; @@ -37,6 +37,20 @@ final class PhabricatorFileIconSetSelectController $view = id(new PHUIIconView()) ->setIconFont($icon->getIcon()); + $classes = array(); + $classes[] = 'icon-button'; + + $is_selected = ($icon->getKey() == $v_icon); + + if ($is_selected) { + $classes[] = 'selected'; + } + + $is_disabled = $icon->getIsDisabled(); + if ($is_disabled && !$is_selected) { + continue; + } + $aural = javelin_tag( 'span', array( @@ -44,13 +58,6 @@ final class PhabricatorFileIconSetSelectController ), pht('Choose "%s" Icon', $label)); - $classes = array(); - $classes[] = 'icon-button'; - - if ($icon->getKey() == $v_icon) { - $classes[] = 'selected'; - } - $buttons[] = javelin_tag( 'button', array( diff --git a/src/applications/files/iconset/PhabricatorIconSetIcon.php b/src/applications/files/iconset/PhabricatorIconSetIcon.php index 1e1773aa16..03511cc75b 100644 --- a/src/applications/files/iconset/PhabricatorIconSetIcon.php +++ b/src/applications/files/iconset/PhabricatorIconSetIcon.php @@ -6,6 +6,7 @@ final class PhabricatorIconSetIcon private $key; private $icon; private $label; + private $isDisabled; public function setKey($key) { $this->key = $key; @@ -28,6 +29,15 @@ final class PhabricatorIconSetIcon return $this->icon; } + public function setIsDisabled($is_disabled) { + $this->isDisabled = $is_disabled; + return $this; + } + + public function getIsDisabled() { + return $this->isDisabled; + } + public function setLabel($label) { $this->label = $label; return $this; diff --git a/src/applications/project/conduit/ProjectConduitAPIMethod.php b/src/applications/project/conduit/ProjectConduitAPIMethod.php index 6acf320daf..f6e40f38a3 100644 --- a/src/applications/project/conduit/ProjectConduitAPIMethod.php +++ b/src/applications/project/conduit/ProjectConduitAPIMethod.php @@ -26,7 +26,7 @@ abstract class ProjectConduitAPIMethod extends ConduitAPIMethod { $project_slugs = $project->getSlugs(); $project_slugs = array_values(mpull($project_slugs, 'getSlug')); - $project_icon = substr($project->getIcon(), 3); + $project_icon = $project->getDisplayIconKey(); $result[$project->getPHID()] = array( 'id' => $project->getID(), diff --git a/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php b/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php index 2512a9a019..3116ecb7f9 100644 --- a/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php +++ b/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php @@ -76,11 +76,6 @@ final class ProjectQueryConduitAPIMethod extends ProjectConduitAPIMethod { $request->getValue('icons'); if ($request->getValue('icons')) { $icons = array(); - // the internal 'fa-' prefix is a detail hidden from api clients - // but needs to pre prepended to the values in the icons array: - foreach ($request->getValue('icons') as $value) { - $icons[] = 'fa-'.$value; - } $query->withIcons($icons); } diff --git a/src/applications/project/config/PhabricatorProjectConfigOptions.php b/src/applications/project/config/PhabricatorProjectConfigOptions.php index 8ec8bc8607..1cb7654732 100644 --- a/src/applications/project/config/PhabricatorProjectConfigOptions.php +++ b/src/applications/project/config/PhabricatorProjectConfigOptions.php @@ -20,6 +20,34 @@ final class PhabricatorProjectConfigOptions } public function getOptions() { + $default_icons = PhabricatorProjectIconSet::getDefaultConfiguration(); + $icons_type = 'custom:PhabricatorProjectTypeConfigOptionType'; + + $icons_description = $this->deformat(pht(<< Icons and Images}. + +Configure a list of icon specifications. Each icon specification should be +a dictionary, which may contain these keys: + + - `key` //Required string.// Internal key identifying the icon. + - `name` //Required string.// Human-readable icon name. + - `icon` //Required string.// Specifies which actual icon image to use. + - `default` //Optional bool.// Selects a default icon. Exactly one icon must + be selected as the default. + - `disabled` //Optional bool.// If true, this icon will no longer be + available for selection when creating or editing projects. + - `special` //Optional string.// Marks an icon as a special icon: + - `milestone` This is the icon for milestones. Exactly one icon must be + selected as the milestone icon. + +You can look at the default configuration below for an example of a valid +configuration. +EOTEXT + )); + + $default_fields = array( 'std:project:internal:description' => true, ); @@ -45,6 +73,9 @@ final class PhabricatorProjectConfigOptions $this->newOption('projects.fields', $custom_field_type, $default_fields) ->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass()) ->setDescription(pht('Select and reorder project fields.')), + $this->newOption('projects.icons', $icons_type, $default_icons) + ->setSummary(pht('Adjust project icons.')) + ->setDescription($icons_description), ); } diff --git a/src/applications/project/config/PhabricatorProjectTypeConfigOptionType.php b/src/applications/project/config/PhabricatorProjectTypeConfigOptionType.php new file mode 100644 index 0000000000..8c65dfe35a --- /dev/null +++ b/src/applications/project/config/PhabricatorProjectTypeConfigOptionType.php @@ -0,0 +1,10 @@ +supportsMilestones()) { - $milestones_icon = 'fa-map-marker'; - } else { - $milestones_icon = 'fa-map-marker grey'; + $key = PhabricatorProjectIconSet::getMilestoneIconKey(); + $milestones_icon = PhabricatorProjectIconSet::getIconIcon($key); + if (!$project->supportsMilestones()) { + $milestones_icon = "{$milestones_icon} grey"; } $nav->addIcon( diff --git a/src/applications/project/icon/PhabricatorProjectIconSet.php b/src/applications/project/icon/PhabricatorProjectIconSet.php index 470e871096..82592918b1 100644 --- a/src/applications/project/icon/PhabricatorProjectIconSet.php +++ b/src/applications/project/icon/PhabricatorProjectIconSet.php @@ -5,38 +5,121 @@ final class PhabricatorProjectIconSet const ICONSETKEY = 'projects'; + const SPECIAL_MILESTONE = 'milestone'; + public function getSelectIconTitleText() { return pht('Choose Project Icon'); } - protected function newIcons() { - $map = array( - 'fa-briefcase' => pht('Briefcase'), - 'fa-tags' => pht('Tag'), - 'fa-folder' => pht('Folder'), - 'fa-users' => pht('Team'), - - 'fa-bug' => pht('Bug'), - 'fa-trash-o' => pht('Garbage'), - 'fa-calendar' => pht('Deadline'), - 'fa-flag-checkered' => pht('Goal'), - - 'fa-envelope' => pht('Communication'), - 'fa-truck' => pht('Release'), - 'fa-lock' => pht('Policy'), - 'fa-umbrella' => pht('An Umbrella'), - - 'fa-cloud' => pht('The Cloud'), - 'fa-building' => pht('Company'), - 'fa-credit-card' => pht('Accounting'), - 'fa-flask' => pht('Experimental'), + public static function getDefaultConfiguration() { + return array( + array( + 'key' => 'project', + 'icon' => 'fa-briefcase', + 'name' => pht('Project'), + 'default' => true, + ), + array( + 'key' => 'tag', + 'icon' => 'fa-tags', + 'name' => pht('Tag'), + ), + array( + 'key' => 'policy', + 'icon' => 'fa-lock', + 'name' => pht('Policy'), + ), + array( + 'key' => 'group', + 'icon' => 'fa-users', + 'name' => pht('Group'), + ), + array( + 'key' => 'folder', + 'icon' => 'fa-folder', + 'name' => pht('Folder'), + ), + array( + 'key' => 'timeline', + 'icon' => 'fa-calendar', + 'name' => pht('Timeline'), + ), + array( + 'key' => 'goal', + 'icon' => 'fa-flag-checkered', + 'name' => pht('Goal'), + ), + array( + 'key' => 'release', + 'icon' => 'fa-truck', + 'name' => pht('Release'), + ), + array( + 'key' => 'bugs', + 'icon' => 'fa-bug', + 'name' => pht('Bugs'), + ), + array( + 'key' => 'cleanup', + 'icon' => 'fa-trash-o', + 'name' => pht('Cleanup'), + ), + array( + 'key' => 'umbrella', + 'icon' => 'fa-umbrella', + 'name' => pht('Umbrella'), + ), + array( + 'key' => 'communication', + 'icon' => 'fa-envelope', + 'name' => pht('Communication'), + ), + array( + 'key' => 'organization', + 'icon' => 'fa-building', + 'name' => pht('Organization'), + ), + array( + 'key' => 'infrastructure', + 'icon' => 'fa-cloud', + 'name' => pht('Infrastructure'), + ), + array( + 'key' => 'account', + 'icon' => 'fa-credit-card', + 'name' => pht('Account'), + ), + array( + 'key' => 'experimental', + 'icon' => 'fa-flask', + 'name' => pht('Experimental'), + ), + array( + 'key' => 'milestone', + 'icon' => 'fa-map-marker', + 'name' => pht('Milestone'), + 'special' => self::SPECIAL_MILESTONE, + ), ); + } + + + protected function newIcons() { + $map = self::getIconSpecifications(); $icons = array(); - foreach ($map as $key => $label) { + foreach ($map as $spec) { + $special = idx($spec, 'special'); + + if ($special === self::SPECIAL_MILESTONE) { + continue; + } + $icons[] = id(new PhabricatorIconSetIcon()) - ->setKey($key) - ->setLabel($label); + ->setKey($spec['key']) + ->setIsDisabled(idx($spec, 'disabled')) + ->setIcon($spec['icon']) + ->setLabel($spec['name']); } return $icons; @@ -52,4 +135,183 @@ final class PhabricatorProjectIconSet return $shades; } + private static function getIconSpecifications() { + return PhabricatorEnv::getEnvConfig('projects.icons'); + } + + public static function getDefaultIconKey() { + $icons = self::getIconSpecifications(); + foreach ($icons as $icon) { + if (idx($icon, 'default')) { + return $icon['key']; + } + } + return null; + } + + public static function getIconIcon($key) { + $spec = self::getIconSpec($key); + return idx($spec, 'icon', null); + } + + public static function getIconName($key) { + $spec = self::getIconSpec($key); + return idx($spec, 'name', null); + } + + private static function getIconSpec($key) { + $icons = self::getIconSpecifications(); + foreach ($icons as $icon) { + if (idx($icon, 'key') === $key) { + return $icon; + } + } + + return array(); + } + + public static function getMilestoneIconKey() { + $icons = self::getIconSpecifications(); + foreach ($icons as $icon) { + if (idx($icon, 'special') === self::SPECIAL_MILESTONE) { + return idx($icon, 'key'); + } + } + return null; + } + + public static function validateConfiguration($config) { + if (!is_array($config)) { + throw new Exception( + pht('Configuration must be a list of project icon specifications.')); + } + + foreach ($config as $idx => $value) { + if (!is_array($value)) { + throw new Exception( + pht( + 'Value for index "%s" should be a dictionary.', + $idx)); + } + + PhutilTypeSpec::checkMap( + $value, + array( + 'key' => 'string', + 'name' => 'string', + 'icon' => 'string', + 'special' => 'optional string', + 'disabled' => 'optional bool', + 'default' => 'optional bool', + )); + + if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) { + throw new Exception( + pht( + 'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '. + 'characters long and contain only lowercase letters. For example, '. + '"%s" and "%s" are reasonable keys.', + 'tag', + 'group')); + } + + $special = idx($value, 'special'); + $valid = array( + self::SPECIAL_MILESTONE => true, + ); + + if ($special !== null) { + if (empty($valid[$special])) { + throw new Exception( + pht( + 'Icon special attribute "%s" is not valid. Recognized special '. + 'attributes are: %s.', + $special, + implode(', ', array_keys($valid)))); + } + } + } + + $default = null; + $milestone = null; + $keys = array(); + foreach ($config as $idx => $value) { + $key = $value['key']; + if (isset($keys[$key])) { + throw new Exception( + pht( + 'Project icons must have unique keys, but two icons share the '. + 'same key ("%s").', + $key)); + } else { + $keys[$key] = true; + } + + $is_disabled = idx($value, 'disabled'); + + if (idx($value, 'default')) { + if ($default === null) { + if ($is_disabled) { + throw new Exception( + pht( + 'The project icon marked as the default icon ("%s") must not '. + 'be disabled.', + $key)); + } + $default = $value; + } else { + $original_key = $default['key']; + throw new Exception( + pht( + 'Two different icons ("%s", "%s") are marked as the default '. + 'icon. Only one icon may be marked as the default.', + $key, + $original_key)); + } + } + + $special = idx($value, 'special'); + if ($special === self::SPECIAL_MILESTONE) { + if ($milestone === null) { + if ($is_disabled) { + throw new Exception( + pht( + 'The project icon ("%s") with special attribute "%s" must '. + 'not be disabled', + $key, + self::SPECIAL_MIILESTONE)); + } + $milestone = $value; + } else { + $original_key = $milestone['key']; + throw new Exception( + pht( + 'Two different icons ("%s", "%s") are marked with special '. + 'attribute "%s". Only one icon may be marked with this '. + 'attribute.', + $key, + $original_key, + self::SPECIAL_MILESTONE)); + } + } + } + + if ($default === null) { + throw new Exception( + pht( + 'Project icons must include one icon marked as the "%s" icon, '. + 'but no such icon exists.', + 'default')); + } + + if ($milestone === null) { + throw new Exception( + pht( + 'Project icons must include one icon marked with special attribute '. + '"%s", but no such icon exists.', + self::SPECIAL_MILESTONE)); + } + + } + } diff --git a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php index eb1a59a5f5..b9fdefc6b3 100644 --- a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php +++ b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php @@ -51,7 +51,7 @@ final class PhabricatorProjectProjectPHIDType extends PhabricatorPHIDType { } $handle->setImageURI($project->getProfileImageURI()); - $handle->setIcon($project->getDisplayIcon()); + $handle->setIcon($project->getDisplayIconIcon()); $handle->setTagColor($project->getDisplayColor()); if ($project->isArchived()) { diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index 66d64dcdcc..eca90853cc 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -131,6 +131,10 @@ protected function buildQueryFromParameters(array $map) { $set = new PhabricatorProjectIconSet(); foreach ($set->getIcons() as $icon) { + if ($icon->getIsDisabled()) { + continue; + } + $options[$icon->getKey()] = array( id(new PHUIIconView()) ->setIconFont($icon->getIcon()), diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 5d2aa3461e..92c8ac68f8 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -45,7 +45,6 @@ final class PhabricatorProject extends PhabricatorProjectDAO private $slugs = self::ATTACHABLE; private $parentProject = self::ATTACHABLE; - const DEFAULT_ICON = 'fa-briefcase'; const DEFAULT_COLOR = 'blue'; const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken'; @@ -63,9 +62,11 @@ final class PhabricatorProject extends PhabricatorProjectDAO $join_policy = $app->getPolicy( ProjectDefaultJoinCapability::CAPABILITY); + $default_icon = PhabricatorProjectIconSet::getDefaultIconKey(); + return id(new PhabricatorProject()) ->setAuthorPHID($actor->getPHID()) - ->setIcon(self::DEFAULT_ICON) + ->setIcon($default_icon) ->setColor(self::DEFAULT_COLOR) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) @@ -484,12 +485,24 @@ final class PhabricatorProject extends PhabricatorProjectDAO return $number; } - public function getDisplayIcon() { + public function getDisplayIconKey() { if ($this->isMilestone()) { - return 'fa-map-marker'; + $key = PhabricatorProjectIconSet::getMilestoneIconKey(); + } else { + $key = $this->getIcon(); } - return $this->getIcon(); + return $key; + } + + public function getDisplayIconIcon() { + $key = $this->getDisplayIconKey(); + return PhabricatorProjectIconSet::getIconIcon($key); + } + + public function getDisplayIconName() { + $key = $this->getDisplayIconKey(); + return PhabricatorProjectIconSet::getIconName($key); } public function getDisplayColor() { @@ -608,6 +621,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO ->setKey('slug') ->setType('string') ->setDescription(pht('Primary slug/hashtag.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('icon') + ->setType('map') + ->setDescription(pht('Information about the project icon.')), ); } @@ -615,6 +632,11 @@ final class PhabricatorProject extends PhabricatorProjectDAO return array( 'name' => $this->getName(), 'slug' => $this->getPrimarySlug(), + 'icon' => array( + 'key' => $this->getDisplayIconKey(), + 'name' => $this->getDisplayIconName(), + 'icon' => $this->getDisplayIconIcon(), + ), ); } diff --git a/src/applications/project/view/PhabricatorProjectListView.php b/src/applications/project/view/PhabricatorProjectListView.php index f9e864ff3d..87874e3594 100644 --- a/src/applications/project/view/PhabricatorProjectListView.php +++ b/src/applications/project/view/PhabricatorProjectListView.php @@ -25,15 +25,24 @@ final class PhabricatorProjectListView extends AphrontView { foreach ($projects as $key => $project) { $id = $project->getID(); - $tag_list = id(new PHUIHandleTagListView()) - ->setSlim(true) - ->setHandles(array($handles[$project->getPHID()])); + $icon = $project->getDisplayIconIcon(); + $color = $project->getColor(); + + $icon_icon = id(new PHUIIconView()) + ->setIconFont("{$icon} {$color}"); + + $icon_name = $project->getDisplayIconName(); $item = id(new PHUIObjectItemView()) ->setHeader($project->getName()) ->setHref("/project/view/{$id}/") ->setImageURI($project->getProfileImageURI()) - ->addAttribute($tag_list); + ->addAttribute( + array( + $icon_icon, + ' ', + $icon_name, + )); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { $item->addIcon('delete-grey', pht('Archived')); diff --git a/webroot/rsrc/css/application/calendar/calendar-icon.css b/webroot/rsrc/css/application/calendar/calendar-icon.css deleted file mode 100644 index 35757de9e2..0000000000 --- a/webroot/rsrc/css/application/calendar/calendar-icon.css +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @provides calendar-icon-css - */ - -button.icon-button { - background: {$lightgreybackground}; - border: 1px solid {$lightblueborder}; - position: relative; - width: 16px; - height: 16px; - padding: 12px; - margin: 4px; - text-shadow: none; - box-shadow: none; - box-sizing: content-box; -} - -.icon-grid { - text-align: center; -} - -.icon-icon + .icon-icon { - margin-left: 4px; -} - -button.icon-button.selected { - background: {$bluebackground}; -} diff --git a/webroot/rsrc/css/application/projects/project-icon.css b/webroot/rsrc/css/phui/phui-icon-set-selector.css similarity index 84% rename from webroot/rsrc/css/application/projects/project-icon.css rename to webroot/rsrc/css/phui/phui-icon-set-selector.css index 1af85b30d6..09a0362246 100644 --- a/webroot/rsrc/css/application/projects/project-icon.css +++ b/webroot/rsrc/css/phui/phui-icon-set-selector.css @@ -1,5 +1,5 @@ /** - * @provides project-icon-css + * @provides phui-icon-set-selector-css */ button.icon-button { @@ -25,4 +25,5 @@ button.icon-button { button.icon-button.selected { background: {$bluebackground}; + border: 1px solid {$blueborder}; } diff --git a/webroot/rsrc/js/core/behavior-choose-control.js b/webroot/rsrc/js/core/behavior-choose-control.js index 3ff2f166ca..68fbbbc8e0 100644 --- a/webroot/rsrc/js/core/behavior-choose-control.js +++ b/webroot/rsrc/js/core/behavior-choose-control.js @@ -22,7 +22,7 @@ JX.behavior('choose-control', function() { } var params = { - value: input.value + icon: input.value }; new JX.Workflow(data.uri, params)