diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 690aad1ee0..255789312b 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1621,24 +1621,6 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/phriction/phriction-document-preview.js', ), - 'javelin-behavior-projects-resource-editor' => - array( - 'uri' => '/res/ffdde7d9/rsrc/js/application/projects/projects-resource-editor.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'phabricator-prefab', - 2 => 'multirow-row-manager', - 3 => 'javelin-tokenizer', - 4 => 'javelin-typeahead-preloaded-source', - 5 => 'javelin-typeahead', - 6 => 'javelin-dom', - 7 => 'javelin-json', - 8 => 'javelin-util', - ), - 'disk' => '/rsrc/js/application/projects/projects-resource-editor.js', - ), 'javelin-behavior-refresh-csrf' => array( 'uri' => '/res/88beba4c/rsrc/js/application/core/behavior-refresh-csrf.js', @@ -2439,6 +2421,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/base/notification-menu.css', ), + 'phabricator-object-list-view-css' => + array( + 'uri' => '/res/4e060838/rsrc/css/application/projects/phabricator-object-list-view.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/projects/phabricator-object-list-view.css', + ), 'phabricator-object-selector-css' => array( 'uri' => '/res/7eb4c705/rsrc/css/application/objectselector/object-selector.css', @@ -2739,15 +2730,6 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/phriction/phriction-document-css.css', ), - 'project-edit-css' => - array( - 'uri' => '/res/c192b5f9/rsrc/css/application/projects/project-edit.css', - 'type' => 'css', - 'requires' => - array( - ), - 'disk' => '/rsrc/css/application/projects/project-edit.css', - ), 'raphael-core' => array( 'uri' => '/res/3f48575a/rsrc/js/raphael/raphael.js', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5d8cf0c34d..4ee957cb1f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -852,6 +852,7 @@ phutil_register_library_map(array( 'PhabricatorObjectHandleConstants' => 'applications/phid/handle/const/PhabricatorObjectHandleConstants.php', 'PhabricatorObjectHandleData' => 'applications/phid/handle/PhabricatorObjectHandleData.php', 'PhabricatorObjectHandleStatus' => 'applications/phid/handle/const/PhabricatorObjectHandleStatus.php', + 'PhabricatorObjectListView' => 'view/control/PhabricatorObjectListView.php', 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', @@ -900,6 +901,7 @@ phutil_register_library_map(array( 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectEditor' => 'applications/project/editor/PhabricatorProjectEditor.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', + 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 'PhabricatorProjectNameCollisionException' => 'applications/project/exception/PhabricatorProjectNameCollisionException.php', 'PhabricatorProjectProfile' => 'applications/project/storage/PhabricatorProjectProfile.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', @@ -1892,6 +1894,7 @@ phutil_register_library_map(array( 'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController', 'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorObjectHandleStatus' => 'PhabricatorObjectHandleConstants', + 'PhabricatorObjectListView' => 'AphrontView', 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', 'PhabricatorOwnersController' => 'PhabricatorController', 'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO', @@ -1935,6 +1938,7 @@ phutil_register_library_map(array( 'PhabricatorProjectCreateController' => 'PhabricatorProjectController', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectListController' => 'PhabricatorProjectController', + 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectNameCollisionException' => 'Exception', 'PhabricatorProjectProfile' => 'PhabricatorProjectDAO', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php index 216d571efd..b697117143 100644 --- a/src/applications/project/application/PhabricatorApplicationProject.php +++ b/src/applications/project/application/PhabricatorApplicationProject.php @@ -36,6 +36,7 @@ final class PhabricatorApplicationProject extends PhabricatorApplication { '' => 'PhabricatorProjectListController', 'filter/(?P[^/]+)/' => 'PhabricatorProjectListController', 'edit/(?P\d+)/' => 'PhabricatorProjectProfileEditController', + 'members/(?P\d+)/' => 'PhabricatorProjectMembersEditController', 'view/(?P\d+)/(?:(?P\w+)/)?' => 'PhabricatorProjectProfileController', 'create/' => 'PhabricatorProjectCreateController', diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 23226f36a5..5b404a152b 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -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; + } + } diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php new file mode 100644 index 0000000000..02f9a1239f --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php @@ -0,0 +1,172 @@ +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('
'); + $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; + } +} diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 2a5ac7fab9..608cbbe4ab 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -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'); diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php index 6495aa005a..f84ae0d2f3 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php @@ -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, )); diff --git a/src/view/control/PhabricatorObjectListView.php b/src/view/control/PhabricatorObjectListView.php new file mode 100644 index 0000000000..86065d8b28 --- /dev/null +++ b/src/view/control/PhabricatorObjectListView.php @@ -0,0 +1,68 @@ +handles = $handles; + return $this; + } + + public function addButton(PhabricatorObjectHandle $handle, $button) { + $this->buttons[$handle->getPHID()][] = $button; + return $this; + } + + public function render() { + $handles = $this->handles; + + require_celerity_resource('phabricator-object-list-view-css'); + + $out = array(); + foreach ($handles as $handle) { + $buttons = idx($this->buttons, $handle->getPHID(), array()); + if ($buttons) { + $buttons = + '
'. + implode('', $buttons). + '
'; + } else { + $buttons = null; + } + + $out[] = javelin_render_tag( + 'div', + array( + 'class' => 'phabricator-object-list-view-item', + 'style' => 'background-image: url('.$handle->getImageURI().');', + ), + $handle->renderLink().$buttons); + } + + return + '
'. + implode("\n", $out). + '
'; + } + +} diff --git a/webroot/rsrc/css/application/projects/phabricator-object-list-view.css b/webroot/rsrc/css/application/projects/phabricator-object-list-view.css new file mode 100644 index 0000000000..3336794807 --- /dev/null +++ b/webroot/rsrc/css/application/projects/phabricator-object-list-view.css @@ -0,0 +1,27 @@ +/** + * @provides phabricator-object-list-view-css + */ + +.phabricator-object-list-view { + max-height: 400px; + overflow-y: auto; +} + +.phabricator-object-list-view-item { + line-height: 60px; + background-repeat: no-repeat; + background-position: 5px 5px; + padding-left: 70px; + position: relative; +} + +.phabricator-object-list-view-item + .phabricator-object-list-view-item { + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +.phabricator-object-list-view-buttons { + position: absolute; + text-align: right; + top: 0; + right: 0; +} diff --git a/webroot/rsrc/css/application/projects/project-edit.css b/webroot/rsrc/css/application/projects/project-edit.css deleted file mode 100644 index 4847a725d4..0000000000 --- a/webroot/rsrc/css/application/projects/project-edit.css +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @provides project-edit-css - */ - -.project-resource-table { - width: 100%; -} - -.project-resource-table td { - padding: 2px 4px; - vertical-align: middle; -} - -.project-resource-table td label { - font-weight: bold; - color: #666666; - text-align: right; -} - -.project-resource-table td.user-tokenizer { - width: 35%; -} - -.project-resource-table td.role-label { - padding-left: 25px; -} - -.project-resource-table td.role input { - width: 300px; -} diff --git a/webroot/rsrc/js/application/projects/projects-resource-editor.js b/webroot/rsrc/js/application/projects/projects-resource-editor.js deleted file mode 100644 index 31626b8cbc..0000000000 --- a/webroot/rsrc/js/application/projects/projects-resource-editor.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @requires javelin-behavior - * phabricator-prefab - * multirow-row-manager - * javelin-tokenizer - * javelin-typeahead-preloaded-source - * javelin-typeahead - * javelin-dom - * javelin-json - * javelin-util - * @provides javelin-behavior-projects-resource-editor - * @javelin - */ - -JX.behavior('projects-resource-editor', function(config) { - - var root = JX.$(config.root); - var resources_table = JX.DOM.find(root, 'table', 'resources'); - var manager = new JX.MultirowRowManager(resources_table); - var resource_rows = []; - - for (var ii = 0; ii < config.state.length; ii++) { - addRow(config.state[ii]); - } - - function renderRow(data) { - - var template = JX.$N('div', JX.$H(config.tokenizerTemplate)).firstChild; - template.id = ''; - var datasource = new JX.TypeaheadPreloadedSource( - config.tokenizerSource); - var typeahead = new JX.Typeahead(template); - typeahead.setDatasource(datasource); - var tokenizer = new JX.Tokenizer(template); - tokenizer.setTypeahead(typeahead); - tokenizer.setLimit(1); - tokenizer.start(); - - if (data.phid) { - tokenizer.addToken(data.phid, data.name); - } - - var role = JX.$N('input', {type: 'text', value : data.role || ''}); - - var ownership = JX.Prefab.renderSelect( - {0 : 'Nonowner', 1 : 'Owner'}, - data.owner || 0); - - var as_object = function() { - var tokens = tokenizer.getTokens(); - return { - phid : JX.keys(tokens)[0] || null, - role : role.value, - owner : ownership.value - }; - } - - var r = []; - r.push([null, JX.$N('label', {}, 'User:')]); - r.push(['user-tokenizer', template]); - r.push(['role-label', JX.$N('label', {}, 'Role:')]); - r.push(['role', role]); - r.push([null, ownership]); - - for (var ii = 0; ii < r.length; ii++) { - r[ii] = JX.$N('td', {className : r[ii][0]}, r[ii][1]); - } - - return { - nodes : r, - dataCallback : as_object - }; - } - - function onaddresource(e) { - e.kill(); - addRow({}); - } - - function addRow(info) { - var data = renderRow(info); - var row = manager.addRow(data.nodes); - var id = manager.getRowID(row); - - resource_rows[id] = data.dataCallback; - } - - function onsubmit(e) { - var result = []; - for (var ii = 0; ii < resource_rows.length; ii++) { - if (resource_rows[ii]) { - var obj = resource_rows[ii](); - result.push(obj); - } - } - JX.$(config.input).value = JX.JSON.stringify(result); - } - - JX.DOM.listen( - root, - 'click', - 'add-resource', - onaddresource); - - JX.DOM.listen( - root, - 'submit', - null, - onsubmit); - - manager.listen( - 'row-removed', - function(row_id) { - delete resource_rows[row_id]; - }); -});