Add a Join / Leave button to Projects
Summary: Make it easy to join or leave (well, slightly less easy) a project. Publish join/leave to feed. Fix a couple of membership editor bugs. Test Plan: Joined, left a project. Reviewers: btrahan, jungejason Reviewed By: btrahan CC: aran, epriestley Maniphest Tasks: T681 Differential Revision: https://secure.phabricator.com/D1485
This commit is contained in:
@@ -1483,7 +1483,7 @@ celerity_register_resource_map(array(
|
|||||||
),
|
),
|
||||||
'phabricator-profile-header-css' =>
|
'phabricator-profile-header-css' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/d39ef6a4/rsrc/css/application/profile/profile-header-view.css',
|
'uri' => '/res/4b1cb23b/rsrc/css/application/profile/profile-header-view.css',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
'requires' =>
|
'requires' =>
|
||||||
array(
|
array(
|
||||||
|
|||||||
@@ -628,6 +628,7 @@ phutil_register_library_map(array(
|
|||||||
'PhabricatorProjectSubproject' => 'applications/project/storage/subproject',
|
'PhabricatorProjectSubproject' => 'applications/project/storage/subproject',
|
||||||
'PhabricatorProjectTransaction' => 'applications/project/storage/transaction',
|
'PhabricatorProjectTransaction' => 'applications/project/storage/transaction',
|
||||||
'PhabricatorProjectTransactionType' => 'applications/project/constants/transaction',
|
'PhabricatorProjectTransactionType' => 'applications/project/constants/transaction',
|
||||||
|
'PhabricatorProjectUpdateController' => 'applications/project/controller/update',
|
||||||
'PhabricatorRedirectController' => 'applications/base/controller/redirect',
|
'PhabricatorRedirectController' => 'applications/base/controller/redirect',
|
||||||
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/refresh',
|
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/refresh',
|
||||||
'PhabricatorRemarkupRuleDifferential' => 'infrastructure/markup/remarkup/markuprule/differential',
|
'PhabricatorRemarkupRuleDifferential' => 'infrastructure/markup/remarkup/markuprule/differential',
|
||||||
@@ -1313,6 +1314,7 @@ phutil_register_library_map(array(
|
|||||||
'PhabricatorProjectSubproject' => 'PhabricatorProjectDAO',
|
'PhabricatorProjectSubproject' => 'PhabricatorProjectDAO',
|
||||||
'PhabricatorProjectTransaction' => 'PhabricatorProjectDAO',
|
'PhabricatorProjectTransaction' => 'PhabricatorProjectDAO',
|
||||||
'PhabricatorProjectTransactionType' => 'PhabricatorProjectConstants',
|
'PhabricatorProjectTransactionType' => 'PhabricatorProjectConstants',
|
||||||
|
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorRedirectController' => 'PhabricatorController',
|
'PhabricatorRedirectController' => 'PhabricatorController',
|
||||||
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
|
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
|
||||||
'PhabricatorRemarkupRuleDifferential' => 'PhabricatorRemarkupRuleObjectName',
|
'PhabricatorRemarkupRuleDifferential' => 'PhabricatorRemarkupRuleObjectName',
|
||||||
|
|||||||
@@ -207,6 +207,8 @@ class AphrontDefaultApplicationConfiguration
|
|||||||
'affiliation/(?P<id>\d+)/$'
|
'affiliation/(?P<id>\d+)/$'
|
||||||
=> 'PhabricatorProjectAffiliationEditController',
|
=> 'PhabricatorProjectAffiliationEditController',
|
||||||
'create/$' => 'PhabricatorProjectCreateController',
|
'create/$' => 'PhabricatorProjectCreateController',
|
||||||
|
'update/(?P<id>\d+)/(?P<action>[^/]+)/$'
|
||||||
|
=> 'PhabricatorProjectUpdateController',
|
||||||
),
|
),
|
||||||
|
|
||||||
'/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)$'
|
'/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)$'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2011 Facebook, Inc.
|
* Copyright 2012 Facebook, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -77,4 +77,12 @@ abstract class PhabricatorFeedStory {
|
|||||||
return $this->getStoryData()->getEpoch();
|
return $this->getStoryData()->getEpoch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final protected function renderHandleList(array $phids) {
|
||||||
|
$list = array();
|
||||||
|
foreach ($phids as $phid) {
|
||||||
|
$list[] = '<strong>'.$this->getHandle($phid)->renderLink().'</strong>';
|
||||||
|
}
|
||||||
|
return implode(', ', $list);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ class PhabricatorFeedStoryProject extends PhabricatorFeedStory {
|
|||||||
$old = $data->getValue('old');
|
$old = $data->getValue('old');
|
||||||
$new = $data->getValue('new');
|
$new = $data->getValue('new');
|
||||||
$proj = $this->getHandle($data->getValue('projectPHID'));
|
$proj = $this->getHandle($data->getValue('projectPHID'));
|
||||||
$auth = $this->getHandle($data->getAuthorPHID());
|
|
||||||
|
$author_phid = $data->getAuthorPHID();
|
||||||
|
$author = $this->getHandle($author_phid);
|
||||||
|
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case PhabricatorProjectTransactionType::TYPE_NAME:
|
case PhabricatorProjectTransactionType::TYPE_NAME:
|
||||||
@@ -58,11 +60,36 @@ class PhabricatorFeedStoryProject extends PhabricatorFeedStory {
|
|||||||
'<strong>'.phutil_escape_html($new).'</strong>).';
|
'<strong>'.phutil_escape_html($new).'</strong>).';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case PhabricatorProjectTransactionType::TYPE_MEMBERS:
|
||||||
|
$add = array_diff($new, $old);
|
||||||
|
$rem = array_diff($old, $new);
|
||||||
|
|
||||||
|
if ((count($add) == 1) && (count($rem) == 0) &&
|
||||||
|
(head($add) == $author_phid)) {
|
||||||
|
$action = 'joined project <strong>'.$proj->renderLink().'</strong>.';
|
||||||
|
} else if ((count($add) == 0) && (count($rem) == 1) &&
|
||||||
|
(head($rem) == $author_phid)) {
|
||||||
|
$action = 'left project <strong>'.$proj->renderLink().'</strong>.';
|
||||||
|
} else if (empty($rem)) {
|
||||||
|
$action = 'added members to project '.
|
||||||
|
'<strong>'.$proj->renderLink().'</strong>: '.
|
||||||
|
$this->renderHandleList($add).'.';
|
||||||
|
} else if (empty($add)) {
|
||||||
|
$action = 'removed members from project '.
|
||||||
|
'<strong>'.$proj->renderLink().'</strong>: '.
|
||||||
|
$this->renderHandleList($rem).'.';
|
||||||
|
} else {
|
||||||
|
$action = 'changed members of project '.
|
||||||
|
'<strong>'.$proj->renderLink().'</strong>, added: '.
|
||||||
|
$this->renderHandleList($add).'; removed: '.
|
||||||
|
$this->renderHandleList($rem).'.';
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$action = 'updated project <strong>'.$proj->renderLink().'</strong>';
|
$action = 'updated project <strong>'.$proj->renderLink().'</strong>.';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$view->setTitle('<strong>'.$auth->renderLink().'</strong> '.$action);
|
$view->setTitle('<strong>'.$author->renderLink().'</strong> '.$action);
|
||||||
$view->setOneLineStory(true);
|
$view->setOneLineStory(true);
|
||||||
|
|
||||||
return $view;
|
return $view;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ phutil_require_module('phabricator', 'applications/feed/view/story');
|
|||||||
phutil_require_module('phabricator', 'applications/project/constants/transaction');
|
phutil_require_module('phabricator', 'applications/project/constants/transaction');
|
||||||
|
|
||||||
phutil_require_module('phutil', 'markup');
|
phutil_require_module('phutil', 'markup');
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
phutil_require_source('PhabricatorFeedStoryProject.php');
|
phutil_require_source('PhabricatorFeedStoryProject.php');
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class PhabricatorProjectProfileController
|
|||||||
$picture = null;
|
$picture = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$members = mpull($project->loadAffiliations(), null, 'getUserPHID');
|
||||||
|
|
||||||
$nav_view = new AphrontSideNavFilterView();
|
$nav_view = new AphrontSideNavFilterView();
|
||||||
$uri = new PhutilURI('/project/view/'.$project->getID().'/');
|
$uri = new PhutilURI('/project/view/'.$project->getID().'/');
|
||||||
@@ -114,6 +114,33 @@ class PhabricatorProjectProfileController
|
|||||||
phutil_utf8_shorten($profile->getBlurb(), 1024));
|
phutil_utf8_shorten($profile->getBlurb(), 1024));
|
||||||
$header->setProfilePicture($picture);
|
$header->setProfilePicture($picture);
|
||||||
|
|
||||||
|
$action = null;
|
||||||
|
if (empty($members[$user->getPHID()])) {
|
||||||
|
$action = phabricator_render_form(
|
||||||
|
$user,
|
||||||
|
array(
|
||||||
|
'action' => '/project/update/'.$project->getID().'/join/',
|
||||||
|
'method' => 'post',
|
||||||
|
),
|
||||||
|
phutil_render_tag(
|
||||||
|
'button',
|
||||||
|
array(
|
||||||
|
'class' => 'green',
|
||||||
|
),
|
||||||
|
'Join Project'));
|
||||||
|
} else {
|
||||||
|
$action = javelin_render_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'href' => '/project/update/'.$project->getID().'/leave/',
|
||||||
|
'sigil' => 'workflow',
|
||||||
|
'class' => 'grey button',
|
||||||
|
),
|
||||||
|
'Leave Project...');
|
||||||
|
}
|
||||||
|
|
||||||
|
$header->addAction($action);
|
||||||
|
|
||||||
$header->appendChild($nav_view);
|
$header->appendChild($nav_view);
|
||||||
|
|
||||||
return $this->buildStandardPageResponse(
|
return $this->buildStandardPageResponse(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ phutil_require_module('phabricator', 'applications/project/controller/base');
|
|||||||
phutil_require_module('phabricator', 'applications/project/storage/profile');
|
phutil_require_module('phabricator', 'applications/project/storage/profile');
|
||||||
phutil_require_module('phabricator', 'applications/project/storage/project');
|
phutil_require_module('phabricator', 'applications/project/storage/project');
|
||||||
phutil_require_module('phabricator', 'infrastructure/celerity/api');
|
phutil_require_module('phabricator', 'infrastructure/celerity/api');
|
||||||
|
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
|
||||||
phutil_require_module('phabricator', 'view/control/table');
|
phutil_require_module('phabricator', 'view/control/table');
|
||||||
phutil_require_module('phabricator', 'view/layout/profileheader');
|
phutil_require_module('phabricator', 'view/layout/profileheader');
|
||||||
phutil_require_module('phabricator', 'view/layout/sidenavfilter');
|
phutil_require_module('phabricator', 'view/layout/sidenavfilter');
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2012 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PhabricatorProjectUpdateController
|
||||||
|
extends PhabricatorProjectController {
|
||||||
|
|
||||||
|
private $id;
|
||||||
|
private $action;
|
||||||
|
|
||||||
|
public function willProcessRequest(array $data) {
|
||||||
|
$this->id = $data['id'];
|
||||||
|
$this->action = $data['action'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
$user = $request->getUser();
|
||||||
|
|
||||||
|
$project = id(new PhabricatorProject())->load($this->id);
|
||||||
|
if (!$project) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$process_action = false;
|
||||||
|
switch ($this->action) {
|
||||||
|
case 'join':
|
||||||
|
$process_action = $request->isFormPost();
|
||||||
|
break;
|
||||||
|
case 'leave':
|
||||||
|
$process_action = $request->isDialogFormPost();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$project_uri = '/project/view/'.$project->getID().'/';
|
||||||
|
|
||||||
|
if ($process_action) {
|
||||||
|
$xactions = array();
|
||||||
|
|
||||||
|
switch ($this->action) {
|
||||||
|
case 'join':
|
||||||
|
$affils = $project->loadAffiliations();
|
||||||
|
$affils = mpull($affils, null, 'getUserPHID');
|
||||||
|
if (empty($affils[$user->getPHID()])) {
|
||||||
|
$affils[$user->getPHID()] = true;
|
||||||
|
|
||||||
|
$xaction = new PhabricatorProjectTransaction();
|
||||||
|
$xaction->setTransactionType(
|
||||||
|
PhabricatorProjectTransactionType::TYPE_MEMBERS);
|
||||||
|
$xaction->setNewValue(array_keys($affils));
|
||||||
|
$xactions[] = $xaction;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'leave':
|
||||||
|
$affils = $project->loadAffiliations();
|
||||||
|
$affils = mpull($affils, null, 'getUserPHID');
|
||||||
|
if (isset($affils[$user->getPHID()])) {
|
||||||
|
unset($affils[$user->getPHID()]);
|
||||||
|
|
||||||
|
$xaction = new PhabricatorProjectTransaction();
|
||||||
|
$xaction->setTransactionType(
|
||||||
|
PhabricatorProjectTransactionType::TYPE_MEMBERS);
|
||||||
|
$xaction->setNewValue(array_keys($affils));
|
||||||
|
$xactions[] = $xaction;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($xactions) {
|
||||||
|
$editor = new PhabricatorProjectEditor($project);
|
||||||
|
$editor->setUser($user);
|
||||||
|
$editor->applyTransactions($xactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())->setURI($project_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialog = null;
|
||||||
|
switch ($this->action) {
|
||||||
|
case 'leave':
|
||||||
|
$dialog = new AphrontDialogView();
|
||||||
|
$dialog->setUser($user);
|
||||||
|
$dialog->setTitle('Really leave project?');
|
||||||
|
$dialog->appendChild(
|
||||||
|
'<p>Your tremendous contributions to this project will be sorely '.
|
||||||
|
'missed. Are you sure you want to leave?</p>');
|
||||||
|
$dialog->addCancelButton($project_uri);
|
||||||
|
$dialog->addSubmitButton('Leave Project');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/applications/project/controller/update/__init__.php
Normal file
22
src/applications/project/controller/update/__init__.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/404');
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/dialog');
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||||
|
phutil_require_module('phabricator', 'applications/project/constants/transaction');
|
||||||
|
phutil_require_module('phabricator', 'applications/project/controller/base');
|
||||||
|
phutil_require_module('phabricator', 'applications/project/editor/project');
|
||||||
|
phutil_require_module('phabricator', 'applications/project/storage/project');
|
||||||
|
phutil_require_module('phabricator', 'applications/project/storage/transaction');
|
||||||
|
phutil_require_module('phabricator', 'view/dialog');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorProjectUpdateController.php');
|
||||||
@@ -134,7 +134,7 @@ final class PhabricatorProjectEditor {
|
|||||||
|
|
||||||
$old_value = mpull($affils, 'getUserPHID');
|
$old_value = mpull($affils, 'getUserPHID');
|
||||||
$old_value = array_values($old_value);
|
$old_value = array_values($old_value);
|
||||||
$xaction->setOldValue($affils);
|
$xaction->setOldValue($old_value);
|
||||||
|
|
||||||
$new_value = $xaction->getNewValue();
|
$new_value = $xaction->getNewValue();
|
||||||
$new_value = array_filter($new_value);
|
$new_value = array_filter($new_value);
|
||||||
@@ -174,6 +174,7 @@ final class PhabricatorProjectEditor {
|
|||||||
foreach ($new as $phid => $ignored) {
|
foreach ($new as $phid => $ignored) {
|
||||||
if (empty($old[$phid])) {
|
if (empty($old[$phid])) {
|
||||||
$affil = new PhabricatorProjectAffiliation();
|
$affil = new PhabricatorProjectAffiliation();
|
||||||
|
$affil->setRole('');
|
||||||
$affil->setUserPHID($phid);
|
$affil->setUserPHID($phid);
|
||||||
$add[] = $affil;
|
$add[] = $affil;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ final class PhabricatorProfileHeaderView extends AphrontView {
|
|||||||
protected $profilePicture;
|
protected $profilePicture;
|
||||||
protected $profileName;
|
protected $profileName;
|
||||||
protected $profileDescription;
|
protected $profileDescription;
|
||||||
|
protected $profileActions = array();
|
||||||
|
|
||||||
public function setProfilePicture($picture) {
|
public function setProfilePicture($picture) {
|
||||||
$this->profilePicture = $picture;
|
$this->profilePicture = $picture;
|
||||||
@@ -37,6 +38,11 @@ final class PhabricatorProfileHeaderView extends AphrontView {
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addAction($action) {
|
||||||
|
$this->profileActions[] = $action;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function render() {
|
public function render() {
|
||||||
require_celerity_resource('phabricator-profile-header-css');
|
require_celerity_resource('phabricator-profile-header-css');
|
||||||
|
|
||||||
@@ -57,6 +63,9 @@ final class PhabricatorProfileHeaderView extends AphrontView {
|
|||||||
<td class="profile-header-name">'.
|
<td class="profile-header-name">'.
|
||||||
phutil_escape_html($this->profileName).
|
phutil_escape_html($this->profileName).
|
||||||
'</td>
|
'</td>
|
||||||
|
<td class="profile-header-actions" rowspan="2">'.
|
||||||
|
self::renderSingleView($this->profileActions).
|
||||||
|
'</td>
|
||||||
<td class="profile-header-picture" rowspan="2">'.
|
<td class="profile-header-picture" rowspan="2">'.
|
||||||
$image.
|
$image.
|
||||||
'</td>
|
'</td>
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phabricator-profile-header .profile-header-actions {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.phabricator-profile-header .profile-header-picture-frame {
|
.phabricator-profile-header .profile-header-picture-frame {
|
||||||
margin: 11px;
|
margin: 11px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
|||||||
Reference in New Issue
Block a user