@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.

Move member/watch actions to "Members/Watchers" page

Summary:
Ref T10054. This tries to make the members page a bit more consistent and provide hints to users about subproject/milestone membership rules. In particular:

- You now join, leave, watch, unwatch, add and remove members, and lock and unlock membership from the members screen.
- We now explain the membership rule for the project on this screen. There are currently four rules:
- Normal Project: Join/leave normally.
- Parent Project: Uses subprojects to determine members.
- Milestone: Uses parent project to determine members.
- Locked: Membership is locked.
- (Future) Imported from LDAP/other external sources: Membership is determined by something else.

Test Plan: {F1064878}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10054

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

+500 -285
+3 -3
resources/celerity/map.php
··· 7 7 */ 8 8 return array( 9 9 'names' => array( 10 - 'core.pkg.css' => '7fce81fc', 10 + 'core.pkg.css' => 'bd4f3259', 11 11 'core.pkg.js' => '573e6664', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => '2de124c9', ··· 143 143 'rsrc/css/phui/phui-object-item-list-view.css' => '26c30d3f', 144 144 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 145 145 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 146 - 'rsrc/css/phui/phui-profile-menu.css' => '72d69773', 146 + 'rsrc/css/phui/phui-profile-menu.css' => '84966ae9', 147 147 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 148 148 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 149 149 'rsrc/css/phui/phui-spacing.css' => '042804d6', ··· 819 819 'phui-object-item-list-view-css' => '26c30d3f', 820 820 'phui-pager-css' => 'bea33d23', 821 821 'phui-pinboard-view-css' => '2495140e', 822 - 'phui-profile-menu-css' => '72d69773', 822 + 'phui-profile-menu-css' => '84966ae9', 823 823 'phui-property-list-view-css' => '27b2849e', 824 824 'phui-remarkup-preview-css' => '1a8f2591', 825 825 'phui-spacing-css' => '042804d6',
+10 -2
src/__phutil_library_map__.php
··· 2895 2895 'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php', 2896 2896 'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php', 2897 2897 'PhabricatorProjectMaterializedMemberEdgeType' => 'applications/project/edge/PhabricatorProjectMaterializedMemberEdgeType.php', 2898 + 'PhabricatorProjectMemberListView' => 'applications/project/view/PhabricatorProjectMemberListView.php', 2898 2899 'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php', 2900 + 'PhabricatorProjectMembersAddController' => 'applications/project/controller/PhabricatorProjectMembersAddController.php', 2899 2901 'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php', 2900 - 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 2901 2902 'PhabricatorProjectMembersPolicyRule' => 'applications/project/policyrule/PhabricatorProjectMembersPolicyRule.php', 2902 2903 'PhabricatorProjectMembersProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php', 2903 2904 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 2905 + 'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php', 2904 2906 'PhabricatorProjectMilestonesController' => 'applications/project/controller/PhabricatorProjectMilestonesController.php', 2905 2907 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', 2906 2908 'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php', ··· 2932 2934 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 2933 2935 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 2934 2936 'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php', 2937 + 'PhabricatorProjectUserListView' => 'applications/project/view/PhabricatorProjectUserListView.php', 2935 2938 'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php', 2936 2939 'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php', 2940 + 'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php', 2937 2941 'PhabricatorProjectWorkboardProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php', 2938 2942 'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php', 2939 2943 'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php', ··· 7292 7296 'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7293 7297 'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource', 7294 7298 'PhabricatorProjectMaterializedMemberEdgeType' => 'PhabricatorEdgeType', 7299 + 'PhabricatorProjectMemberListView' => 'PhabricatorProjectUserListView', 7295 7300 'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType', 7301 + 'PhabricatorProjectMembersAddController' => 'PhabricatorProjectController', 7296 7302 'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7297 - 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 7298 7303 'PhabricatorProjectMembersPolicyRule' => 'PhabricatorPolicyRule', 7299 7304 'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel', 7300 7305 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 7306 + 'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController', 7301 7307 'PhabricatorProjectMilestonesController' => 'PhabricatorProjectController', 7302 7308 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 7303 7309 'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar', ··· 7332 7338 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 7333 7339 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 7334 7340 'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7341 + 'PhabricatorProjectUserListView' => 'AphrontView', 7335 7342 'PhabricatorProjectViewController' => 'PhabricatorProjectController', 7336 7343 'PhabricatorProjectWatchController' => 'PhabricatorProjectController', 7344 + 'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView', 7337 7345 'PhabricatorProjectWorkboardProfilePanel' => 'PhabricatorProfilePanel', 7338 7346 'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension', 7339 7347 'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField',
+3 -1
src/applications/project/application/PhabricatorProjectApplication.php
··· 48 48 'lock/(?P<id>[1-9]\d*)/' 49 49 => 'PhabricatorProjectLockController', 50 50 'members/(?P<id>[1-9]\d*)/' 51 - => 'PhabricatorProjectMembersEditController', 51 + => 'PhabricatorProjectMembersViewController', 52 + 'members/(?P<id>[1-9]\d*)/add/' 53 + => 'PhabricatorProjectMembersAddController', 52 54 'members/(?P<id>[1-9]\d*)/remove/' 53 55 => 'PhabricatorProjectMembersRemoveController', 54 56 'profile/(?P<id>[1-9]\d*)/'
+10 -1
src/applications/project/controller/PhabricatorProjectLockController.php
··· 27 27 return new Aphront404Response(); 28 28 } 29 29 30 - $done_uri = $project->getURI(); 30 + $done_uri = "/project/members/{$id}/"; 31 + 32 + if (!$project->supportsEditMembers()) { 33 + return $this->newDialog() 34 + ->setTitle(pht('Membership Immutable')) 35 + ->appendChild( 36 + pht('This project does not support editing membership.')) 37 + ->addCancelButton($done_uri); 38 + } 39 + 31 40 $is_locked = $project->getIsMembershipLocked(); 32 41 33 42 if ($request->isFormPost()) {
+72
src/applications/project/controller/PhabricatorProjectMembersAddController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectMembersAddController 4 + extends PhabricatorProjectController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + $id = $request->getURIData('id'); 9 + 10 + $project = id(new PhabricatorProjectQuery()) 11 + ->setViewer($viewer) 12 + ->withIDs(array($id)) 13 + ->requireCapabilities( 14 + array( 15 + PhabricatorPolicyCapability::CAN_VIEW, 16 + PhabricatorPolicyCapability::CAN_EDIT, 17 + )) 18 + ->executeOne(); 19 + if (!$project) { 20 + return new Aphront404Response(); 21 + } 22 + 23 + $this->setProject($project); 24 + 25 + if (!$project->supportsEditMembers()) { 26 + return new Aphront404Response(); 27 + } 28 + 29 + $done_uri = "/project/members/{$id}/"; 30 + 31 + if ($request->isFormPost()) { 32 + $member_phids = $request->getArr('memberPHIDs'); 33 + 34 + $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 35 + 36 + $xactions = array(); 37 + 38 + $xactions[] = id(new PhabricatorProjectTransaction()) 39 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 40 + ->setMetadataValue('edge:type', $type_member) 41 + ->setNewValue( 42 + array( 43 + '+' => array_fuse($member_phids), 44 + )); 45 + 46 + $editor = id(new PhabricatorProjectTransactionEditor($project)) 47 + ->setActor($viewer) 48 + ->setContentSourceFromRequest($request) 49 + ->setContinueOnNoEffect(true) 50 + ->setContinueOnMissingFields(true) 51 + ->applyTransactions($project, $xactions); 52 + 53 + return id(new AphrontRedirectResponse()) 54 + ->setURI($done_uri); 55 + } 56 + 57 + $form = id(new AphrontFormView()) 58 + ->setUser($viewer) 59 + ->appendControl( 60 + id(new AphrontFormTokenizerControl()) 61 + ->setName('memberPHIDs') 62 + ->setLabel(pht('Members')) 63 + ->setDatasource(new PhabricatorPeopleDatasource())); 64 + 65 + return $this->newDialog() 66 + ->setTitle(pht('Add Members')) 67 + ->appendForm($form) 68 + ->addCancelButton($done_uri) 69 + ->addSubmitButton(pht('Add Members')); 70 + } 71 + 72 + }
-156
src/applications/project/controller/PhabricatorProjectMembersEditController.php
··· 1 - <?php 2 - 3 - final class PhabricatorProjectMembersEditController 4 - extends PhabricatorProjectController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - $viewer = $request->getViewer(); 8 - $id = $request->getURIData('id'); 9 - 10 - $project = id(new PhabricatorProjectQuery()) 11 - ->setViewer($viewer) 12 - ->withIDs(array($id)) 13 - ->needMembers(true) 14 - ->needImages(true) 15 - ->executeOne(); 16 - if (!$project) { 17 - return new Aphront404Response(); 18 - } 19 - 20 - $this->setProject($project); 21 - 22 - $member_phids = $project->getMemberPHIDs(); 23 - 24 - if ($request->isFormPost()) { 25 - $member_spec = array(); 26 - 27 - $remove = $request->getStr('remove'); 28 - if ($remove) { 29 - $member_spec['-'] = array_fuse(array($remove)); 30 - } 31 - 32 - $add_members = $request->getArr('phids'); 33 - if ($add_members) { 34 - $member_spec['+'] = array_fuse($add_members); 35 - } 36 - 37 - $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 38 - 39 - $xactions = array(); 40 - 41 - $xactions[] = id(new PhabricatorProjectTransaction()) 42 - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 43 - ->setMetadataValue('edge:type', $type_member) 44 - ->setNewValue($member_spec); 45 - 46 - $editor = id(new PhabricatorProjectTransactionEditor($project)) 47 - ->setActor($viewer) 48 - ->setContentSourceFromRequest($request) 49 - ->setContinueOnNoEffect(true) 50 - ->setContinueOnMissingFields(true) 51 - ->applyTransactions($project, $xactions); 52 - 53 - return id(new AphrontRedirectResponse()) 54 - ->setURI($request->getRequestURI()); 55 - } 56 - 57 - $member_phids = array_reverse($member_phids); 58 - $handles = $this->loadViewerHandles($member_phids); 59 - 60 - $state = array(); 61 - foreach ($handles as $handle) { 62 - $state[] = array( 63 - 'phid' => $handle->getPHID(), 64 - 'name' => $handle->getFullName(), 65 - ); 66 - } 67 - 68 - $can_edit = PhabricatorPolicyFilter::hasCapability( 69 - $viewer, 70 - $project, 71 - PhabricatorPolicyCapability::CAN_EDIT); 72 - 73 - $supports_edit = $project->supportsEditMembers(); 74 - 75 - $form_box = null; 76 - $title = pht('Add Members'); 77 - if ($can_edit && $supports_edit) { 78 - $header_name = pht('Edit Members'); 79 - $view_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); 80 - 81 - $form = new AphrontFormView(); 82 - $form 83 - ->setUser($viewer) 84 - ->appendControl( 85 - id(new AphrontFormTokenizerControl()) 86 - ->setName('phids') 87 - ->setLabel(pht('Add Members')) 88 - ->setDatasource(new PhabricatorPeopleDatasource())) 89 - ->appendChild( 90 - id(new AphrontFormSubmitControl()) 91 - ->addCancelButton($view_uri) 92 - ->setValue(pht('Add Members'))); 93 - $form_box = id(new PHUIObjectBoxView()) 94 - ->setHeaderText($title) 95 - ->setForm($form); 96 - } 97 - 98 - $member_list = $this->renderMemberList($project, $handles); 99 - 100 - $nav = $this->getProfileMenu(); 101 - $nav->selectFilter(PhabricatorProject::PANEL_MEMBERS); 102 - 103 - $crumbs = $this->buildApplicationCrumbs(); 104 - $crumbs->addTextCrumb(pht('Members')); 105 - 106 - return $this->newPage() 107 - ->setNavigation($nav) 108 - ->setCrumbs($crumbs) 109 - ->setTitle(array($project->getName(), $title)) 110 - ->appendChild($form_box) 111 - ->appendChild($member_list); 112 - } 113 - 114 - private function renderMemberList( 115 - PhabricatorProject $project, 116 - array $handles) { 117 - 118 - $request = $this->getRequest(); 119 - $viewer = $request->getUser(); 120 - 121 - $can_edit = PhabricatorPolicyFilter::hasCapability( 122 - $viewer, 123 - $project, 124 - PhabricatorPolicyCapability::CAN_EDIT); 125 - 126 - $list = id(new PHUIObjectItemListView()) 127 - ->setNoDataString(pht('This project does not have any members.')); 128 - 129 - foreach ($handles as $handle) { 130 - $remove_uri = $this->getApplicationURI( 131 - '/members/'.$project->getID().'/remove/?phid='.$handle->getPHID()); 132 - 133 - $item = id(new PHUIObjectItemView()) 134 - ->setHeader($handle->getFullName()) 135 - ->setHref($handle->getURI()) 136 - ->setImageURI($handle->getImageURI()); 137 - 138 - if ($can_edit) { 139 - $item->addAction( 140 - id(new PHUIListItemView()) 141 - ->setIcon('fa-times') 142 - ->setName(pht('Remove')) 143 - ->setHref($remove_uri) 144 - ->setWorkflow(true)); 145 - } 146 - 147 - $list->addItem($item); 148 - } 149 - 150 - $box = id(new PHUIObjectBoxView()) 151 - ->setHeaderText(pht('Members')) 152 - ->setObjectList($list); 153 - 154 - return $box; 155 - } 156 - }
+205
src/applications/project/controller/PhabricatorProjectMembersViewController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectMembersViewController 4 + extends PhabricatorProjectController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + $id = $request->getURIData('id'); 9 + 10 + $project = id(new PhabricatorProjectQuery()) 11 + ->setViewer($viewer) 12 + ->withIDs(array($id)) 13 + ->needMembers(true) 14 + ->needWatchers(true) 15 + ->needImages(true) 16 + ->executeOne(); 17 + if (!$project) { 18 + return new Aphront404Response(); 19 + } 20 + 21 + $this->setProject($project); 22 + $title = pht('Members and Watchers'); 23 + 24 + $properties = $this->buildProperties($project); 25 + $actions = $this->buildActions($project); 26 + $properties->setActionList($actions); 27 + 28 + $object_box = id(new PHUIObjectBoxView()) 29 + ->setHeaderText($title) 30 + ->addPropertyList($properties); 31 + 32 + $member_list = id(new PhabricatorProjectMemberListView()) 33 + ->setUser($viewer) 34 + ->setProject($project) 35 + ->setUserPHIDs($project->getMemberPHIDs()); 36 + 37 + $watcher_list = id(new PhabricatorProjectWatcherListView()) 38 + ->setUser($viewer) 39 + ->setProject($project) 40 + ->setUserPHIDs($project->getWatcherPHIDs()); 41 + 42 + $nav = $this->getProfileMenu(); 43 + $nav->selectFilter(PhabricatorProject::PANEL_MEMBERS); 44 + 45 + $crumbs = $this->buildApplicationCrumbs(); 46 + $crumbs->addTextCrumb(pht('Members')); 47 + 48 + return $this->newPage() 49 + ->setNavigation($nav) 50 + ->setCrumbs($crumbs) 51 + ->setTitle(array($project->getName(), $title)) 52 + ->appendChild( 53 + array( 54 + $object_box, 55 + $member_list, 56 + $watcher_list, 57 + )); 58 + } 59 + 60 + private function buildProperties(PhabricatorProject $project) { 61 + $viewer = $this->getViewer(); 62 + 63 + $view = id(new PHUIPropertyListView()) 64 + ->setUser($viewer) 65 + ->setObject($project); 66 + 67 + if ($project->isMilestone()) { 68 + $icon_key = PhabricatorProjectIconSet::getMilestoneIconKey(); 69 + $icon = PhabricatorProjectIconSet::getIconIcon($icon_key); 70 + $target = PhabricatorProjectIconSet::getIconName($icon_key); 71 + $note = pht( 72 + 'Members of the parent project are members of this project.'); 73 + $show_join = false; 74 + } else if ($project->getHasSubprojects()) { 75 + $icon = 'fa-sitemap'; 76 + $target = pht('Parent Project'); 77 + $note = pht( 78 + 'Members of all subprojects are members of this project.'); 79 + $show_join = false; 80 + } else if ($project->getIsMembershipLocked()) { 81 + $icon = 'fa-lock'; 82 + $target = pht('Locked Project'); 83 + $note = pht( 84 + 'Users with access may join this project, but may not leave.'); 85 + $show_join = true; 86 + } else { 87 + $icon = 'fa-briefcase'; 88 + $target = pht('Normal Project'); 89 + $note = pht('Users with access may join and leave this project.'); 90 + $show_join = true; 91 + } 92 + 93 + $item = id(new PHUIStatusItemView()) 94 + ->setIcon($icon) 95 + ->setTarget(phutil_tag('strong', array(), $target)) 96 + ->setNote($note); 97 + 98 + $status = id(new PHUIStatusListView()) 99 + ->addItem($item); 100 + 101 + $view->addProperty(pht('Membership'), $status); 102 + 103 + if ($show_join) { 104 + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( 105 + $viewer, 106 + $project); 107 + 108 + $view->addProperty( 109 + pht('Joinable By'), 110 + $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); 111 + } 112 + 113 + return $view; 114 + } 115 + 116 + private function buildActions(PhabricatorProject $project) { 117 + $viewer = $this->getViewer(); 118 + $id = $project->getID(); 119 + 120 + $view = id(new PhabricatorActionListView()) 121 + ->setUser($viewer); 122 + 123 + $is_locked = $project->getIsMembershipLocked(); 124 + 125 + $can_edit = PhabricatorPolicyFilter::hasCapability( 126 + $viewer, 127 + $project, 128 + PhabricatorPolicyCapability::CAN_EDIT); 129 + 130 + $supports_edit = $project->supportsEditMembers(); 131 + 132 + $can_join = $supports_edit && PhabricatorPolicyFilter::hasCapability( 133 + $viewer, 134 + $project, 135 + PhabricatorPolicyCapability::CAN_JOIN); 136 + 137 + $can_leave = $supports_edit && (!$is_locked || $can_edit); 138 + 139 + if (!$project->isUserMember($viewer->getPHID())) { 140 + $view->addAction( 141 + id(new PhabricatorActionView()) 142 + ->setHref('/project/update/'.$project->getID().'/join/') 143 + ->setIcon('fa-plus') 144 + ->setDisabled(!$can_join) 145 + ->setWorkflow(true) 146 + ->setName(pht('Join Project'))); 147 + } else { 148 + $view->addAction( 149 + id(new PhabricatorActionView()) 150 + ->setHref('/project/update/'.$project->getID().'/leave/') 151 + ->setIcon('fa-times') 152 + ->setDisabled(!$can_leave) 153 + ->setWorkflow(true) 154 + ->setName(pht('Leave Project'))); 155 + 156 + if (!$project->isUserWatcher($viewer->getPHID())) { 157 + $view->addAction( 158 + id(new PhabricatorActionView()) 159 + ->setWorkflow(true) 160 + ->setHref('/project/watch/'.$project->getID().'/') 161 + ->setIcon('fa-eye') 162 + ->setName(pht('Watch Project'))); 163 + } else { 164 + $view->addAction( 165 + id(new PhabricatorActionView()) 166 + ->setWorkflow(true) 167 + ->setHref('/project/unwatch/'.$project->getID().'/') 168 + ->setIcon('fa-eye-slash') 169 + ->setName(pht('Unwatch Project'))); 170 + } 171 + } 172 + 173 + $can_add = $can_edit && $supports_edit; 174 + 175 + $view->addAction( 176 + id(new PhabricatorActionView()) 177 + ->setName(pht('Add Members')) 178 + ->setIcon('fa-user-plus') 179 + ->setHref("/project/members/{$id}/add/") 180 + ->setWorkflow(true) 181 + ->setDisabled(!$can_add)); 182 + 183 + $can_lock = $can_edit && $supports_edit && $this->hasApplicationCapability( 184 + ProjectCanLockProjectsCapability::CAPABILITY); 185 + 186 + if ($is_locked) { 187 + $lock_name = pht('Unlock Project'); 188 + $lock_icon = 'fa-unlock'; 189 + } else { 190 + $lock_name = pht('Lock Project'); 191 + $lock_icon = 'fa-lock'; 192 + } 193 + 194 + $view->addAction( 195 + id(new PhabricatorActionView()) 196 + ->setName($lock_name) 197 + ->setIcon($lock_icon) 198 + ->setHref($this->getApplicationURI("lock/{$id}/")) 199 + ->setDisabled(!$can_lock) 200 + ->setWorkflow(true)); 201 + 202 + return $view; 203 + } 204 + 205 + }
-67
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 106 106 ->setWorkflow(true)); 107 107 } 108 108 109 - $can_lock = $can_edit && $this->hasApplicationCapability( 110 - ProjectCanLockProjectsCapability::CAPABILITY); 111 - 112 - if ($project->getIsMembershipLocked()) { 113 - $lock_name = pht('Unlock Project'); 114 - $lock_icon = 'fa-unlock'; 115 - } else { 116 - $lock_name = pht('Lock Project'); 117 - $lock_icon = 'fa-lock'; 118 - } 119 - 120 - $view->addAction( 121 - id(new PhabricatorActionView()) 122 - ->setName($lock_name) 123 - ->setIcon($lock_icon) 124 - ->setHref($this->getApplicationURI("lock/{$id}/")) 125 - ->setDisabled(!$can_lock) 126 - ->setWorkflow(true)); 127 - 128 - $action = null; 129 - if (!$project->isUserMember($viewer->getPHID())) { 130 - $can_join = PhabricatorPolicyFilter::hasCapability( 131 - $viewer, 132 - $project, 133 - PhabricatorPolicyCapability::CAN_JOIN); 134 - 135 - $action = id(new PhabricatorActionView()) 136 - ->setUser($viewer) 137 - ->setRenderAsForm(true) 138 - ->setHref('/project/update/'.$project->getID().'/join/') 139 - ->setIcon('fa-plus') 140 - ->setDisabled(!$can_join) 141 - ->setName(pht('Join Project')); 142 - $view->addAction($action); 143 - } else { 144 - $action = id(new PhabricatorActionView()) 145 - ->setWorkflow(true) 146 - ->setHref('/project/update/'.$project->getID().'/leave/') 147 - ->setIcon('fa-times') 148 - ->setName(pht('Leave Project...')); 149 - $view->addAction($action); 150 - 151 - if (!$project->isUserWatcher($viewer->getPHID())) { 152 - $action = id(new PhabricatorActionView()) 153 - ->setWorkflow(true) 154 - ->setHref('/project/watch/'.$project->getID().'/') 155 - ->setIcon('fa-eye') 156 - ->setName(pht('Watch Project')); 157 - $view->addAction($action); 158 - } else { 159 - $action = id(new PhabricatorActionView()) 160 - ->setWorkflow(true) 161 - ->setHref('/project/unwatch/'.$project->getID().'/') 162 - ->setIcon('fa-eye-slash') 163 - ->setName(pht('Unwatch Project')); 164 - $view->addAction($action); 165 - } 166 - } 167 - 168 109 return $view; 169 110 } 170 111 ··· 206 147 ->setAsInline(true) 207 148 : phutil_tag('em', array(), pht('None'))); 208 149 209 - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( 210 - $viewer, 211 - $project); 212 - 213 150 $view->addProperty( 214 151 pht('Looks Like'), 215 152 $viewer->renderHandle($project->getPHID())->setAsTag(true)); 216 - 217 - $view->addProperty( 218 - pht('Joinable By'), 219 - $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); 220 153 221 154 $field_list = PhabricatorCustomField::getObjectFields( 222 155 $project,
+42 -40
src/applications/project/controller/PhabricatorProjectUpdateController.php
··· 12 12 PhabricatorPolicyCapability::CAN_VIEW, 13 13 ); 14 14 15 - $process_action = false; 16 15 switch ($action) { 17 16 case 'join': 18 17 $capabilities[] = PhabricatorPolicyCapability::CAN_JOIN; 19 - $process_action = $request->isFormPost(); 20 18 break; 21 19 case 'leave': 22 - $process_action = $request->isDialogFormPost(); 23 20 break; 24 21 default: 25 22 return new Aphront404Response(); ··· 35 32 return new Aphront404Response(); 36 33 } 37 34 38 - $project_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); 35 + if (!$project->supportsEditMembers()) { 36 + return new Aphront404Response(); 37 + } 39 38 40 - if ($process_action) { 39 + $done_uri = "/project/members/{$id}/"; 41 40 41 + if ($request->isFormPost()) { 42 42 $edge_action = null; 43 43 switch ($action) { 44 44 case 'join': ··· 50 50 } 51 51 52 52 $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 53 + 53 54 $member_spec = array( 54 55 $edge_action => array($viewer->getPHID() => $viewer->getPHID()), 55 56 ); ··· 67 68 ->setContinueOnMissingFields(true) 68 69 ->applyTransactions($project, $xactions); 69 70 70 - return id(new AphrontRedirectResponse())->setURI($project_uri); 71 + return id(new AphrontRedirectResponse())->setURI($done_uri); 71 72 } 72 73 73 - $dialog = null; 74 - switch ($action) { 75 - case 'leave': 76 - $dialog = new AphrontDialogView(); 77 - $dialog->setUser($viewer); 78 - if ($this->userCannotLeave($project)) { 79 - $dialog->setTitle(pht('You can not leave this project.')); 80 - $body = pht('The membership is locked for this project.'); 81 - } else { 82 - $dialog->setTitle(pht('Really leave project?')); 83 - $body = pht( 84 - 'Your tremendous contributions to this project will be sorely '. 85 - 'missed. Are you sure you want to leave?'); 86 - $dialog->addSubmitButton(pht('Leave Project')); 87 - } 88 - $dialog->appendParagraph($body); 89 - $dialog->addCancelButton($project_uri); 90 - break; 91 - default: 92 - return new Aphront404Response(); 74 + $is_locked = $project->getIsMembershipLocked(); 75 + $can_edit = PhabricatorPolicyFilter::hasCapability( 76 + $viewer, 77 + $project, 78 + PhabricatorPolicyCapability::CAN_EDIT); 79 + $can_leave = ($can_edit || !$is_locked); 80 + 81 + $button = null; 82 + if ($action == 'leave') { 83 + if ($can_leave) { 84 + $title = pht('Leave Project'); 85 + $body = pht( 86 + 'Your tremendous contributions to this project will be sorely '. 87 + 'missed. Are you sure you want to leave?'); 88 + $button = pht('Leave Project'); 89 + } else { 90 + $title = pht('Membership Locked'); 91 + $body = pht( 92 + 'Membership for this project is locked. You can not leave.'); 93 + } 94 + } else { 95 + $title = pht('Join Project'); 96 + $body = pht( 97 + 'Join this project? You will become a member and enjoy whatever '. 98 + 'benefits membership may confer.'); 99 + $button = pht('Join Project'); 93 100 } 94 101 95 - return id(new AphrontDialogResponse())->setDialog($dialog); 96 - } 102 + $dialog = $this->newDialog() 103 + ->setTitle($title) 104 + ->appendParagraph($body) 105 + ->addCancelButton($done_uri); 97 106 98 - /** 99 - * This is enforced in @{class:PhabricatorProjectTransactionEditor}. We use 100 - * this logic to render a better form for users hitting this case. 101 - */ 102 - private function userCannotLeave(PhabricatorProject $project) { 103 - $viewer = $this->getViewer(); 107 + if ($button) { 108 + $dialog->addSubmitButton($button); 109 + } 104 110 105 - return 106 - $project->getIsMembershipLocked() && 107 - !PhabricatorPolicyFilter::hasCapability( 108 - $viewer, 109 - $project, 110 - PhabricatorPolicyCapability::CAN_EDIT); 111 + return $dialog; 111 112 } 113 + 112 114 }
+4 -4
src/applications/project/controller/PhabricatorProjectWatchController.php
··· 18 18 return new Aphront404Response(); 19 19 } 20 20 21 - $project_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); 21 + $done_uri = "/project/members/{$id}/"; 22 22 23 - // You must be a member of a project to 23 + // You must be a member of a project to watch it. 24 24 if (!$project->isUserMember($viewer->getPHID())) { 25 25 return new Aphront400Response(); 26 26 } ··· 56 56 ->setContinueOnMissingFields(true) 57 57 ->applyTransactions($project, $xactions); 58 58 59 - return id(new AphrontRedirectResponse())->setURI($project_uri); 59 + return id(new AphrontRedirectResponse())->setURI($done_uri); 60 60 } 61 61 62 62 $dialog = null; ··· 83 83 return $this->newDialog() 84 84 ->setTitle($title) 85 85 ->appendParagraph($body) 86 - ->addCancelButton($project_uri) 86 + ->addCancelButton($done_uri) 87 87 ->addSubmitButton($submit); 88 88 } 89 89
+34
src/applications/project/view/PhabricatorProjectMemberListView.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectMemberListView 4 + extends PhabricatorProjectUserListView { 5 + 6 + protected function canEditList() { 7 + $viewer = $this->getUser(); 8 + $project = $this->getProject(); 9 + 10 + if (!$project->supportsEditMembers()) { 11 + return false; 12 + } 13 + 14 + return PhabricatorPolicyFilter::hasCapability( 15 + $viewer, 16 + $project, 17 + PhabricatorPolicyCapability::CAN_EDIT); 18 + } 19 + 20 + protected function getNoDataString() { 21 + return pht('This project does not have any members.'); 22 + } 23 + 24 + protected function getRemoveURI($phid) { 25 + $project = $this->getProject(); 26 + $id = $project->getID(); 27 + return "/project/members/{$id}/remove/?phid={$phid}"; 28 + } 29 + 30 + protected function getHeaderText() { 31 + return pht('Members'); 32 + } 33 + 34 + }
+78
src/applications/project/view/PhabricatorProjectUserListView.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorProjectUserListView extends AphrontView { 4 + 5 + private $project; 6 + private $userPHIDs; 7 + 8 + public function setProject(PhabricatorProject $project) { 9 + $this->project = $project; 10 + return $this; 11 + } 12 + 13 + public function getProject() { 14 + return $this->project; 15 + } 16 + 17 + public function setUserPHIDs(array $user_phids) { 18 + $this->userPHIDs = $user_phids; 19 + return $this; 20 + } 21 + 22 + public function getUserPHIDs() { 23 + return $this->userPHIDs; 24 + } 25 + 26 + abstract protected function canEditList(); 27 + abstract protected function getNoDataString(); 28 + abstract protected function getRemoveURI($phid); 29 + abstract protected function getHeaderText(); 30 + 31 + public function render() { 32 + $viewer = $this->getUser(); 33 + $project = $this->getProject(); 34 + $user_phids = $this->getUserPHIDs(); 35 + 36 + $can_edit = $this->canEditList(); 37 + $no_data = $this->getNoDataString(); 38 + 39 + $list = id(new PHUIObjectItemListView()) 40 + ->setNoDataString($no_data); 41 + 42 + $user_phids = array_reverse($user_phids); 43 + $handles = $viewer->loadHandles($user_phids); 44 + 45 + // Always put the viewer first if they are on the list. 46 + $user_phids = array_fuse($user_phids); 47 + $user_phids = 48 + array_select_keys($user_phids, array($viewer->getPHID())) + 49 + $user_phids; 50 + 51 + foreach ($user_phids as $user_phid) { 52 + $handle = $handles[$user_phid]; 53 + 54 + $item = id(new PHUIObjectItemView()) 55 + ->setHeader($handle->getFullName()) 56 + ->setHref($handle->getURI()) 57 + ->setImageURI($handle->getImageURI()); 58 + 59 + if ($can_edit) { 60 + $remove_uri = $this->getRemoveURI($user_phid); 61 + 62 + $item->addAction( 63 + id(new PHUIListItemView()) 64 + ->setIcon('fa-times') 65 + ->setName(pht('Remove')) 66 + ->setHref($remove_uri) 67 + ->setWorkflow(true)); 68 + } 69 + 70 + $list->addItem($item); 71 + } 72 + 73 + return id(new PHUIObjectBoxView()) 74 + ->setHeaderText($this->getHeaderText()) 75 + ->setObjectList($list); 76 + } 77 + 78 + }
+22
src/applications/project/view/PhabricatorProjectWatcherListView.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectWatcherListView 4 + extends PhabricatorProjectUserListView { 5 + 6 + protected function canEditList() { 7 + return false; 8 + } 9 + 10 + protected function getNoDataString() { 11 + return pht('This project does not have any watchers.'); 12 + } 13 + 14 + protected function getRemoveURI($phid) { 15 + return null; 16 + } 17 + 18 + protected function getHeaderText() { 19 + return pht('Watchers'); 20 + } 21 + 22 + }
-1
src/view/phui/PHUIStatusItemView.php
··· 24 24 const ICON_CLOCK = 'fa-clock-o'; 25 25 const ICON_STAR = 'fa-star'; 26 26 27 - /* render_textarea */ 28 27 public function setIcon($icon, $color = null, $label = null) { 29 28 $this->icon = $icon; 30 29 $this->iconLabel = $label;
+17 -10
webroot/rsrc/css/phui/phui-profile-menu.css
··· 73 73 background-size: 100%; 74 74 } 75 75 76 - .phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-href { 76 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 77 + .phui-list-item-href { 77 78 text-align: center; 78 79 padding: 42px 8px 12px; 79 80 font-size: 11px; 80 81 line-height: 13px; 81 82 } 82 83 83 - .phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-name { 84 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 85 + .phui-list-item-name { 84 86 display: block; 85 87 overflow: hidden; 86 88 text-overflow: ellipsis; 87 89 } 88 90 89 - .phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-icon, 90 - .phui-profile-menu .phui-profile-menu-collapsed 91 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 92 + .phui-list-item-icon, 93 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 91 94 .phui-list-item-href .phui-icon-view { 92 95 top: 10px; 93 96 left: 29px; ··· 166 169 left: 120px; 167 170 } 168 171 169 - .phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer { 172 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 173 + .phui-profile-menu-footer { 170 174 width: 40px; 171 175 height: {$menu.profile.item.height}; 172 176 bottom: 0px; 173 177 } 174 178 175 - .phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer-1 { 179 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 180 + .phui-profile-menu-footer-1 { 176 181 left: 0; 177 182 } 178 183 179 - .phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer-2 { 184 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 185 + .phui-profile-menu-footer-2 { 180 186 left: 40px; 181 187 } 182 188 183 - 184 - .phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer 189 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 190 + .phui-profile-menu-footer 185 191 .phui-list-item-name { 186 192 display: none; 187 193 } 188 194 189 - .phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer 195 + .phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu 196 + .phui-profile-menu-footer 190 197 .phui-list-item-icon { 191 198 top: 10px; 192 199 left: 10px;