mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 00:42:41 +01:00
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
This commit is contained in:
parent
c7520cd9f2
commit
9ab22e21b3
19 changed files with 452 additions and 96 deletions
|
@ -37,7 +37,6 @@ return array(
|
||||||
'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601',
|
'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601',
|
||||||
'rsrc/css/application/base/phui-theme.css' => '6b451f24',
|
'rsrc/css/application/base/phui-theme.css' => '6b451f24',
|
||||||
'rsrc/css/application/base/standard-page-view.css' => '3c99cdf4',
|
'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/chatlog/chatlog.css' => 'd295b020',
|
||||||
'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4',
|
'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4',
|
||||||
'rsrc/css/application/config/config-options.css' => '0ede4c9b',
|
'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-transaction-detail.css' => '82100a43',
|
||||||
'rsrc/css/application/policy/policy.css' => '957ea14c',
|
'rsrc/css/application/policy/policy.css' => '957ea14c',
|
||||||
'rsrc/css/application/ponder/ponder-view.css' => '7b0df4da',
|
'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-core.css' => '9b3c5733',
|
||||||
'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5',
|
'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5',
|
||||||
'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd',
|
'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-view.css' => '4a1a0f5e',
|
||||||
'rsrc/css/phui/phui-form.css' => '0b98e572',
|
'rsrc/css/phui/phui-form.css' => '0b98e572',
|
||||||
'rsrc/css/phui/phui-header-view.css' => '55bb32dd',
|
'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-icon.css' => 'b0a6b1b6',
|
||||||
'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8',
|
'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8',
|
||||||
'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
|
'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-active-nav.js' => 'e379b58e',
|
||||||
'rsrc/js/core/behavior-audio-source.js' => '59b251eb',
|
'rsrc/js/core/behavior-audio-source.js' => '59b251eb',
|
||||||
'rsrc/js/core/behavior-autofocus.js' => '7319e029',
|
'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-crop.js' => 'fa0f4fc2',
|
||||||
'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae',
|
'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae',
|
||||||
'rsrc/js/core/behavior-device.js' => 'a205cf28',
|
'rsrc/js/core/behavior-device.js' => 'a205cf28',
|
||||||
|
@ -524,7 +523,6 @@ return array(
|
||||||
'aphront-typeahead-control-css' => '0e403212',
|
'aphront-typeahead-control-css' => '0e403212',
|
||||||
'auth-css' => '0877ed6e',
|
'auth-css' => '0877ed6e',
|
||||||
'bulk-job-css' => 'df9c1d4a',
|
'bulk-job-css' => 'df9c1d4a',
|
||||||
'calendar-icon-css' => 'c69aa59f',
|
|
||||||
'changeset-view-manager' => '58562350',
|
'changeset-view-manager' => '58562350',
|
||||||
'conduit-api-css' => '7bc725c4',
|
'conduit-api-css' => '7bc725c4',
|
||||||
'config-options-css' => '0ede4c9b',
|
'config-options-css' => '0ede4c9b',
|
||||||
|
@ -571,7 +569,7 @@ return array(
|
||||||
'javelin-behavior-audio-source' => '59b251eb',
|
'javelin-behavior-audio-source' => '59b251eb',
|
||||||
'javelin-behavior-audit-preview' => 'd835b03a',
|
'javelin-behavior-audit-preview' => 'd835b03a',
|
||||||
'javelin-behavior-bulk-job-reload' => 'edf8a145',
|
'javelin-behavior-bulk-job-reload' => 'edf8a145',
|
||||||
'javelin-behavior-choose-control' => '8fee767e',
|
'javelin-behavior-choose-control' => '327a00d1',
|
||||||
'javelin-behavior-comment-actions' => 'b65559c0',
|
'javelin-behavior-comment-actions' => 'b65559c0',
|
||||||
'javelin-behavior-config-reorder-fields' => 'b6993408',
|
'javelin-behavior-config-reorder-fields' => 'b6993408',
|
||||||
'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a',
|
'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a',
|
||||||
|
@ -809,6 +807,7 @@ return array(
|
||||||
'phui-form-css' => '0b98e572',
|
'phui-form-css' => '0b98e572',
|
||||||
'phui-form-view-css' => '4a1a0f5e',
|
'phui-form-view-css' => '4a1a0f5e',
|
||||||
'phui-header-view-css' => '55bb32dd',
|
'phui-header-view-css' => '55bb32dd',
|
||||||
|
'phui-icon-set-selector-css' => '1ab67aad',
|
||||||
'phui-icon-view-css' => 'b0a6b1b6',
|
'phui-icon-view-css' => 'b0a6b1b6',
|
||||||
'phui-image-mask-css' => '5a8b09c8',
|
'phui-image-mask-css' => '5a8b09c8',
|
||||||
'phui-info-panel-css' => '27ea50a1',
|
'phui-info-panel-css' => '27ea50a1',
|
||||||
|
@ -839,7 +838,6 @@ return array(
|
||||||
'policy-edit-css' => '815c66f7',
|
'policy-edit-css' => '815c66f7',
|
||||||
'policy-transaction-detail-css' => '82100a43',
|
'policy-transaction-detail-css' => '82100a43',
|
||||||
'ponder-view-css' => '7b0df4da',
|
'ponder-view-css' => '7b0df4da',
|
||||||
'project-icon-css' => '4e3eaa5a',
|
|
||||||
'raphael-core' => '51ee6b43',
|
'raphael-core' => '51ee6b43',
|
||||||
'raphael-g' => '40dde778',
|
'raphael-g' => '40dde778',
|
||||||
'raphael-g-line' => '40da039e',
|
'raphael-g-line' => '40da039e',
|
||||||
|
@ -1044,6 +1042,12 @@ return array(
|
||||||
'2f670a96' => array(
|
'2f670a96' => array(
|
||||||
'phui-theme-css',
|
'phui-theme-css',
|
||||||
),
|
),
|
||||||
|
'327a00d1' => array(
|
||||||
|
'javelin-behavior',
|
||||||
|
'javelin-stratcom',
|
||||||
|
'javelin-dom',
|
||||||
|
'javelin-workflow',
|
||||||
|
),
|
||||||
'331b1611' => array(
|
'331b1611' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
),
|
),
|
||||||
|
@ -1522,12 +1526,6 @@ return array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
),
|
),
|
||||||
'8fee767e' => array(
|
|
||||||
'javelin-behavior',
|
|
||||||
'javelin-stratcom',
|
|
||||||
'javelin-dom',
|
|
||||||
'javelin-workflow',
|
|
||||||
),
|
|
||||||
'901935ef' => array(
|
'901935ef' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
|
34
resources/sql/autopatches/20151231.proj.01.icon.php
Normal file
34
resources/sql/autopatches/20151231.proj.01.icon.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$icon_map = array(
|
||||||
|
'fa-briefcase' => '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);
|
||||||
|
}
|
|
@ -2902,6 +2902,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
|
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
|
||||||
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
||||||
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
|
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
|
||||||
|
'PhabricatorProjectTypeConfigOptionType' => 'applications/project/config/PhabricatorProjectTypeConfigOptionType.php',
|
||||||
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
|
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
|
||||||
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
|
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
|
||||||
'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php',
|
'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php',
|
||||||
|
@ -7268,6 +7269,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction',
|
'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction',
|
||||||
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||||
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||||
|
'PhabricatorProjectTypeConfigOptionType' => 'PhabricatorConfigJSONOptionType',
|
||||||
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
|
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
|
||||||
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
|
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
||||||
|
|
|
@ -24,8 +24,7 @@ abstract class PhabricatorConfigOptionType extends Phobject {
|
||||||
$value) {
|
$value) {
|
||||||
|
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
$json = new PhutilJSON();
|
return PhabricatorConfigJSON::prettyPrintJSON($value);
|
||||||
return $json->encodeFormatted($value);
|
|
||||||
} else {
|
} else {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
Javelin::initBehavior('phabricator-tooltips');
|
||||||
|
|
||||||
$ii = 0;
|
$ii = 0;
|
||||||
|
@ -37,6 +37,20 @@ final class PhabricatorFileIconSetSelectController
|
||||||
$view = id(new PHUIIconView())
|
$view = id(new PHUIIconView())
|
||||||
->setIconFont($icon->getIcon());
|
->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(
|
$aural = javelin_tag(
|
||||||
'span',
|
'span',
|
||||||
array(
|
array(
|
||||||
|
@ -44,13 +58,6 @@ final class PhabricatorFileIconSetSelectController
|
||||||
),
|
),
|
||||||
pht('Choose "%s" Icon', $label));
|
pht('Choose "%s" Icon', $label));
|
||||||
|
|
||||||
$classes = array();
|
|
||||||
$classes[] = 'icon-button';
|
|
||||||
|
|
||||||
if ($icon->getKey() == $v_icon) {
|
|
||||||
$classes[] = 'selected';
|
|
||||||
}
|
|
||||||
|
|
||||||
$buttons[] = javelin_tag(
|
$buttons[] = javelin_tag(
|
||||||
'button',
|
'button',
|
||||||
array(
|
array(
|
||||||
|
|
|
@ -6,6 +6,7 @@ final class PhabricatorIconSetIcon
|
||||||
private $key;
|
private $key;
|
||||||
private $icon;
|
private $icon;
|
||||||
private $label;
|
private $label;
|
||||||
|
private $isDisabled;
|
||||||
|
|
||||||
public function setKey($key) {
|
public function setKey($key) {
|
||||||
$this->key = $key;
|
$this->key = $key;
|
||||||
|
@ -28,6 +29,15 @@ final class PhabricatorIconSetIcon
|
||||||
return $this->icon;
|
return $this->icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setIsDisabled($is_disabled) {
|
||||||
|
$this->isDisabled = $is_disabled;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIsDisabled() {
|
||||||
|
return $this->isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
public function setLabel($label) {
|
public function setLabel($label) {
|
||||||
$this->label = $label;
|
$this->label = $label;
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
@ -26,7 +26,7 @@ abstract class ProjectConduitAPIMethod extends ConduitAPIMethod {
|
||||||
$project_slugs = $project->getSlugs();
|
$project_slugs = $project->getSlugs();
|
||||||
$project_slugs = array_values(mpull($project_slugs, 'getSlug'));
|
$project_slugs = array_values(mpull($project_slugs, 'getSlug'));
|
||||||
|
|
||||||
$project_icon = substr($project->getIcon(), 3);
|
$project_icon = $project->getDisplayIconKey();
|
||||||
|
|
||||||
$result[$project->getPHID()] = array(
|
$result[$project->getPHID()] = array(
|
||||||
'id' => $project->getID(),
|
'id' => $project->getID(),
|
||||||
|
|
|
@ -76,11 +76,6 @@ final class ProjectQueryConduitAPIMethod extends ProjectConduitAPIMethod {
|
||||||
$request->getValue('icons');
|
$request->getValue('icons');
|
||||||
if ($request->getValue('icons')) {
|
if ($request->getValue('icons')) {
|
||||||
$icons = array();
|
$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);
|
$query->withIcons($icons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,34 @@ final class PhabricatorProjectConfigOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOptions() {
|
public function getOptions() {
|
||||||
|
$default_icons = PhabricatorProjectIconSet::getDefaultConfiguration();
|
||||||
|
$icons_type = 'custom:PhabricatorProjectTypeConfigOptionType';
|
||||||
|
|
||||||
|
$icons_description = $this->deformat(pht(<<<EOTEXT
|
||||||
|
Allows you to change and customize the available project icons.
|
||||||
|
|
||||||
|
You can find a list of available icons in {nav UIExamples > 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(
|
$default_fields = array(
|
||||||
'std:project:internal:description' => true,
|
'std:project:internal:description' => true,
|
||||||
);
|
);
|
||||||
|
@ -45,6 +73,9 @@ final class PhabricatorProjectConfigOptions
|
||||||
$this->newOption('projects.fields', $custom_field_type, $default_fields)
|
$this->newOption('projects.fields', $custom_field_type, $default_fields)
|
||||||
->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass())
|
->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass())
|
||||||
->setDescription(pht('Select and reorder project fields.')),
|
->setDescription(pht('Select and reorder project fields.')),
|
||||||
|
$this->newOption('projects.icons', $icons_type, $default_icons)
|
||||||
|
->setSummary(pht('Adjust project icons.'))
|
||||||
|
->setDescription($icons_description),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorProjectTypeConfigOptionType
|
||||||
|
extends PhabricatorConfigJSONOptionType {
|
||||||
|
|
||||||
|
public function validateOption(PhabricatorConfigOption $option, $value) {
|
||||||
|
PhabricatorProjectIconSet::validateConfiguration($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -156,10 +156,10 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
||||||
$subprojects_icon = 'fa-sitemap grey';
|
$subprojects_icon = 'fa-sitemap grey';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($project->supportsMilestones()) {
|
$key = PhabricatorProjectIconSet::getMilestoneIconKey();
|
||||||
$milestones_icon = 'fa-map-marker';
|
$milestones_icon = PhabricatorProjectIconSet::getIconIcon($key);
|
||||||
} else {
|
if (!$project->supportsMilestones()) {
|
||||||
$milestones_icon = 'fa-map-marker grey';
|
$milestones_icon = "{$milestones_icon} grey";
|
||||||
}
|
}
|
||||||
|
|
||||||
$nav->addIcon(
|
$nav->addIcon(
|
||||||
|
|
|
@ -5,38 +5,121 @@ final class PhabricatorProjectIconSet
|
||||||
|
|
||||||
const ICONSETKEY = 'projects';
|
const ICONSETKEY = 'projects';
|
||||||
|
|
||||||
|
const SPECIAL_MILESTONE = 'milestone';
|
||||||
|
|
||||||
public function getSelectIconTitleText() {
|
public function getSelectIconTitleText() {
|
||||||
return pht('Choose Project Icon');
|
return pht('Choose Project Icon');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function newIcons() {
|
public static function getDefaultConfiguration() {
|
||||||
$map = array(
|
return array(
|
||||||
'fa-briefcase' => pht('Briefcase'),
|
array(
|
||||||
'fa-tags' => pht('Tag'),
|
'key' => 'project',
|
||||||
'fa-folder' => pht('Folder'),
|
'icon' => 'fa-briefcase',
|
||||||
'fa-users' => pht('Team'),
|
'name' => pht('Project'),
|
||||||
|
'default' => true,
|
||||||
'fa-bug' => pht('Bug'),
|
),
|
||||||
'fa-trash-o' => pht('Garbage'),
|
array(
|
||||||
'fa-calendar' => pht('Deadline'),
|
'key' => 'tag',
|
||||||
'fa-flag-checkered' => pht('Goal'),
|
'icon' => 'fa-tags',
|
||||||
|
'name' => pht('Tag'),
|
||||||
'fa-envelope' => pht('Communication'),
|
),
|
||||||
'fa-truck' => pht('Release'),
|
array(
|
||||||
'fa-lock' => pht('Policy'),
|
'key' => 'policy',
|
||||||
'fa-umbrella' => pht('An Umbrella'),
|
'icon' => 'fa-lock',
|
||||||
|
'name' => pht('Policy'),
|
||||||
'fa-cloud' => pht('The Cloud'),
|
),
|
||||||
'fa-building' => pht('Company'),
|
array(
|
||||||
'fa-credit-card' => pht('Accounting'),
|
'key' => 'group',
|
||||||
'fa-flask' => pht('Experimental'),
|
'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();
|
$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())
|
$icons[] = id(new PhabricatorIconSetIcon())
|
||||||
->setKey($key)
|
->setKey($spec['key'])
|
||||||
->setLabel($label);
|
->setIsDisabled(idx($spec, 'disabled'))
|
||||||
|
->setIcon($spec['icon'])
|
||||||
|
->setLabel($spec['name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $icons;
|
return $icons;
|
||||||
|
@ -52,4 +135,183 @@ final class PhabricatorProjectIconSet
|
||||||
return $shades;
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ final class PhabricatorProjectProjectPHIDType extends PhabricatorPHIDType {
|
||||||
}
|
}
|
||||||
|
|
||||||
$handle->setImageURI($project->getProfileImageURI());
|
$handle->setImageURI($project->getProfileImageURI());
|
||||||
$handle->setIcon($project->getDisplayIcon());
|
$handle->setIcon($project->getDisplayIconIcon());
|
||||||
$handle->setTagColor($project->getDisplayColor());
|
$handle->setTagColor($project->getDisplayColor());
|
||||||
|
|
||||||
if ($project->isArchived()) {
|
if ($project->isArchived()) {
|
||||||
|
|
|
@ -131,6 +131,10 @@ protected function buildQueryFromParameters(array $map) {
|
||||||
|
|
||||||
$set = new PhabricatorProjectIconSet();
|
$set = new PhabricatorProjectIconSet();
|
||||||
foreach ($set->getIcons() as $icon) {
|
foreach ($set->getIcons() as $icon) {
|
||||||
|
if ($icon->getIsDisabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$options[$icon->getKey()] = array(
|
$options[$icon->getKey()] = array(
|
||||||
id(new PHUIIconView())
|
id(new PHUIIconView())
|
||||||
->setIconFont($icon->getIcon()),
|
->setIconFont($icon->getIcon()),
|
||||||
|
|
|
@ -45,7 +45,6 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
private $slugs = self::ATTACHABLE;
|
private $slugs = self::ATTACHABLE;
|
||||||
private $parentProject = self::ATTACHABLE;
|
private $parentProject = self::ATTACHABLE;
|
||||||
|
|
||||||
const DEFAULT_ICON = 'fa-briefcase';
|
|
||||||
const DEFAULT_COLOR = 'blue';
|
const DEFAULT_COLOR = 'blue';
|
||||||
|
|
||||||
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
|
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
|
||||||
|
@ -63,9 +62,11 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
$join_policy = $app->getPolicy(
|
$join_policy = $app->getPolicy(
|
||||||
ProjectDefaultJoinCapability::CAPABILITY);
|
ProjectDefaultJoinCapability::CAPABILITY);
|
||||||
|
|
||||||
|
$default_icon = PhabricatorProjectIconSet::getDefaultIconKey();
|
||||||
|
|
||||||
return id(new PhabricatorProject())
|
return id(new PhabricatorProject())
|
||||||
->setAuthorPHID($actor->getPHID())
|
->setAuthorPHID($actor->getPHID())
|
||||||
->setIcon(self::DEFAULT_ICON)
|
->setIcon($default_icon)
|
||||||
->setColor(self::DEFAULT_COLOR)
|
->setColor(self::DEFAULT_COLOR)
|
||||||
->setViewPolicy($view_policy)
|
->setViewPolicy($view_policy)
|
||||||
->setEditPolicy($edit_policy)
|
->setEditPolicy($edit_policy)
|
||||||
|
@ -484,12 +485,24 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
return $number;
|
return $number;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDisplayIcon() {
|
public function getDisplayIconKey() {
|
||||||
if ($this->isMilestone()) {
|
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() {
|
public function getDisplayColor() {
|
||||||
|
@ -608,6 +621,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
->setKey('slug')
|
->setKey('slug')
|
||||||
->setType('string')
|
->setType('string')
|
||||||
->setDescription(pht('Primary slug/hashtag.')),
|
->setDescription(pht('Primary slug/hashtag.')),
|
||||||
|
id(new PhabricatorConduitSearchFieldSpecification())
|
||||||
|
->setKey('icon')
|
||||||
|
->setType('map<string, wild>')
|
||||||
|
->setDescription(pht('Information about the project icon.')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -615,6 +632,11 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
return array(
|
return array(
|
||||||
'name' => $this->getName(),
|
'name' => $this->getName(),
|
||||||
'slug' => $this->getPrimarySlug(),
|
'slug' => $this->getPrimarySlug(),
|
||||||
|
'icon' => array(
|
||||||
|
'key' => $this->getDisplayIconKey(),
|
||||||
|
'name' => $this->getDisplayIconName(),
|
||||||
|
'icon' => $this->getDisplayIconIcon(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,15 +25,24 @@ final class PhabricatorProjectListView extends AphrontView {
|
||||||
foreach ($projects as $key => $project) {
|
foreach ($projects as $key => $project) {
|
||||||
$id = $project->getID();
|
$id = $project->getID();
|
||||||
|
|
||||||
$tag_list = id(new PHUIHandleTagListView())
|
$icon = $project->getDisplayIconIcon();
|
||||||
->setSlim(true)
|
$color = $project->getColor();
|
||||||
->setHandles(array($handles[$project->getPHID()]));
|
|
||||||
|
$icon_icon = id(new PHUIIconView())
|
||||||
|
->setIconFont("{$icon} {$color}");
|
||||||
|
|
||||||
|
$icon_name = $project->getDisplayIconName();
|
||||||
|
|
||||||
$item = id(new PHUIObjectItemView())
|
$item = id(new PHUIObjectItemView())
|
||||||
->setHeader($project->getName())
|
->setHeader($project->getName())
|
||||||
->setHref("/project/view/{$id}/")
|
->setHref("/project/view/{$id}/")
|
||||||
->setImageURI($project->getProfileImageURI())
|
->setImageURI($project->getProfileImageURI())
|
||||||
->addAttribute($tag_list);
|
->addAttribute(
|
||||||
|
array(
|
||||||
|
$icon_icon,
|
||||||
|
' ',
|
||||||
|
$icon_name,
|
||||||
|
));
|
||||||
|
|
||||||
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) {
|
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) {
|
||||||
$item->addIcon('delete-grey', pht('Archived'));
|
$item->addIcon('delete-grey', pht('Archived'));
|
||||||
|
|
|
@ -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};
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* @provides project-icon-css
|
* @provides phui-icon-set-selector-css
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button.icon-button {
|
button.icon-button {
|
||||||
|
@ -25,4 +25,5 @@ button.icon-button {
|
||||||
|
|
||||||
button.icon-button.selected {
|
button.icon-button.selected {
|
||||||
background: {$bluebackground};
|
background: {$bluebackground};
|
||||||
|
border: 1px solid {$blueborder};
|
||||||
}
|
}
|
|
@ -22,7 +22,7 @@ JX.behavior('choose-control', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
value: input.value
|
icon: input.value
|
||||||
};
|
};
|
||||||
|
|
||||||
new JX.Workflow(data.uri, params)
|
new JX.Workflow(data.uri, params)
|
||||||
|
|
Loading…
Reference in a new issue