Make project membership edits use transactions and PHIDs and not be awful
Summary: - Split project profile editing apart from project membership editing. - Make project membership editing simpler and easier to use. - Drop role / owner stuff from the UI. Test Plan: Added and removed project members. Edited project profile information. Reviewers: vrana, btrahan Reviewed By: vrana CC: aran Maniphest Tasks: T603 Differential Revision: https://secure.phabricator.com/D3184
This commit is contained in:
@@ -36,6 +36,7 @@ final class PhabricatorApplicationProject extends PhabricatorApplication {
|
||||
'' => 'PhabricatorProjectListController',
|
||||
'filter/(?P<filter>[^/]+)/' => 'PhabricatorProjectListController',
|
||||
'edit/(?P<id>\d+)/' => 'PhabricatorProjectProfileEditController',
|
||||
'members/(?P<id>\d+)/' => 'PhabricatorProjectMembersEditController',
|
||||
'view/(?P<id>\d+)/(?:(?P<page>\w+)/)?'
|
||||
=> 'PhabricatorProjectProfileController',
|
||||
'create/' => 'PhabricatorProjectCreateController',
|
||||
|
||||
@@ -31,4 +31,33 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
||||
return $response->setContent($page->render());
|
||||
}
|
||||
|
||||
protected function buildLocalNavigation(PhabricatorProject $project) {
|
||||
$id = $project->getID();
|
||||
|
||||
$nav_view = new AphrontSideNavFilterView();
|
||||
$uri = new PhutilURI('/project/view/'.$id.'/');
|
||||
$nav_view->setBaseURI($uri);
|
||||
|
||||
$external_arrow = "\xE2\x86\x97";
|
||||
$tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID();
|
||||
$slug = PhabricatorSlug::normalize($project->getName());
|
||||
$phriction_uri = '/w/projects/'.$slug;
|
||||
|
||||
$edit_uri = '/project/edit/'.$id.'/';
|
||||
$members_uri = '/project/members/'.$id.'/';
|
||||
|
||||
$nav_view->addFilter('dashboard', 'Dashboard');
|
||||
$nav_view->addSpacer();
|
||||
$nav_view->addFilter('feed', 'Feed');
|
||||
$nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri);
|
||||
$nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri);
|
||||
$nav_view->addFilter('people', 'People');
|
||||
$nav_view->addFilter('about', 'About');
|
||||
$nav_view->addSpacer();
|
||||
$nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri);
|
||||
$nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri);
|
||||
|
||||
return $nav_view;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
final class PhabricatorProjectMembersEditController
|
||||
extends PhabricatorProjectController {
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->id = $data['id'];
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$project = id(new PhabricatorProject())->load($this->id);
|
||||
if (!$project) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
$profile = $project->loadProfile();
|
||||
if (empty($profile)) {
|
||||
$profile = new PhabricatorProjectProfile();
|
||||
}
|
||||
|
||||
$member_phids = $project->loadMemberPHIDs();
|
||||
|
||||
$errors = array();
|
||||
if ($request->isFormPost()) {
|
||||
$changed_something = false;
|
||||
$member_map = array_fill_keys($member_phids, true);
|
||||
|
||||
$remove = $request->getStr('remove');
|
||||
if ($remove) {
|
||||
if (isset($member_map[$remove])) {
|
||||
unset($member_map[$remove]);
|
||||
$changed_something = true;
|
||||
}
|
||||
} else {
|
||||
$new_members = $request->getArr('phids');
|
||||
foreach ($new_members as $member) {
|
||||
if (empty($member_map[$member])) {
|
||||
$member_map[$member] = true;
|
||||
$changed_something = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$xactions = array();
|
||||
if ($changed_something) {
|
||||
$xaction = new PhabricatorProjectTransaction();
|
||||
$xaction->setTransactionType(
|
||||
PhabricatorProjectTransactionType::TYPE_MEMBERS);
|
||||
$xaction->setNewValue(array_keys($member_map));
|
||||
$xactions[] = $xaction;
|
||||
}
|
||||
|
||||
if ($xactions) {
|
||||
$editor = new PhabricatorProjectEditor($project);
|
||||
$editor->setUser($user);
|
||||
$editor->applyTransactions($xactions);
|
||||
}
|
||||
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI($request->getRequestURI());
|
||||
}
|
||||
|
||||
$member_phids = array_reverse($member_phids);
|
||||
$handles = id(new PhabricatorObjectHandleData($member_phids))
|
||||
->loadHandles();
|
||||
|
||||
$state = array();
|
||||
foreach ($handles as $handle) {
|
||||
$state[] = array(
|
||||
'phid' => $handle->getPHID(),
|
||||
'name' => $handle->getFullName(),
|
||||
);
|
||||
}
|
||||
|
||||
$header_name = 'Edit Members';
|
||||
$title = 'Edit Members';
|
||||
|
||||
$list = $this->renderMemberList($handles);
|
||||
|
||||
$form = new AphrontFormView();
|
||||
$form
|
||||
->setUser($user)
|
||||
->appendChild(
|
||||
id(new AphrontFormTokenizerControl())
|
||||
->setName('phids')
|
||||
->setLabel('Add Members')
|
||||
->setDatasource('/typeahead/common/users/'))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->addCancelButton('/project/view/'.$project->getID().'/')
|
||||
->setValue('Add Members'));
|
||||
$faux_form = id(new AphrontFormLayoutView())
|
||||
->setBackgroundShading(true)
|
||||
->setPadded(true)
|
||||
->appendChild(
|
||||
id(new AphrontFormInsetView())
|
||||
->setTitle('Current Members ('.count($handles).')')
|
||||
->appendChild($list));
|
||||
|
||||
$panel = new AphrontPanelView();
|
||||
$panel->setHeader($header_name);
|
||||
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
|
||||
$panel->appendChild($form);
|
||||
$panel->appendChild('<br />');
|
||||
$panel->appendChild($faux_form);
|
||||
|
||||
$nav = $this->buildLocalNavigation($project);
|
||||
$nav->selectFilter('members');
|
||||
$nav->appendChild($panel);
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$nav,
|
||||
array(
|
||||
'title' => $title,
|
||||
));
|
||||
}
|
||||
|
||||
private function renderMemberList(array $handles) {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
$list = id(new PhabricatorObjectListView())
|
||||
->setHandles($handles);
|
||||
|
||||
foreach ($handles as $handle) {
|
||||
$hidden_input = phutil_render_tag(
|
||||
'input',
|
||||
array(
|
||||
'type' => 'hidden',
|
||||
'name' => 'remove',
|
||||
'value' => $handle->getPHID(),
|
||||
),
|
||||
'');
|
||||
|
||||
$button = javelin_render_tag(
|
||||
'button',
|
||||
array(
|
||||
'class' => 'grey',
|
||||
),
|
||||
pht('Remove'));
|
||||
|
||||
$list->addButton(
|
||||
$handle,
|
||||
phabricator_render_form(
|
||||
$user,
|
||||
array(
|
||||
'method' => 'POST',
|
||||
'action' => $request->getRequestURI(),
|
||||
),
|
||||
$hidden_input.$button));
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -44,26 +44,7 @@ final class PhabricatorProjectProfileController
|
||||
$members = $project->loadMemberPHIDs();
|
||||
$member_map = array_fill_keys($members, true);
|
||||
|
||||
$nav_view = new AphrontSideNavFilterView();
|
||||
$uri = new PhutilURI('/project/view/'.$project->getID().'/');
|
||||
$nav_view->setBaseURI($uri);
|
||||
|
||||
$external_arrow = "\xE2\x86\x97";
|
||||
$tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID();
|
||||
$slug = PhabricatorSlug::normalize($project->getName());
|
||||
$phriction_uri = '/w/projects/'.$slug;
|
||||
|
||||
$edit_uri = '/project/edit/'.$project->getID().'/';
|
||||
|
||||
$nav_view->addFilter('dashboard', 'Dashboard');
|
||||
$nav_view->addSpacer();
|
||||
$nav_view->addFilter('feed', 'Feed');
|
||||
$nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri);
|
||||
$nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri);
|
||||
$nav_view->addFilter('people', 'People');
|
||||
$nav_view->addFilter('about', 'About');
|
||||
$nav_view->addSpacer();
|
||||
$nav_view->addFilter(null, "Edit Project\xE2\x80\xA6", $edit_uri);
|
||||
$nav_view = $this->buildLocalNavigation($project);
|
||||
|
||||
$this->page = $nav_view->selectFilter($this->page, 'dashboard');
|
||||
|
||||
|
||||
@@ -41,18 +41,13 @@ final class PhabricatorProjectProfileEditController
|
||||
|
||||
$options = PhabricatorProjectStatus::getStatusMap();
|
||||
|
||||
$affiliations = $project->loadAffiliations();
|
||||
$affiliations = mpull($affiliations, null, 'getUserPHID');
|
||||
|
||||
$supported_formats = PhabricatorFile::getTransformableImageFormats();
|
||||
|
||||
$e_name = true;
|
||||
$e_image = null;
|
||||
|
||||
$errors = array();
|
||||
$state = null;
|
||||
if ($request->isFormPost()) {
|
||||
|
||||
try {
|
||||
$xactions = array();
|
||||
$xaction = new PhabricatorProjectTransaction();
|
||||
@@ -112,94 +107,12 @@ final class PhabricatorProjectProfileEditController
|
||||
}
|
||||
}
|
||||
|
||||
$resources = $request->getStr('resources');
|
||||
$resources = json_decode($resources, true);
|
||||
if (!is_array($resources)) {
|
||||
throw new Exception(
|
||||
"Project resource information was not correctly encoded in the ".
|
||||
"request.");
|
||||
}
|
||||
|
||||
$state = array();
|
||||
foreach ($resources as $resource) {
|
||||
$user_phid = $resource['phid'];
|
||||
if (!$user_phid) {
|
||||
continue;
|
||||
}
|
||||
if (isset($state[$user_phid])) {
|
||||
// TODO: We should deal with this better -- the user has entered
|
||||
// the same resource more than once.
|
||||
}
|
||||
$state[$user_phid] = array(
|
||||
'phid' => $user_phid,
|
||||
'role' => $resource['role'],
|
||||
'owner' => $resource['owner'],
|
||||
);
|
||||
}
|
||||
|
||||
$all_phids = array_merge(array_keys($state), array_keys($affiliations));
|
||||
$all_phids = array_unique($all_phids);
|
||||
|
||||
$delete_affiliations = array();
|
||||
$save_affiliations = array();
|
||||
foreach ($all_phids as $phid) {
|
||||
$old = idx($affiliations, $phid);
|
||||
$new = idx($state, $phid);
|
||||
|
||||
if ($old && !$new) {
|
||||
$delete_affiliations[] = $affiliations[$phid];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$old) {
|
||||
$affil = new PhabricatorProjectAffiliation();
|
||||
$affil->setUserPHID($phid);
|
||||
} else {
|
||||
$affil = $old;
|
||||
}
|
||||
|
||||
$affil->setRole((string)$new['role']);
|
||||
$affil->setIsOwner((int)$new['owner']);
|
||||
|
||||
$save_affiliations[] = $affil;
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$project->save();
|
||||
$profile->setProjectPHID($project->getPHID());
|
||||
$profile->save();
|
||||
|
||||
foreach ($delete_affiliations as $affil) {
|
||||
$affil->delete();
|
||||
}
|
||||
|
||||
foreach ($save_affiliations as $save) {
|
||||
$save->setProjectPHID($project->getPHID());
|
||||
$save->save();
|
||||
}
|
||||
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI('/project/view/'.$project->getID().'/');
|
||||
} else {
|
||||
$phids = array_keys($state);
|
||||
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
|
||||
foreach ($state as $phid => $info) {
|
||||
$state[$phid]['name'] = $handles[$phid]->getFullName();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$phids = mpull($affiliations, 'getUserPHID');
|
||||
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
|
||||
|
||||
$state = array();
|
||||
foreach ($affiliations as $affil) {
|
||||
$user_phid = $affil->getUserPHID();
|
||||
$state[] = array(
|
||||
'phid' => $user_phid,
|
||||
'name' => $handles[$user_phid]->getFullName(),
|
||||
'role' => $affil->getRole(),
|
||||
'owner' => $affil->getIsOwner(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,8 +127,6 @@ final class PhabricatorProjectProfileEditController
|
||||
$title = 'Edit Project';
|
||||
$action = '/project/edit/'.$project->getID().'/';
|
||||
|
||||
require_celerity_resource('project-edit-css');
|
||||
|
||||
$form = new AphrontFormView();
|
||||
$form
|
||||
->setID('project-edit-form')
|
||||
@@ -254,61 +165,26 @@ final class PhabricatorProjectProfileEditController
|
||||
->setName('image')
|
||||
->setError($e_image)
|
||||
->setCaption('Supported formats: '.implode(', ', $supported_formats)))
|
||||
->appendChild(
|
||||
id(new AphrontFormInsetView())
|
||||
->setTitle('Resources')
|
||||
->setRightButton(javelin_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '#',
|
||||
'class' => 'button green',
|
||||
'sigil' => 'add-resource',
|
||||
'mustcapture' => true,
|
||||
),
|
||||
'Add New Resource'))
|
||||
->appendChild(
|
||||
phutil_render_tag(
|
||||
'input',
|
||||
array(
|
||||
'type' => 'hidden',
|
||||
'name' => 'resources',
|
||||
'id' => 'resources',
|
||||
)))
|
||||
->setContent(javelin_render_tag(
|
||||
'table',
|
||||
array(
|
||||
'sigil' => 'resources',
|
||||
'class' => 'project-resource-table',
|
||||
),
|
||||
'')))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->addCancelButton('/project/view/'.$project->getID().'/')
|
||||
->setValue('Save'));
|
||||
|
||||
$template = new AphrontTokenizerTemplateView();
|
||||
$template = $template->render();
|
||||
|
||||
Javelin::initBehavior(
|
||||
'projects-resource-editor',
|
||||
array(
|
||||
'root' => 'project-edit-form',
|
||||
'tokenizerTemplate' => $template,
|
||||
'tokenizerSource' => '/typeahead/common/users/',
|
||||
'input' => 'resources',
|
||||
'state' => array_values($state),
|
||||
));
|
||||
|
||||
$panel = new AphrontPanelView();
|
||||
$panel->setHeader($header_name);
|
||||
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
|
||||
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
|
||||
$panel->appendChild($form);
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$nav = $this->buildLocalNavigation($project);
|
||||
$nav->selectFilter('edit');
|
||||
$nav->appendChild(
|
||||
array(
|
||||
$error_view,
|
||||
$panel,
|
||||
),
|
||||
));
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$nav,
|
||||
array(
|
||||
'title' => $title,
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user