From 6b1b21c999da983395ee563d0657e1bf58d8c62a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Jan 2016 16:27:36 -0800 Subject: [PATCH] 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 --- resources/celerity/map.php | 6 +- src/__phutil_library_map__.php | 12 +- .../PhabricatorProjectApplication.php | 4 +- .../PhabricatorProjectLockController.php | 11 +- ...PhabricatorProjectMembersAddController.php | 72 ++++++ ...habricatorProjectMembersEditController.php | 156 ------------- ...habricatorProjectMembersViewController.php | 205 ++++++++++++++++++ .../PhabricatorProjectProfileController.php | 67 ------ .../PhabricatorProjectUpdateController.php | 84 +++---- .../PhabricatorProjectWatchController.php | 8 +- .../view/PhabricatorProjectMemberListView.php | 34 +++ .../view/PhabricatorProjectUserListView.php | 78 +++++++ .../PhabricatorProjectWatcherListView.php | 22 ++ src/view/phui/PHUIStatusItemView.php | 1 - webroot/rsrc/css/phui/phui-profile-menu.css | 27 ++- 15 files changed, 501 insertions(+), 286 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectMembersAddController.php delete mode 100644 src/applications/project/controller/PhabricatorProjectMembersEditController.php create mode 100644 src/applications/project/controller/PhabricatorProjectMembersViewController.php create mode 100644 src/applications/project/view/PhabricatorProjectMemberListView.php create mode 100644 src/applications/project/view/PhabricatorProjectUserListView.php create mode 100644 src/applications/project/view/PhabricatorProjectWatcherListView.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index d58b545881..d7ae6285ef 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '7fce81fc', + 'core.pkg.css' => 'bd4f3259', 'core.pkg.js' => '573e6664', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -143,7 +143,7 @@ return array( 'rsrc/css/phui/phui-object-item-list-view.css' => '26c30d3f', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => '72d69773', + 'rsrc/css/phui/phui-profile-menu.css' => '84966ae9', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-spacing.css' => '042804d6', @@ -819,7 +819,7 @@ return array( 'phui-object-item-list-view-css' => '26c30d3f', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => '72d69773', + 'phui-profile-menu-css' => '84966ae9', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', 'phui-spacing-css' => '042804d6', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ff47a4e360..a7e6796ef2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2895,12 +2895,14 @@ phutil_register_library_map(array( 'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php', 'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php', 'PhabricatorProjectMaterializedMemberEdgeType' => 'applications/project/edge/PhabricatorProjectMaterializedMemberEdgeType.php', + 'PhabricatorProjectMemberListView' => 'applications/project/view/PhabricatorProjectMemberListView.php', 'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php', + 'PhabricatorProjectMembersAddController' => 'applications/project/controller/PhabricatorProjectMembersAddController.php', 'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php', - 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 'PhabricatorProjectMembersPolicyRule' => 'applications/project/policyrule/PhabricatorProjectMembersPolicyRule.php', 'PhabricatorProjectMembersProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php', 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', + 'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php', 'PhabricatorProjectMilestonesController' => 'applications/project/controller/PhabricatorProjectMilestonesController.php', 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', 'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php', @@ -2932,8 +2934,10 @@ phutil_register_library_map(array( 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php', + 'PhabricatorProjectUserListView' => 'applications/project/view/PhabricatorProjectUserListView.php', 'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php', 'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php', + 'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php', 'PhabricatorProjectWorkboardProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php', 'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php', 'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php', @@ -7292,12 +7296,14 @@ phutil_register_library_map(array( 'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectMaterializedMemberEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorProjectMemberListView' => 'PhabricatorProjectUserListView', 'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorProjectMembersAddController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource', - 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', + 'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController', 'PhabricatorProjectMilestonesController' => 'PhabricatorProjectController', 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar', @@ -7332,8 +7338,10 @@ phutil_register_library_map(array( 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorProjectUserListView' => 'AphrontView', 'PhabricatorProjectViewController' => 'PhabricatorProjectController', 'PhabricatorProjectWatchController' => 'PhabricatorProjectController', + 'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView', 'PhabricatorProjectWorkboardProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index e3732f2b31..d2990f527a 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -48,7 +48,9 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { 'lock/(?P[1-9]\d*)/' => 'PhabricatorProjectLockController', 'members/(?P[1-9]\d*)/' - => 'PhabricatorProjectMembersEditController', + => 'PhabricatorProjectMembersViewController', + 'members/(?P[1-9]\d*)/add/' + => 'PhabricatorProjectMembersAddController', 'members/(?P[1-9]\d*)/remove/' => 'PhabricatorProjectMembersRemoveController', 'profile/(?P[1-9]\d*)/' diff --git a/src/applications/project/controller/PhabricatorProjectLockController.php b/src/applications/project/controller/PhabricatorProjectLockController.php index 744be32f99..b9b56bde10 100644 --- a/src/applications/project/controller/PhabricatorProjectLockController.php +++ b/src/applications/project/controller/PhabricatorProjectLockController.php @@ -27,7 +27,16 @@ final class PhabricatorProjectLockController return new Aphront404Response(); } - $done_uri = $project->getURI(); + $done_uri = "/project/members/{$id}/"; + + if (!$project->supportsEditMembers()) { + return $this->newDialog() + ->setTitle(pht('Membership Immutable')) + ->appendChild( + pht('This project does not support editing membership.')) + ->addCancelButton($done_uri); + } + $is_locked = $project->getIsMembershipLocked(); if ($request->isFormPost()) { diff --git a/src/applications/project/controller/PhabricatorProjectMembersAddController.php b/src/applications/project/controller/PhabricatorProjectMembersAddController.php new file mode 100644 index 0000000000..bd1631ee92 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectMembersAddController.php @@ -0,0 +1,72 @@ +getViewer(); + $id = $request->getURIData('id'); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + $this->setProject($project); + + if (!$project->supportsEditMembers()) { + return new Aphront404Response(); + } + + $done_uri = "/project/members/{$id}/"; + + if ($request->isFormPost()) { + $member_phids = $request->getArr('memberPHIDs'); + + $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; + + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $type_member) + ->setNewValue( + array( + '+' => array_fuse($member_phids), + )); + + $editor = id(new PhabricatorProjectTransactionEditor($project)) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($project, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($done_uri); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('memberPHIDs') + ->setLabel(pht('Members')) + ->setDatasource(new PhabricatorPeopleDatasource())); + + return $this->newDialog() + ->setTitle(pht('Add Members')) + ->appendForm($form) + ->addCancelButton($done_uri) + ->addSubmitButton(pht('Add Members')); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php deleted file mode 100644 index f485e2997a..0000000000 --- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php +++ /dev/null @@ -1,156 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needMembers(true) - ->needImages(true) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); - } - - $this->setProject($project); - - $member_phids = $project->getMemberPHIDs(); - - if ($request->isFormPost()) { - $member_spec = array(); - - $remove = $request->getStr('remove'); - if ($remove) { - $member_spec['-'] = array_fuse(array($remove)); - } - - $add_members = $request->getArr('phids'); - if ($add_members) { - $member_spec['+'] = array_fuse($add_members); - } - - $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; - - $xactions = array(); - - $xactions[] = id(new PhabricatorProjectTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $type_member) - ->setNewValue($member_spec); - - $editor = id(new PhabricatorProjectTransactionEditor($project)) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true) - ->applyTransactions($project, $xactions); - - return id(new AphrontRedirectResponse()) - ->setURI($request->getRequestURI()); - } - - $member_phids = array_reverse($member_phids); - $handles = $this->loadViewerHandles($member_phids); - - $state = array(); - foreach ($handles as $handle) { - $state[] = array( - 'phid' => $handle->getPHID(), - 'name' => $handle->getFullName(), - ); - } - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $project, - PhabricatorPolicyCapability::CAN_EDIT); - - $supports_edit = $project->supportsEditMembers(); - - $form_box = null; - $title = pht('Add Members'); - if ($can_edit && $supports_edit) { - $header_name = pht('Edit Members'); - $view_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); - - $form = new AphrontFormView(); - $form - ->setUser($viewer) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setName('phids') - ->setLabel(pht('Add Members')) - ->setDatasource(new PhabricatorPeopleDatasource())) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($view_uri) - ->setValue(pht('Add Members'))); - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setForm($form); - } - - $member_list = $this->renderMemberList($project, $handles); - - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::PANEL_MEMBERS); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Members')); - - return $this->newPage() - ->setNavigation($nav) - ->setCrumbs($crumbs) - ->setTitle(array($project->getName(), $title)) - ->appendChild($form_box) - ->appendChild($member_list); - } - - private function renderMemberList( - PhabricatorProject $project, - array $handles) { - - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $project, - PhabricatorPolicyCapability::CAN_EDIT); - - $list = id(new PHUIObjectItemListView()) - ->setNoDataString(pht('This project does not have any members.')); - - foreach ($handles as $handle) { - $remove_uri = $this->getApplicationURI( - '/members/'.$project->getID().'/remove/?phid='.$handle->getPHID()); - - $item = id(new PHUIObjectItemView()) - ->setHeader($handle->getFullName()) - ->setHref($handle->getURI()) - ->setImageURI($handle->getImageURI()); - - if ($can_edit) { - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-times') - ->setName(pht('Remove')) - ->setHref($remove_uri) - ->setWorkflow(true)); - } - - $list->addItem($item); - } - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Members')) - ->setObjectList($list); - - return $box; - } -} diff --git a/src/applications/project/controller/PhabricatorProjectMembersViewController.php b/src/applications/project/controller/PhabricatorProjectMembersViewController.php new file mode 100644 index 0000000000..dd9f263e5d --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectMembersViewController.php @@ -0,0 +1,205 @@ +getViewer(); + $id = $request->getURIData('id'); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needMembers(true) + ->needWatchers(true) + ->needImages(true) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + $this->setProject($project); + $title = pht('Members and Watchers'); + + $properties = $this->buildProperties($project); + $actions = $this->buildActions($project); + $properties->setActionList($actions); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->addPropertyList($properties); + + $member_list = id(new PhabricatorProjectMemberListView()) + ->setUser($viewer) + ->setProject($project) + ->setUserPHIDs($project->getMemberPHIDs()); + + $watcher_list = id(new PhabricatorProjectWatcherListView()) + ->setUser($viewer) + ->setProject($project) + ->setUserPHIDs($project->getWatcherPHIDs()); + + $nav = $this->getProfileMenu(); + $nav->selectFilter(PhabricatorProject::PANEL_MEMBERS); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Members')); + + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle(array($project->getName(), $title)) + ->appendChild( + array( + $object_box, + $member_list, + $watcher_list, + )); + } + + private function buildProperties(PhabricatorProject $project) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($project); + + if ($project->isMilestone()) { + $icon_key = PhabricatorProjectIconSet::getMilestoneIconKey(); + $icon = PhabricatorProjectIconSet::getIconIcon($icon_key); + $target = PhabricatorProjectIconSet::getIconName($icon_key); + $note = pht( + 'Members of the parent project are members of this project.'); + $show_join = false; + } else if ($project->getHasSubprojects()) { + $icon = 'fa-sitemap'; + $target = pht('Parent Project'); + $note = pht( + 'Members of all subprojects are members of this project.'); + $show_join = false; + } else if ($project->getIsMembershipLocked()) { + $icon = 'fa-lock'; + $target = pht('Locked Project'); + $note = pht( + 'Users with access may join this project, but may not leave.'); + $show_join = true; + } else { + $icon = 'fa-briefcase'; + $target = pht('Normal Project'); + $note = pht('Users with access may join and leave this project.'); + $show_join = true; + } + + $item = id(new PHUIStatusItemView()) + ->setIcon($icon) + ->setTarget(phutil_tag('strong', array(), $target)) + ->setNote($note); + + $status = id(new PHUIStatusListView()) + ->addItem($item); + + $view->addProperty(pht('Membership'), $status); + + if ($show_join) { + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( + $viewer, + $project); + + $view->addProperty( + pht('Joinable By'), + $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); + } + + return $view; + } + + private function buildActions(PhabricatorProject $project) { + $viewer = $this->getViewer(); + $id = $project->getID(); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $is_locked = $project->getIsMembershipLocked(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $supports_edit = $project->supportsEditMembers(); + + $can_join = $supports_edit && PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_JOIN); + + $can_leave = $supports_edit && (!$is_locked || $can_edit); + + if (!$project->isUserMember($viewer->getPHID())) { + $view->addAction( + id(new PhabricatorActionView()) + ->setHref('/project/update/'.$project->getID().'/join/') + ->setIcon('fa-plus') + ->setDisabled(!$can_join) + ->setWorkflow(true) + ->setName(pht('Join Project'))); + } else { + $view->addAction( + id(new PhabricatorActionView()) + ->setHref('/project/update/'.$project->getID().'/leave/') + ->setIcon('fa-times') + ->setDisabled(!$can_leave) + ->setWorkflow(true) + ->setName(pht('Leave Project'))); + + if (!$project->isUserWatcher($viewer->getPHID())) { + $view->addAction( + id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setHref('/project/watch/'.$project->getID().'/') + ->setIcon('fa-eye') + ->setName(pht('Watch Project'))); + } else { + $view->addAction( + id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setHref('/project/unwatch/'.$project->getID().'/') + ->setIcon('fa-eye-slash') + ->setName(pht('Unwatch Project'))); + } + } + + $can_add = $can_edit && $supports_edit; + + $view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Add Members')) + ->setIcon('fa-user-plus') + ->setHref("/project/members/{$id}/add/") + ->setWorkflow(true) + ->setDisabled(!$can_add)); + + $can_lock = $can_edit && $supports_edit && $this->hasApplicationCapability( + ProjectCanLockProjectsCapability::CAPABILITY); + + if ($is_locked) { + $lock_name = pht('Unlock Project'); + $lock_icon = 'fa-unlock'; + } else { + $lock_name = pht('Lock Project'); + $lock_icon = 'fa-lock'; + } + + $view->addAction( + id(new PhabricatorActionView()) + ->setName($lock_name) + ->setIcon($lock_icon) + ->setHref($this->getApplicationURI("lock/{$id}/")) + ->setDisabled(!$can_lock) + ->setWorkflow(true)); + + return $view; + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index a2eb5eebe6..89600e97d7 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -106,65 +106,6 @@ final class PhabricatorProjectProfileController ->setWorkflow(true)); } - $can_lock = $can_edit && $this->hasApplicationCapability( - ProjectCanLockProjectsCapability::CAPABILITY); - - if ($project->getIsMembershipLocked()) { - $lock_name = pht('Unlock Project'); - $lock_icon = 'fa-unlock'; - } else { - $lock_name = pht('Lock Project'); - $lock_icon = 'fa-lock'; - } - - $view->addAction( - id(new PhabricatorActionView()) - ->setName($lock_name) - ->setIcon($lock_icon) - ->setHref($this->getApplicationURI("lock/{$id}/")) - ->setDisabled(!$can_lock) - ->setWorkflow(true)); - - $action = null; - if (!$project->isUserMember($viewer->getPHID())) { - $can_join = PhabricatorPolicyFilter::hasCapability( - $viewer, - $project, - PhabricatorPolicyCapability::CAN_JOIN); - - $action = id(new PhabricatorActionView()) - ->setUser($viewer) - ->setRenderAsForm(true) - ->setHref('/project/update/'.$project->getID().'/join/') - ->setIcon('fa-plus') - ->setDisabled(!$can_join) - ->setName(pht('Join Project')); - $view->addAction($action); - } else { - $action = id(new PhabricatorActionView()) - ->setWorkflow(true) - ->setHref('/project/update/'.$project->getID().'/leave/') - ->setIcon('fa-times') - ->setName(pht('Leave Project...')); - $view->addAction($action); - - if (!$project->isUserWatcher($viewer->getPHID())) { - $action = id(new PhabricatorActionView()) - ->setWorkflow(true) - ->setHref('/project/watch/'.$project->getID().'/') - ->setIcon('fa-eye') - ->setName(pht('Watch Project')); - $view->addAction($action); - } else { - $action = id(new PhabricatorActionView()) - ->setWorkflow(true) - ->setHref('/project/unwatch/'.$project->getID().'/') - ->setIcon('fa-eye-slash') - ->setName(pht('Unwatch Project')); - $view->addAction($action); - } - } - return $view; } @@ -206,18 +147,10 @@ final class PhabricatorProjectProfileController ->setAsInline(true) : phutil_tag('em', array(), pht('None'))); - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( - $viewer, - $project); - $view->addProperty( pht('Looks Like'), $viewer->renderHandle($project->getPHID())->setAsTag(true)); - $view->addProperty( - pht('Joinable By'), - $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); - $field_list = PhabricatorCustomField::getObjectFields( $project, PhabricatorCustomField::ROLE_VIEW); diff --git a/src/applications/project/controller/PhabricatorProjectUpdateController.php b/src/applications/project/controller/PhabricatorProjectUpdateController.php index cfdeb4fd09..762343f485 100644 --- a/src/applications/project/controller/PhabricatorProjectUpdateController.php +++ b/src/applications/project/controller/PhabricatorProjectUpdateController.php @@ -12,14 +12,11 @@ final class PhabricatorProjectUpdateController PhabricatorPolicyCapability::CAN_VIEW, ); - $process_action = false; switch ($action) { case 'join': $capabilities[] = PhabricatorPolicyCapability::CAN_JOIN; - $process_action = $request->isFormPost(); break; case 'leave': - $process_action = $request->isDialogFormPost(); break; default: return new Aphront404Response(); @@ -35,10 +32,13 @@ final class PhabricatorProjectUpdateController return new Aphront404Response(); } - $project_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); + if (!$project->supportsEditMembers()) { + return new Aphront404Response(); + } - if ($process_action) { + $done_uri = "/project/members/{$id}/"; + if ($request->isFormPost()) { $edge_action = null; switch ($action) { case 'join': @@ -50,6 +50,7 @@ final class PhabricatorProjectUpdateController } $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; + $member_spec = array( $edge_action => array($viewer->getPHID() => $viewer->getPHID()), ); @@ -67,46 +68,47 @@ final class PhabricatorProjectUpdateController ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); - return id(new AphrontRedirectResponse())->setURI($project_uri); + return id(new AphrontRedirectResponse())->setURI($done_uri); } - $dialog = null; - switch ($action) { - case 'leave': - $dialog = new AphrontDialogView(); - $dialog->setUser($viewer); - if ($this->userCannotLeave($project)) { - $dialog->setTitle(pht('You can not leave this project.')); - $body = pht('The membership is locked for this project.'); - } else { - $dialog->setTitle(pht('Really leave project?')); - $body = pht( - 'Your tremendous contributions to this project will be sorely '. - 'missed. Are you sure you want to leave?'); - $dialog->addSubmitButton(pht('Leave Project')); - } - $dialog->appendParagraph($body); - $dialog->addCancelButton($project_uri); - break; - default: - return new Aphront404Response(); + $is_locked = $project->getIsMembershipLocked(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + $can_leave = ($can_edit || !$is_locked); + + $button = null; + if ($action == 'leave') { + if ($can_leave) { + $title = pht('Leave Project'); + $body = pht( + 'Your tremendous contributions to this project will be sorely '. + 'missed. Are you sure you want to leave?'); + $button = pht('Leave Project'); + } else { + $title = pht('Membership Locked'); + $body = pht( + 'Membership for this project is locked. You can not leave.'); + } + } else { + $title = pht('Join Project'); + $body = pht( + 'Join this project? You will become a member and enjoy whatever '. + 'benefits membership may confer.'); + $button = pht('Join Project'); } - return id(new AphrontDialogResponse())->setDialog($dialog); + $dialog = $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($done_uri); + + if ($button) { + $dialog->addSubmitButton($button); + } + + return $dialog; } - /** - * This is enforced in @{class:PhabricatorProjectTransactionEditor}. We use - * this logic to render a better form for users hitting this case. - */ - private function userCannotLeave(PhabricatorProject $project) { - $viewer = $this->getViewer(); - - return - $project->getIsMembershipLocked() && - !PhabricatorPolicyFilter::hasCapability( - $viewer, - $project, - PhabricatorPolicyCapability::CAN_EDIT); - } } diff --git a/src/applications/project/controller/PhabricatorProjectWatchController.php b/src/applications/project/controller/PhabricatorProjectWatchController.php index 53538f8393..c39e7dd510 100644 --- a/src/applications/project/controller/PhabricatorProjectWatchController.php +++ b/src/applications/project/controller/PhabricatorProjectWatchController.php @@ -18,9 +18,9 @@ final class PhabricatorProjectWatchController return new Aphront404Response(); } - $project_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); + $done_uri = "/project/members/{$id}/"; - // You must be a member of a project to + // You must be a member of a project to watch it. if (!$project->isUserMember($viewer->getPHID())) { return new Aphront400Response(); } @@ -56,7 +56,7 @@ final class PhabricatorProjectWatchController ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); - return id(new AphrontRedirectResponse())->setURI($project_uri); + return id(new AphrontRedirectResponse())->setURI($done_uri); } $dialog = null; @@ -83,7 +83,7 @@ final class PhabricatorProjectWatchController return $this->newDialog() ->setTitle($title) ->appendParagraph($body) - ->addCancelButton($project_uri) + ->addCancelButton($done_uri) ->addSubmitButton($submit); } diff --git a/src/applications/project/view/PhabricatorProjectMemberListView.php b/src/applications/project/view/PhabricatorProjectMemberListView.php new file mode 100644 index 0000000000..12b7cc7a76 --- /dev/null +++ b/src/applications/project/view/PhabricatorProjectMemberListView.php @@ -0,0 +1,34 @@ +getUser(); + $project = $this->getProject(); + + if (!$project->supportsEditMembers()) { + return false; + } + + return PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + } + + protected function getNoDataString() { + return pht('This project does not have any members.'); + } + + protected function getRemoveURI($phid) { + $project = $this->getProject(); + $id = $project->getID(); + return "/project/members/{$id}/remove/?phid={$phid}"; + } + + protected function getHeaderText() { + return pht('Members'); + } + +} diff --git a/src/applications/project/view/PhabricatorProjectUserListView.php b/src/applications/project/view/PhabricatorProjectUserListView.php new file mode 100644 index 0000000000..e42b427ed3 --- /dev/null +++ b/src/applications/project/view/PhabricatorProjectUserListView.php @@ -0,0 +1,78 @@ +project = $project; + return $this; + } + + public function getProject() { + return $this->project; + } + + public function setUserPHIDs(array $user_phids) { + $this->userPHIDs = $user_phids; + return $this; + } + + public function getUserPHIDs() { + return $this->userPHIDs; + } + + abstract protected function canEditList(); + abstract protected function getNoDataString(); + abstract protected function getRemoveURI($phid); + abstract protected function getHeaderText(); + + public function render() { + $viewer = $this->getUser(); + $project = $this->getProject(); + $user_phids = $this->getUserPHIDs(); + + $can_edit = $this->canEditList(); + $no_data = $this->getNoDataString(); + + $list = id(new PHUIObjectItemListView()) + ->setNoDataString($no_data); + + $user_phids = array_reverse($user_phids); + $handles = $viewer->loadHandles($user_phids); + + // Always put the viewer first if they are on the list. + $user_phids = array_fuse($user_phids); + $user_phids = + array_select_keys($user_phids, array($viewer->getPHID())) + + $user_phids; + + foreach ($user_phids as $user_phid) { + $handle = $handles[$user_phid]; + + $item = id(new PHUIObjectItemView()) + ->setHeader($handle->getFullName()) + ->setHref($handle->getURI()) + ->setImageURI($handle->getImageURI()); + + if ($can_edit) { + $remove_uri = $this->getRemoveURI($user_phid); + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('fa-times') + ->setName(pht('Remove')) + ->setHref($remove_uri) + ->setWorkflow(true)); + } + + $list->addItem($item); + } + + return id(new PHUIObjectBoxView()) + ->setHeaderText($this->getHeaderText()) + ->setObjectList($list); + } + +} diff --git a/src/applications/project/view/PhabricatorProjectWatcherListView.php b/src/applications/project/view/PhabricatorProjectWatcherListView.php new file mode 100644 index 0000000000..ec993f031c --- /dev/null +++ b/src/applications/project/view/PhabricatorProjectWatcherListView.php @@ -0,0 +1,22 @@ +icon = $icon; $this->iconLabel = $label; diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css index b2475a5457..c749c7f3d4 100644 --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -73,21 +73,24 @@ background-size: 100%; } -.phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-href { +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-list-item-href { text-align: center; padding: 42px 8px 12px; font-size: 11px; line-height: 13px; } -.phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-name { +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-list-item-name { display: block; overflow: hidden; text-overflow: ellipsis; } -.phui-profile-menu .phui-profile-menu-collapsed .phui-list-item-icon, -.phui-profile-menu .phui-profile-menu-collapsed +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-list-item-icon, +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu .phui-list-item-href .phui-icon-view { top: 10px; left: 29px; @@ -166,27 +169,31 @@ left: 120px; } -.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer { +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-menu-footer { width: 40px; height: {$menu.profile.item.height}; bottom: 0px; } -.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer-1 { +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-menu-footer-1 { left: 0; } -.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer-2 { +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-menu-footer-2 { left: 40px; } - -.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-menu-footer .phui-list-item-name { display: none; } -.phui-profile-menu .phui-profile-menu-collapsed .phui-profile-menu-footer +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-menu-footer .phui-list-item-icon { top: 10px; left: 10px;