1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-22 13:30:55 +01:00

De-garbage the horrible garbage project section of the policy selection control

Summary:
Fixes T4136.

When listing projects in the "Visible To" selector control:

  - Instead of showing every project you are a member of, show only a few.
  - Add an option to choose something else which isn't in the menu.
  - If you've used the control before, show the stuff you've selected in the recent past.
  - If you haven't used the control before or haven't used it much, show the stuff you've picked and them some filler.
  - Don't offer milestones.
  - Also don't offer milestones in the custom policy UI.

Test Plan:
{F1091999}

{F1092000}

  - Selected a project.
  - Used "find" to select a different project.
  - Saw reasonable defaults.
  - Saw favorites stick.
  - Tried to typeahead a milestone (nope).
  - Used "Custom Policy", tried to typeahead a milestone (nope).
  - Used "Custom Policy" in general.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T4136

Differential Revision: https://secure.phabricator.com/D15184
This commit is contained in:
epriestley 2016-02-04 09:43:40 -08:00
parent 8a9f760975
commit e1c934ab22
7 changed files with 224 additions and 22 deletions

View file

@ -6,7 +6,6 @@ final class PhabricatorPolicyEditController
public function handleRequest(AphrontRequest $request) { public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$object_phid = $request->getURIData('objectPHID'); $object_phid = $request->getURIData('objectPHID');
if ($object_phid) { if ($object_phid) {
$object = id(new PhabricatorObjectQuery()) $object = id(new PhabricatorObjectQuery())
@ -32,6 +31,17 @@ final class PhabricatorPolicyEditController
} }
} }
$phid = $request->getURIData('phid');
switch ($phid) {
case AphrontFormPolicyControl::getSelectProjectKey():
return $this->handleProjectRequest($request);
case AphrontFormPolicyControl::getSelectCustomKey():
$phid = null;
break;
default:
break;
}
$action_options = array( $action_options = array(
PhabricatorPolicy::ACTION_ALLOW => pht('Allow'), PhabricatorPolicy::ACTION_ALLOW => pht('Allow'),
PhabricatorPolicy::ACTION_DENY => pht('Deny'), PhabricatorPolicy::ACTION_DENY => pht('Deny'),
@ -55,7 +65,6 @@ final class PhabricatorPolicyEditController
'value' => null, 'value' => null,
); );
$phid = $request->getURIData('phid');
if ($phid) { if ($phid) {
$policies = id(new PhabricatorPolicyQuery()) $policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer) ->setViewer($viewer)
@ -253,4 +262,79 @@ final class PhabricatorPolicyEditController
return id(new AphrontDialogResponse())->setDialog($dialog); return id(new AphrontDialogResponse())->setDialog($dialog);
} }
private function handleProjectRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$errors = array();
$e_project = true;
if ($request->isFormPost()) {
$project_phids = $request->getArr('projectPHIDs');
$project_phid = head($project_phids);
$project = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($project_phid))
->executeOne();
if ($project) {
// Save this project as one of the user's most recently used projects,
// so we'll show it by default in future menus.
$pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES;
$preferences = $viewer->loadPreferences();
$favorites = $preferences->getPreference($pref_key);
if (!is_array($favorites)) {
$favorites = array();
}
// Add this, or move it to the end of the list.
unset($favorites[$project_phid]);
$favorites[$project_phid] = true;
$preferences->setPreference($pref_key, $favorites);
$preferences->save();
$data = array(
'phid' => $project->getPHID(),
'info' => array(
'name' => $project->getName(),
'full' => $project->getName(),
'icon' => $project->getDisplayIconIcon(),
),
);
return id(new AphrontAjaxResponse())->setContent($data);
} else {
$errors[] = pht('You must choose a project.');
$e_project = pht('Required');
}
}
$project_datasource = id(new PhabricatorProjectDatasource())
->setParameters(
array(
'policy' => 1,
));
$form = id(new AphrontFormView())
->setUser($viewer)
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Members Of'))
->setName('projectPHIDs')
->setLimit(1)
->setError($e_project)
->setDatasource($project_datasource));
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->setTitle(pht('Select Project'))
->appendForm($form)
->addSubmitButton(pht('Done'))
->addCancelButton('#');
}
} }

View file

