@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+224 -22
+86 -2
src/applications/policy/controller/PhabricatorPolicyEditController.php
··· 6 6 public function handleRequest(AphrontRequest $request) { 7 7 $viewer = $this->getViewer(); 8 8 9 - 10 9 $object_phid = $request->getURIData('objectPHID'); 11 10 if ($object_phid) { 12 11 $object = id(new PhabricatorObjectQuery()) ··· 32 31 } 33 32 } 34 33 34 + $phid = $request->getURIData('phid'); 35 + switch ($phid) { 36 + case AphrontFormPolicyControl::getSelectProjectKey(): 37 + return $this->handleProjectRequest($request); 38 + case AphrontFormPolicyControl::getSelectCustomKey(): 39 + $phid = null; 40 + break; 41 + default: 42 + break; 43 + } 44 + 35 45 $action_options = array( 36 46 PhabricatorPolicy::ACTION_ALLOW => pht('Allow'), 37 47 PhabricatorPolicy::ACTION_DENY => pht('Deny'), ··· 55 65 'value' => null, 56 66 ); 57 67 58 - $phid = $request->getURIData('phid'); 59 68 if ($phid) { 60 69 $policies = id(new PhabricatorPolicyQuery()) 61 70 ->setViewer($viewer) ··· 251 260 ->addCancelButton('#'); 252 261 253 262 return id(new AphrontDialogResponse())->setDialog($dialog); 263 + } 264 + 265 + private function handleProjectRequest(AphrontRequest $request) { 266 + $viewer = $this->getViewer(); 267 + 268 + $errors = array(); 269 + $e_project = true; 270 + 271 + if ($request->isFormPost()) { 272 + $project_phids = $request->getArr('projectPHIDs'); 273 + $project_phid = head($project_phids); 274 + 275 + $project = id(new PhabricatorObjectQuery()) 276 + ->setViewer($viewer) 277 + ->withPHIDs(array($project_phid)) 278 + ->executeOne(); 279 + 280 + if ($project) { 281 + // Save this project as one of the user's most recently used projects, 282 + // so we'll show it by default in future menus. 283 + 284 + $pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES; 285 + 286 + $preferences = $viewer->loadPreferences(); 287 + $favorites = $preferences->getPreference($pref_key); 288 + if (!is_array($favorites)) { 289 + $favorites = array(); 290 + } 291 + 292 + // Add this, or move it to the end of the list. 293 + unset($favorites[$project_phid]); 294 + $favorites[$project_phid] = true; 295 + 296 + $preferences->setPreference($pref_key, $favorites); 297 + $preferences->save(); 298 + 299 + $data = array( 300 + 'phid' => $project->getPHID(), 301 + 'info' => array( 302 + 'name' => $project->getName(), 303 + 'full' => $project->getName(), 304 + 'icon' => $project->getDisplayIconIcon(), 305 + ), 306 + ); 307 + 308 + return id(new AphrontAjaxResponse())->setContent($data); 309 + } else { 310 + $errors[] = pht('You must choose a project.'); 311 + $e_project = pht('Required'); 312 + } 313 + } 314 + 315 + $project_datasource = id(new PhabricatorProjectDatasource()) 316 + ->setParameters( 317 + array( 318 + 'policy' => 1, 319 + )); 320 + 321 + $form = id(new AphrontFormView()) 322 + ->setUser($viewer) 323 + ->appendControl( 324 + id(new AphrontFormTokenizerControl()) 325 + ->setLabel(pht('Members Of')) 326 + ->setName('projectPHIDs') 327 + ->setLimit(1) 328 + ->setError($e_project) 329 + ->setDatasource($project_datasource)); 330 + 331 + return $this->newDialog() 332 + ->setWidth(AphrontDialogView::WIDTH_FORM) 333 + ->setErrors($errors) 334 + ->setTitle(pht('Select Project')) 335 + ->appendForm($form) 336 + ->addSubmitButton(pht('Done')) 337 + ->addCancelButton('#'); 254 338 } 255 339 256 340 }
+42 -4
src/applications/policy/query/PhabricatorPolicyQuery.php
··· 195 195 $viewer = $this->getViewer(); 196 196 197 197 if ($viewer->getPHID()) { 198 - $projects = id(new PhabricatorProjectQuery()) 199 - ->setViewer($viewer) 200 - ->withMemberPHIDs(array($viewer->getPHID())) 201 - ->execute(); 198 + $pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES; 199 + 200 + $favorite_limit = 10; 201 + $default_limit = 5; 202 + 203 + // If possible, show the user's 10 most recently used projects. 204 + $preferences = $viewer->loadPreferences(); 205 + $favorites = $preferences->getPreference($pref_key); 206 + if (!is_array($favorites)) { 207 + $favorites = array(); 208 + } 209 + $favorite_phids = array_keys($favorites); 210 + $favorite_phids = array_slice($favorite_phids, -$favorite_limit); 211 + 212 + if ($favorite_phids) { 213 + $projects = id(new PhabricatorProjectQuery()) 214 + ->setViewer($viewer) 215 + ->withPHIDs($favorite_phids) 216 + ->withIsMilestone(false) 217 + ->setLimit($favorite_limit) 218 + ->execute(); 219 + $projects = mpull($projects, null, 'getPHID'); 220 + } else { 221 + $projects = array(); 222 + } 223 + 224 + // If we didn't find enough favorites, add some default projects. These 225 + // are just arbitrary projects that the viewer is a member of, but may 226 + // be useful on smaller installs and for new users until they can use 227 + // the control enough time to establish useful favorites. 228 + if (count($projects) < $default_limit) { 229 + $default_projects = id(new PhabricatorProjectQuery()) 230 + ->setViewer($viewer) 231 + ->withMemberPHIDs(array($viewer->getPHID())) 232 + ->withIsMilestone(false) 233 + ->setLimit($default_limit) 234 + ->execute(); 235 + $default_projects = mpull($default_projects, null, 'getPHID'); 236 + $projects = $projects + $default_projects; 237 + $projects = array_slice($projects, 0, $default_limit); 238 + } 239 + 202 240 foreach ($projects as $project) { 203 241 $phids[] = $project->getPHID(); 204 242 }
+7 -1
src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php
··· 48 48 } 49 49 50 50 public function getValueControlTemplate() { 51 - return $this->getDatasourceTemplate(new PhabricatorProjectDatasource()); 51 + $datasource = id(new PhabricatorProjectDatasource()) 52 + ->setParameters( 53 + array( 54 + 'policy' => 1, 55 + )); 56 + 57 + return $this->getDatasourceTemplate($datasource); 52 58 } 53 59 54 60 public function getRuleOrder() {
+6
src/applications/project/typeahead/PhabricatorProjectDatasource.php
··· 32 32 $query->withNameTokens($tokens); 33 33 } 34 34 35 + // If this is for policy selection, prevent users from using milestones. 36 + $for_policy = $this->getParameter('policy'); 37 + if ($for_policy) { 38 + $query->withIsMilestone(false); 39 + } 40 + 35 41 $projs = $this->executeQuery($query); 36 42 37 43 $projs = mpull($projs, null, 'getPHID');
+1
src/applications/settings/storage/PhabricatorUserPreferences.php
··· 42 42 const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications'; 43 43 44 44 const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed'; 45 + const PREFERENCE_FAVORITE_POLICIES = 'policy.favorites'; 45 46 46 47 // These are in an unusual order for historic reasons. 47 48 const MAILTAG_PREFERENCE_NOTIFY = 0;
+43 -6
src/view/form/control/AphrontFormPolicyControl.php
··· 103 103 protected function getOptions() { 104 104 $capability = $this->capability; 105 105 $policies = $this->policies; 106 + $viewer = $this->getUser(); 107 + 108 + // Check if we're missing the policy for the current control value. This 109 + // is unusual, but can occur if the user is submitting a form and selected 110 + // an unusual project as a policy but the change has not been saved yet. 111 + $policy_map = mpull($policies, null, 'getPHID'); 112 + $value = $this->getValue(); 113 + if ($value && empty($policy_map[$value])) { 114 + $handle = id(new PhabricatorHandleQuery()) 115 + ->setViewer($viewer) 116 + ->withPHIDs(array($value)) 117 + ->executeOne(); 118 + if ($handle->isComplete()) { 119 + $policies[] = PhabricatorPolicy::newFromPolicyAndHandle( 120 + $value, 121 + $handle); 122 + } 123 + } 106 124 107 125 // Exclude object policies which don't make sense here. This primarily 108 126 // filters object policies associated from template capabilities (like ··· 143 161 'name' => $policy_short_name, 144 162 'full' => $policy->getName(), 145 163 'icon' => $policy->getIcon(), 164 + 'sort' => phutil_utf8_strtolower($policy->getName()), 146 165 ); 147 166 } 148 167 168 + $type_project = PhabricatorPolicyType::TYPE_PROJECT; 169 + 170 + $placeholder = id(new PhabricatorPolicy()) 171 + ->setName(pht('Other Project...')) 172 + ->setIcon('fa-search'); 173 + 174 + $options[$type_project] = isort($options[$type_project], 'sort'); 175 + 176 + $options[$type_project][$this->getSelectProjectKey()] = array( 177 + 'name' => $placeholder->getName(), 178 + 'full' => $placeholder->getName(), 179 + 'icon' => $placeholder->getIcon(), 180 + ); 181 + 149 182 // If we were passed several custom policy options, throw away the ones 150 183 // which aren't the value for this capability. For example, an object might 151 - // have a custom view pollicy and a custom edit policy. When we render 184 + // have a custom view policy and a custom edit policy. When we render 152 185 // the selector for "Can View", we don't want to show the "Can Edit" 153 186 // custom policy -- if we did, the menu would look like this: 154 187 // ··· 172 205 if (empty($options[$type_custom])) { 173 206 $placeholder = new PhabricatorPolicy(); 174 207 $placeholder->setName(pht('Custom Policy...')); 175 - $options[$type_custom][$this->getCustomPolicyPlaceholder()] = array( 208 + $options[$type_custom][$this->getSelectCustomKey()] = array( 176 209 'name' => $placeholder->getName(), 177 210 'full' => $placeholder->getName(), 178 211 'icon' => $placeholder->getIcon(), ··· 266 299 'options' => $flat_options, 267 300 'groups' => array_keys($options), 268 301 'order' => $order, 269 - 'icons' => $icons, 270 302 'labels' => $labels, 271 303 'value' => $this->getValue(), 272 304 'capability' => $this->capability, 273 305 'editURI' => '/policy/edit/'.$context_path, 274 - 'customPlaceholder' => $this->getCustomPolicyPlaceholder(), 306 + 'customKey' => $this->getSelectCustomKey(), 307 + 'projectKey' => $this->getSelectProjectKey(), 275 308 'disabled' => $this->getDisabled(), 276 309 )); 277 310 ··· 322 355 )); 323 356 } 324 357 325 - private function getCustomPolicyPlaceholder() { 326 - return 'custom:placeholder'; 358 + public static function getSelectCustomKey() { 359 + return 'select:custom'; 360 + } 361 + 362 + public static function getSelectProjectKey() { 363 + return 'select:project'; 327 364 } 328 365 329 366 private function buildSpacesControl() {
+39 -9
webroot/rsrc/js/application/policy/behavior-policy-control.js
··· 7 7 * phuix-action-list-view 8 8 * phuix-action-view 9 9 * javelin-workflow 10 + * phuix-icon-view 10 11 * @javelin 11 12 */ 12 13 JX.behavior('policy-control', function(config) { ··· 57 58 .start(); 58 59 59 60 }, phid); 61 + } else if (phid == config.projectKey) { 62 + onselect = JX.bind(null, function(phid) { 63 + var uri = get_custom_uri(phid, config.capability); 64 + 65 + new JX.Workflow(uri) 66 + .setHandler(function(response) { 67 + if (!response.phid) { 68 + return; 69 + } 70 + 71 + add_policy(phid, response.phid, response.info); 72 + select_policy(response.phid); 73 + }) 74 + .start(); 75 + }, phid); 60 76 } else { 61 77 onselect = JX.bind(null, select_policy, phid); 62 78 } ··· 101 117 name = JX.$N('span', {title: option.full}, name); 102 118 } 103 119 104 - return [JX.$H(config.icons[option.icon]), name]; 120 + return [render_icon(option.icon), name]; 105 121 }; 106 122 123 + var render_icon = function(icon) { 124 + return new JX.PHUIXIconView() 125 + .setIcon(icon) 126 + .getNode(); 127 + }; 107 128 108 129 /** 109 130 * Get the workflow URI to create or edit a policy with a given PHID. 110 131 */ 111 132 var get_custom_uri = function(phid, capability) { 112 - var uri = config.editURI; 113 - if (phid != config.customPlaceholder) { 114 - uri += phid + '/'; 115 - } 116 - uri += '?capability=' + capability; 117 - return uri; 133 + return JX.$U(config.editURI + phid + '/') 134 + .setQueryParam('capability', capability) 135 + .toString(); 118 136 }; 119 137 120 138 ··· 123 141 * policies after the user edits them. 124 142 */ 125 143 var replace_policy = function(old_phid, new_phid, info) { 144 + return add_policy(old_phid, new_phid, info, true); 145 + }; 146 + 147 + 148 + /** 149 + * Add a new policy above an existing one, optionally replacing it. 150 + */ 151 + var add_policy = function(old_phid, new_phid, info, replace) { 152 + if (config.options[new_phid]) { 153 + return; 154 + } 155 + 126 156 config.options[new_phid] = info; 157 + 127 158 for (var k in config.order) { 128 159 for (var ii = 0; ii < config.order[k].length; ii++) { 129 160 if (config.order[k][ii] == old_phid) { 130 - config.order[k][ii] = new_phid; 161 + config.order[k].splice(ii, (replace ? 1 : 0), new_phid); 131 162 return; 132 163 } 133 164 } 134 165 } 135 166 }; 136 - 137 167 138 168 });