@ -195,10 +195,48 @@ final class PhabricatorPolicyQuery
$viewer = $this->getViewer(); $viewer = $this->getViewer();
if ($viewer->getPHID()) { if ($viewer->getPHID()) {
$pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES;
$favorite_limit = 10;
$default_limit = 5;
// If possible, show the user's 10 most recently used projects.
$preferences = $viewer->loadPreferences();
$favorites = $preferences->getPreference($pref_key);
if (!is_array($favorites)) {
$favorites = array();
}
$favorite_phids = array_keys($favorites);
$favorite_phids = array_slice($favorite_phids, -$favorite_limit);
if ($favorite_phids) {
$projects = id(new PhabricatorProjectQuery()) $projects = id(new PhabricatorProjectQuery())
->setViewer($viewer) ->setViewer($viewer)
->withMemberPHIDs(array($viewer->getPHID())) ->withPHIDs($favorite_phids)
->withIsMilestone(false)
->setLimit($favorite_limit)
->execute(); ->execute();
$projects = mpull($projects, null, 'getPHID');
} else {
$projects = array();
}
// If we didn't find enough favorites, add some default projects. These
// are just arbitrary projects that the viewer is a member of, but may
// be useful on smaller installs and for new users until they can use
// the control enough time to establish useful favorites.
if (count($projects) < $default_limit) {
$default_projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($viewer->getPHID()))
->withIsMilestone(false)
->setLimit($default_limit)
->execute();
$default_projects = mpull($default_projects, null, 'getPHID');
$projects = $projects + $default_projects;
$projects = array_slice($projects, 0, $default_limit);
}
foreach ($projects as $project) { foreach ($projects as $project) {
$phids[] = $project->getPHID(); $phids[] = $project->getPHID();
} }

View file

@ -48,7 +48,13 @@ final class PhabricatorProjectsPolicyRule
} }
public function getValueControlTemplate() { public function getValueControlTemplate() {
return $this->getDatasourceTemplate(new PhabricatorProjectDatasource()); $datasource = id(new PhabricatorProjectDatasource())
->setParameters(
array(
'policy' => 1,
));
return $this->getDatasourceTemplate($datasource);
} }
public function getRuleOrder() { public function getRuleOrder() {

View file

@ -32,6 +32,12 @@ final class PhabricatorProjectDatasource
$query->withNameTokens($tokens); $query->withNameTokens($tokens);
} }
// If this is for policy selection, prevent users from using milestones.
$for_policy = $this->getParameter('policy');
if ($for_policy) {
$query->withIsMilestone(false);
}
$projs = $this->executeQuery($query); $projs = $this->executeQuery($query);
$projs = mpull($projs, null, 'getPHID'); $projs = mpull($projs, null, 'getPHID');

View file

@ -42,6 +42,7 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO {
const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications'; const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications';
const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed'; const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed';
const PREFERENCE_FAVORITE_POLICIES = 'policy.favorites';
// These are in an unusual order for historic reasons. // These are in an unusual order for historic reasons.
const MAILTAG_PREFERENCE_NOTIFY = 0; const MAILTAG_PREFERENCE_NOTIFY = 0;

View file

@ -103,6 +103,24 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
protected function getOptions() { protected function getOptions() {
$capability = $this->capability; $capability = $this->capability;
$policies = $this->policies; $policies = $this->policies;
$viewer = $this->getUser();
// Check if we're missing the policy for the current control value. This
// is unusual, but can occur if the user is submitting a form and selected
// an unusual project as a policy but the change has not been saved yet.
$policy_map = mpull($policies, null, 'getPHID');
$value = $this->getValue();
if ($value && empty($policy_map[$value])) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($value))
->executeOne();
if ($handle->isComplete()) {
$policies[] = PhabricatorPolicy::newFromPolicyAndHandle(
$value,
$handle);
}
}
// Exclude object policies which don't make sense here. This primarily // Exclude object policies which don't make sense here. This primarily
// filters object policies associated from template capabilities (like // filters object policies associated from template capabilities (like
@ -143,12 +161,27 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
'name' => $policy_short_name, 'name' => $policy_short_name,
'full' => $policy->getName(), 'full' => $policy->getName(),
'icon' => $policy->getIcon(), 'icon' => $policy->getIcon(),
'sort' => phutil_utf8_strtolower($policy->getName()),
); );
} }
$type_project = PhabricatorPolicyType::TYPE_PROJECT;
$placeholder = id(new PhabricatorPolicy())
->setName(pht('Other Project...'))
->setIcon('fa-search');
$options[$type_project] = isort($options[$type_project], 'sort');
$options[$type_project][$this->getSelectProjectKey()] = array(
'name' => $placeholder->getName(),
'full' => $placeholder->getName(),
'icon' => $placeholder->getIcon(),
);
// If we were passed several custom policy options, throw away the ones // If we were passed several custom policy options, throw away the ones
// which aren't the value for this capability. For example, an object might // which aren't the value for this capability. For example, an object might
// have a custom view pollicy and a custom edit policy. When we render // have a custom view policy and a custom edit policy. When we render
// the selector for "Can View", we don't want to show the "Can Edit" // the selector for "Can View", we don't want to show the "Can Edit"
// custom policy -- if we did, the menu would look like this: // custom policy -- if we did, the menu would look like this:
// //
@ -172,7 +205,7 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
if (empty($options[$type_custom])) { if (empty($options[$type_custom])) {
$placeholder = new PhabricatorPolicy(); $placeholder = new PhabricatorPolicy();
$placeholder->setName(pht('Custom Policy...')); $placeholder->setName(pht('Custom Policy...'));
$options[$type_custom][$this->getCustomPolicyPlaceholder()] = array( $options[$type_custom][$this->getSelectCustomKey()] = array(
'name' => $placeholder->getName(), 'name' => $placeholder->getName(),
'full' => $placeholder->getName(), 'full' => $placeholder->getName(),
'icon' => $placeholder->getIcon(), 'icon' => $placeholder->getIcon(),
@ -266,12 +299,12 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
'options' => $flat_options, 'options' => $flat_options,
'groups' => array_keys($options), 'groups' => array_keys($options),
'order' => $order, 'order' => $order,
'icons' => $icons,
'labels' => $labels, 'labels' => $labels,
'value' => $this->getValue(), 'value' => $this->getValue(),
'capability' => $this->capability, 'capability' => $this->capability,
'editURI' => '/policy/edit/'.$context_path, 'editURI' => '/policy/edit/'.$context_path,
'customPlaceholder' => $this->getCustomPolicyPlaceholder(), 'customKey' => $this->getSelectCustomKey(),
'projectKey' => $this->getSelectProjectKey(),
'disabled' => $this->getDisabled(), 'disabled' => $this->getDisabled(),
)); ));
@ -322,8 +355,12 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
)); ));
} }
private function getCustomPolicyPlaceholder() { public static function getSelectCustomKey() {
return 'custom:placeholder'; return 'select:custom';
}
public static function getSelectProjectKey() {
return 'select:project';
} }
private function buildSpacesControl() { private function buildSpacesControl() {

View file

@ -7,6 +7,7 @@
* phuix-action-list-view * phuix-action-list-view
* phuix-action-view * phuix-action-view
* javelin-workflow * javelin-workflow
* phuix-icon-view
* @javelin * @javelin
*/ */
JX.behavior('policy-control', function(config) { JX.behavior('policy-control', function(config) {
@ -57,6 +58,21 @@ JX.behavior('policy-control', function(config) {
.start(); .start();
}, phid); }, phid);
} else if (phid == config.projectKey) {
onselect = JX.bind(null, function(phid) {
var uri = get_custom_uri(phid, config.capability);
new JX.Workflow(uri)
.setHandler(function(response) {
if (!response.phid) {
return;
}
add_policy(phid, response.phid, response.info);
select_policy(response.phid);
})
.start();
}, phid);
} else { } else {
onselect = JX.bind(null, select_policy, phid); onselect = JX.bind(null, select_policy, phid);
} }
@ -101,20 +117,22 @@ JX.behavior('policy-control', function(config) {
name = JX.$N('span', {title: option.full}, name); name = JX.$N('span', {title: option.full}, name);
} }
return [JX.$H(config.icons[option.icon]), name]; return [render_icon(option.icon), name];
}; };
var render_icon = function(icon) {
return new JX.PHUIXIconView()
.setIcon(icon)
.getNode();
};
/** /**
* Get the workflow URI to create or edit a policy with a given PHID. * Get the workflow URI to create or edit a policy with a given PHID.
*/ */
var get_custom_uri = function(phid, capability) { var get_custom_uri = function(phid, capability) {
var uri = config.editURI; return JX.$U(config.editURI + phid + '/')
if (phid != config.customPlaceholder) { .setQueryParam('capability', capability)
uri += phid + '/'; .toString();
}
uri += '?capability=' + capability;
return uri;
}; };
@ -123,16 +141,28 @@ JX.behavior('policy-control', function(config) {
* policies after the user edits them. * policies after the user edits them.
*/ */
var replace_policy = function(old_phid, new_phid, info) { var replace_policy = function(old_phid, new_phid, info) {
return add_policy(old_phid, new_phid, info, true);
};
/**
* Add a new policy above an existing one, optionally replacing it.
*/
var add_policy = function(old_phid, new_phid, info, replace) {
if (config.options[new_phid]) {
return;
}
config.options[new_phid] = info; config.options[new_phid] = info;
for (var k in config.order) { for (var k in config.order) {
for (var ii = 0; ii < config.order[k].length; ii++) { for (var ii = 0; ii < config.order[k].length; ii++) {
if (config.order[k][ii] == old_phid) { if (config.order[k][ii] == old_phid) {
config.order[k][ii] = new_phid; config.order[k].splice(ii, (replace ? 1 : 0), new_phid);
return; return;
} }
} }
} }
}; };
}); });