@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+319 -325
+9 -27
src/__celerity_resource_map__.php
··· 1621 1621 ), 1622 1622 'disk' => '/rsrc/js/application/phriction/phriction-document-preview.js', 1623 1623 ), 1624 - 'javelin-behavior-projects-resource-editor' => 1625 - array( 1626 - 'uri' => '/res/ffdde7d9/rsrc/js/application/projects/projects-resource-editor.js', 1627 - 'type' => 'js', 1628 - 'requires' => 1629 - array( 1630 - 0 => 'javelin-behavior', 1631 - 1 => 'phabricator-prefab', 1632 - 2 => 'multirow-row-manager', 1633 - 3 => 'javelin-tokenizer', 1634 - 4 => 'javelin-typeahead-preloaded-source', 1635 - 5 => 'javelin-typeahead', 1636 - 6 => 'javelin-dom', 1637 - 7 => 'javelin-json', 1638 - 8 => 'javelin-util', 1639 - ), 1640 - 'disk' => '/rsrc/js/application/projects/projects-resource-editor.js', 1641 - ), 1642 1624 'javelin-behavior-refresh-csrf' => 1643 1625 array( 1644 1626 'uri' => '/res/88beba4c/rsrc/js/application/core/behavior-refresh-csrf.js', ··· 2439 2421 ), 2440 2422 'disk' => '/rsrc/css/application/base/notification-menu.css', 2441 2423 ), 2424 + 'phabricator-object-list-view-css' => 2425 + array( 2426 + 'uri' => '/res/4e060838/rsrc/css/application/projects/phabricator-object-list-view.css', 2427 + 'type' => 'css', 2428 + 'requires' => 2429 + array( 2430 + ), 2431 + 'disk' => '/rsrc/css/application/projects/phabricator-object-list-view.css', 2432 + ), 2442 2433 'phabricator-object-selector-css' => 2443 2434 array( 2444 2435 'uri' => '/res/7eb4c705/rsrc/css/application/objectselector/object-selector.css', ··· 2738 2729 array( 2739 2730 ), 2740 2731 'disk' => '/rsrc/css/application/phriction/phriction-document-css.css', 2741 - ), 2742 - 'project-edit-css' => 2743 - array( 2744 - 'uri' => '/res/c192b5f9/rsrc/css/application/projects/project-edit.css', 2745 - 'type' => 'css', 2746 - 'requires' => 2747 - array( 2748 - ), 2749 - 'disk' => '/rsrc/css/application/projects/project-edit.css', 2750 2732 ), 2751 2733 'raphael-core' => 2752 2734 array(
+4
src/__phutil_library_map__.php
··· 852 852 'PhabricatorObjectHandleConstants' => 'applications/phid/handle/const/PhabricatorObjectHandleConstants.php', 853 853 'PhabricatorObjectHandleData' => 'applications/phid/handle/PhabricatorObjectHandleData.php', 854 854 'PhabricatorObjectHandleStatus' => 'applications/phid/handle/const/PhabricatorObjectHandleStatus.php', 855 + 'PhabricatorObjectListView' => 'view/control/PhabricatorObjectListView.php', 855 856 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 856 857 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', 857 858 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', ··· 900 901 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 901 902 'PhabricatorProjectEditor' => 'applications/project/editor/PhabricatorProjectEditor.php', 902 903 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 904 + 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 903 905 'PhabricatorProjectNameCollisionException' => 'applications/project/exception/PhabricatorProjectNameCollisionException.php', 904 906 'PhabricatorProjectProfile' => 'applications/project/storage/PhabricatorProjectProfile.php', 905 907 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', ··· 1892 1894 'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController', 1893 1895 'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController', 1894 1896 'PhabricatorObjectHandleStatus' => 'PhabricatorObjectHandleConstants', 1897 + 'PhabricatorObjectListView' => 'AphrontView', 1895 1898 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', 1896 1899 'PhabricatorOwnersController' => 'PhabricatorController', 1897 1900 'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO', ··· 1935 1938 'PhabricatorProjectCreateController' => 'PhabricatorProjectController', 1936 1939 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 1937 1940 'PhabricatorProjectListController' => 'PhabricatorProjectController', 1941 + 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 1938 1942 'PhabricatorProjectNameCollisionException' => 'Exception', 1939 1943 'PhabricatorProjectProfile' => 'PhabricatorProjectDAO', 1940 1944 'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
+1
src/applications/project/application/PhabricatorApplicationProject.php
··· 36 36 '' => 'PhabricatorProjectListController', 37 37 'filter/(?P<filter>[^/]+)/' => 'PhabricatorProjectListController', 38 38 'edit/(?P<id>\d+)/' => 'PhabricatorProjectProfileEditController', 39 + 'members/(?P<id>\d+)/' => 'PhabricatorProjectMembersEditController', 39 40 'view/(?P<id>\d+)/(?:(?P<page>\w+)/)?' 40 41 => 'PhabricatorProjectProfileController', 41 42 'create/' => 'PhabricatorProjectCreateController',
+29
src/applications/project/controller/PhabricatorProjectController.php
··· 31 31 return $response->setContent($page->render()); 32 32 } 33 33 34 + protected function buildLocalNavigation(PhabricatorProject $project) { 35 + $id = $project->getID(); 36 + 37 + $nav_view = new AphrontSideNavFilterView(); 38 + $uri = new PhutilURI('/project/view/'.$id.'/'); 39 + $nav_view->setBaseURI($uri); 40 + 41 + $external_arrow = "\xE2\x86\x97"; 42 + $tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID(); 43 + $slug = PhabricatorSlug::normalize($project->getName()); 44 + $phriction_uri = '/w/projects/'.$slug; 45 + 46 + $edit_uri = '/project/edit/'.$id.'/'; 47 + $members_uri = '/project/members/'.$id.'/'; 48 + 49 + $nav_view->addFilter('dashboard', 'Dashboard'); 50 + $nav_view->addSpacer(); 51 + $nav_view->addFilter('feed', 'Feed'); 52 + $nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri); 53 + $nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri); 54 + $nav_view->addFilter('people', 'People'); 55 + $nav_view->addFilter('about', 'About'); 56 + $nav_view->addSpacer(); 57 + $nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri); 58 + $nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri); 59 + 60 + return $nav_view; 61 + } 62 + 34 63 }
+172
src/applications/project/controller/PhabricatorProjectMembersEditController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorProjectMembersEditController 20 + extends PhabricatorProjectController { 21 + 22 + public function willProcessRequest(array $data) { 23 + $this->id = $data['id']; 24 + } 25 + 26 + public function processRequest() { 27 + $request = $this->getRequest(); 28 + $user = $request->getUser(); 29 + 30 + $project = id(new PhabricatorProject())->load($this->id); 31 + if (!$project) { 32 + return new Aphront404Response(); 33 + } 34 + $profile = $project->loadProfile(); 35 + if (empty($profile)) { 36 + $profile = new PhabricatorProjectProfile(); 37 + } 38 + 39 + $member_phids = $project->loadMemberPHIDs(); 40 + 41 + $errors = array(); 42 + if ($request->isFormPost()) { 43 + $changed_something = false; 44 + $member_map = array_fill_keys($member_phids, true); 45 + 46 + $remove = $request->getStr('remove'); 47 + if ($remove) { 48 + if (isset($member_map[$remove])) { 49 + unset($member_map[$remove]); 50 + $changed_something = true; 51 + } 52 + } else { 53 + $new_members = $request->getArr('phids'); 54 + foreach ($new_members as $member) { 55 + if (empty($member_map[$member])) { 56 + $member_map[$member] = true; 57 + $changed_something = true; 58 + } 59 + } 60 + } 61 + 62 + $xactions = array(); 63 + if ($changed_something) { 64 + $xaction = new PhabricatorProjectTransaction(); 65 + $xaction->setTransactionType( 66 + PhabricatorProjectTransactionType::TYPE_MEMBERS); 67 + $xaction->setNewValue(array_keys($member_map)); 68 + $xactions[] = $xaction; 69 + } 70 + 71 + if ($xactions) { 72 + $editor = new PhabricatorProjectEditor($project); 73 + $editor->setUser($user); 74 + $editor->applyTransactions($xactions); 75 + } 76 + 77 + return id(new AphrontRedirectResponse()) 78 + ->setURI($request->getRequestURI()); 79 + } 80 + 81 + $member_phids = array_reverse($member_phids); 82 + $handles = id(new PhabricatorObjectHandleData($member_phids)) 83 + ->loadHandles(); 84 + 85 + $state = array(); 86 + foreach ($handles as $handle) { 87 + $state[] = array( 88 + 'phid' => $handle->getPHID(), 89 + 'name' => $handle->getFullName(), 90 + ); 91 + } 92 + 93 + $header_name = 'Edit Members'; 94 + $title = 'Edit Members'; 95 + 96 + $list = $this->renderMemberList($handles); 97 + 98 + $form = new AphrontFormView(); 99 + $form 100 + ->setUser($user) 101 + ->appendChild( 102 + id(new AphrontFormTokenizerControl()) 103 + ->setName('phids') 104 + ->setLabel('Add Members') 105 + ->setDatasource('/typeahead/common/users/')) 106 + ->appendChild( 107 + id(new AphrontFormSubmitControl()) 108 + ->addCancelButton('/project/view/'.$project->getID().'/') 109 + ->setValue('Add Members')); 110 + $faux_form = id(new AphrontFormLayoutView()) 111 + ->setBackgroundShading(true) 112 + ->setPadded(true) 113 + ->appendChild( 114 + id(new AphrontFormInsetView()) 115 + ->setTitle('Current Members ('.count($handles).')') 116 + ->appendChild($list)); 117 + 118 + $panel = new AphrontPanelView(); 119 + $panel->setHeader($header_name); 120 + $panel->setWidth(AphrontPanelView::WIDTH_FORM); 121 + $panel->appendChild($form); 122 + $panel->appendChild('<br />'); 123 + $panel->appendChild($faux_form); 124 + 125 + $nav = $this->buildLocalNavigation($project); 126 + $nav->selectFilter('members'); 127 + $nav->appendChild($panel); 128 + 129 + return $this->buildStandardPageResponse( 130 + $nav, 131 + array( 132 + 'title' => $title, 133 + )); 134 + } 135 + 136 + private function renderMemberList(array $handles) { 137 + $request = $this->getRequest(); 138 + $user = $request->getUser(); 139 + $list = id(new PhabricatorObjectListView()) 140 + ->setHandles($handles); 141 + 142 + foreach ($handles as $handle) { 143 + $hidden_input = phutil_render_tag( 144 + 'input', 145 + array( 146 + 'type' => 'hidden', 147 + 'name' => 'remove', 148 + 'value' => $handle->getPHID(), 149 + ), 150 + ''); 151 + 152 + $button = javelin_render_tag( 153 + 'button', 154 + array( 155 + 'class' => 'grey', 156 + ), 157 + pht('Remove')); 158 + 159 + $list->addButton( 160 + $handle, 161 + phabricator_render_form( 162 + $user, 163 + array( 164 + 'method' => 'POST', 165 + 'action' => $request->getRequestURI(), 166 + ), 167 + $hidden_input.$button)); 168 + } 169 + 170 + return $list; 171 + } 172 + }
+1 -20
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 44 44 $members = $project->loadMemberPHIDs(); 45 45 $member_map = array_fill_keys($members, true); 46 46 47 - $nav_view = new AphrontSideNavFilterView(); 48 - $uri = new PhutilURI('/project/view/'.$project->getID().'/'); 49 - $nav_view->setBaseURI($uri); 50 - 51 - $external_arrow = "\xE2\x86\x97"; 52 - $tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID(); 53 - $slug = PhabricatorSlug::normalize($project->getName()); 54 - $phriction_uri = '/w/projects/'.$slug; 55 - 56 - $edit_uri = '/project/edit/'.$project->getID().'/'; 57 - 58 - $nav_view->addFilter('dashboard', 'Dashboard'); 59 - $nav_view->addSpacer(); 60 - $nav_view->addFilter('feed', 'Feed'); 61 - $nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri); 62 - $nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri); 63 - $nav_view->addFilter('people', 'People'); 64 - $nav_view->addFilter('about', 'About'); 65 - $nav_view->addSpacer(); 66 - $nav_view->addFilter(null, "Edit Project\xE2\x80\xA6", $edit_uri); 47 + $nav_view = $this->buildLocalNavigation($project); 67 48 68 49 $this->page = $nav_view->selectFilter($this->page, 'dashboard'); 69 50
+8 -132
src/applications/project/controller/PhabricatorProjectProfileEditController.php
··· 41 41 42 42 $options = PhabricatorProjectStatus::getStatusMap(); 43 43 44 - $affiliations = $project->loadAffiliations(); 45 - $affiliations = mpull($affiliations, null, 'getUserPHID'); 46 - 47 44 $supported_formats = PhabricatorFile::getTransformableImageFormats(); 48 45 49 46 $e_name = true; 50 47 $e_image = null; 51 48 52 49 $errors = array(); 53 - $state = null; 54 50 if ($request->isFormPost()) { 55 - 56 51 try { 57 52 $xactions = array(); 58 53 $xaction = new PhabricatorProjectTransaction(); ··· 112 107 } 113 108 } 114 109 115 - $resources = $request->getStr('resources'); 116 - $resources = json_decode($resources, true); 117 - if (!is_array($resources)) { 118 - throw new Exception( 119 - "Project resource information was not correctly encoded in the ". 120 - "request."); 121 - } 122 - 123 - $state = array(); 124 - foreach ($resources as $resource) { 125 - $user_phid = $resource['phid']; 126 - if (!$user_phid) { 127 - continue; 128 - } 129 - if (isset($state[$user_phid])) { 130 - // TODO: We should deal with this better -- the user has entered 131 - // the same resource more than once. 132 - } 133 - $state[$user_phid] = array( 134 - 'phid' => $user_phid, 135 - 'role' => $resource['role'], 136 - 'owner' => $resource['owner'], 137 - ); 138 - } 139 - 140 - $all_phids = array_merge(array_keys($state), array_keys($affiliations)); 141 - $all_phids = array_unique($all_phids); 142 - 143 - $delete_affiliations = array(); 144 - $save_affiliations = array(); 145 - foreach ($all_phids as $phid) { 146 - $old = idx($affiliations, $phid); 147 - $new = idx($state, $phid); 148 - 149 - if ($old && !$new) { 150 - $delete_affiliations[] = $affiliations[$phid]; 151 - continue; 152 - } 153 - 154 - if (!$old) { 155 - $affil = new PhabricatorProjectAffiliation(); 156 - $affil->setUserPHID($phid); 157 - } else { 158 - $affil = $old; 159 - } 160 - 161 - $affil->setRole((string)$new['role']); 162 - $affil->setIsOwner((int)$new['owner']); 163 - 164 - $save_affiliations[] = $affil; 165 - } 166 - 167 110 if (!$errors) { 168 111 $project->save(); 169 112 $profile->setProjectPHID($project->getPHID()); 170 113 $profile->save(); 171 - 172 - foreach ($delete_affiliations as $affil) { 173 - $affil->delete(); 174 - } 175 - 176 - foreach ($save_affiliations as $save) { 177 - $save->setProjectPHID($project->getPHID()); 178 - $save->save(); 179 - } 180 - 181 114 return id(new AphrontRedirectResponse()) 182 115 ->setURI('/project/view/'.$project->getID().'/'); 183 - } else { 184 - $phids = array_keys($state); 185 - $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); 186 - foreach ($state as $phid => $info) { 187 - $state[$phid]['name'] = $handles[$phid]->getFullName(); 188 - } 189 - } 190 - } else { 191 - $phids = mpull($affiliations, 'getUserPHID'); 192 - $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); 193 - 194 - $state = array(); 195 - foreach ($affiliations as $affil) { 196 - $user_phid = $affil->getUserPHID(); 197 - $state[] = array( 198 - 'phid' => $user_phid, 199 - 'name' => $handles[$user_phid]->getFullName(), 200 - 'role' => $affil->getRole(), 201 - 'owner' => $affil->getIsOwner(), 202 - ); 203 116 } 204 117 } 205 118 ··· 214 127 $title = 'Edit Project'; 215 128 $action = '/project/edit/'.$project->getID().'/'; 216 129 217 - require_celerity_resource('project-edit-css'); 218 - 219 130 $form = new AphrontFormView(); 220 131 $form 221 132 ->setID('project-edit-form') ··· 255 166 ->setError($e_image) 256 167 ->setCaption('Supported formats: '.implode(', ', $supported_formats))) 257 168 ->appendChild( 258 - id(new AphrontFormInsetView()) 259 - ->setTitle('Resources') 260 - ->setRightButton(javelin_render_tag( 261 - 'a', 262 - array( 263 - 'href' => '#', 264 - 'class' => 'button green', 265 - 'sigil' => 'add-resource', 266 - 'mustcapture' => true, 267 - ), 268 - 'Add New Resource')) 269 - ->appendChild( 270 - phutil_render_tag( 271 - 'input', 272 - array( 273 - 'type' => 'hidden', 274 - 'name' => 'resources', 275 - 'id' => 'resources', 276 - ))) 277 - ->setContent(javelin_render_tag( 278 - 'table', 279 - array( 280 - 'sigil' => 'resources', 281 - 'class' => 'project-resource-table', 282 - ), 283 - ''))) 284 - ->appendChild( 285 169 id(new AphrontFormSubmitControl()) 286 170 ->addCancelButton('/project/view/'.$project->getID().'/') 287 171 ->setValue('Save')); 288 172 289 - $template = new AphrontTokenizerTemplateView(); 290 - $template = $template->render(); 291 - 292 - Javelin::initBehavior( 293 - 'projects-resource-editor', 294 - array( 295 - 'root' => 'project-edit-form', 296 - 'tokenizerTemplate' => $template, 297 - 'tokenizerSource' => '/typeahead/common/users/', 298 - 'input' => 'resources', 299 - 'state' => array_values($state), 300 - )); 301 - 302 173 $panel = new AphrontPanelView(); 303 174 $panel->setHeader($header_name); 304 - $panel->setWidth(AphrontPanelView::WIDTH_WIDE); 175 + $panel->setWidth(AphrontPanelView::WIDTH_FORM); 305 176 $panel->appendChild($form); 306 177 307 - return $this->buildStandardPageResponse( 178 + $nav = $this->buildLocalNavigation($project); 179 + $nav->selectFilter('edit'); 180 + $nav->appendChild( 308 181 array( 309 182 $error_view, 310 183 $panel, 311 - ), 184 + )); 185 + 186 + return $this->buildStandardPageResponse( 187 + $nav, 312 188 array( 313 189 'title' => $title, 314 190 ));
+68
src/view/control/PhabricatorObjectListView.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorObjectListView extends AphrontView { 20 + 21 + private $handles = array(); 22 + private $buttons = array(); 23 + 24 + public function setHandles(array $handles) { 25 + assert_instances_of($handles, 'PhabricatorObjectHandle'); 26 + 27 + $this->handles = $handles; 28 + return $this; 29 + } 30 + 31 + public function addButton(PhabricatorObjectHandle $handle, $button) { 32 + $this->buttons[$handle->getPHID()][] = $button; 33 + return $this; 34 + } 35 + 36 + public function render() { 37 + $handles = $this->handles; 38 + 39 + require_celerity_resource('phabricator-object-list-view-css'); 40 + 41 + $out = array(); 42 + foreach ($handles as $handle) { 43 + $buttons = idx($this->buttons, $handle->getPHID(), array()); 44 + if ($buttons) { 45 + $buttons = 46 + '<div class="phabricator-object-list-view-buttons">'. 47 + implode('', $buttons). 48 + '</div>'; 49 + } else { 50 + $buttons = null; 51 + } 52 + 53 + $out[] = javelin_render_tag( 54 + 'div', 55 + array( 56 + 'class' => 'phabricator-object-list-view-item', 57 + 'style' => 'background-image: url('.$handle->getImageURI().');', 58 + ), 59 + $handle->renderLink().$buttons); 60 + } 61 + 62 + return 63 + '<div class="phabricator-object-list-view">'. 64 + implode("\n", $out). 65 + '</div>'; 66 + } 67 + 68 + }
+27
webroot/rsrc/css/application/projects/phabricator-object-list-view.css
··· 1 + /** 2 + * @provides phabricator-object-list-view-css 3 + */ 4 + 5 + .phabricator-object-list-view { 6 + max-height: 400px; 7 + overflow-y: auto; 8 + } 9 + 10 + .phabricator-object-list-view-item { 11 + line-height: 60px; 12 + background-repeat: no-repeat; 13 + background-position: 5px 5px; 14 + padding-left: 70px; 15 + position: relative; 16 + } 17 + 18 + .phabricator-object-list-view-item + .phabricator-object-list-view-item { 19 + border-top: 1px solid rgba(0, 0, 0, 0.05); 20 + } 21 + 22 + .phabricator-object-list-view-buttons { 23 + position: absolute; 24 + text-align: right; 25 + top: 0; 26 + right: 0; 27 + }
-30
webroot/rsrc/css/application/projects/project-edit.css
··· 1 - /** 2 - * @provides project-edit-css 3 - */ 4 - 5 - .project-resource-table { 6 - width: 100%; 7 - } 8 - 9 - .project-resource-table td { 10 - padding: 2px 4px; 11 - vertical-align: middle; 12 - } 13 - 14 - .project-resource-table td label { 15 - font-weight: bold; 16 - color: #666666; 17 - text-align: right; 18 - } 19 - 20 - .project-resource-table td.user-tokenizer { 21 - width: 35%; 22 - } 23 - 24 - .project-resource-table td.role-label { 25 - padding-left: 25px; 26 - } 27 - 28 - .project-resource-table td.role input { 29 - width: 300px; 30 - }
-116
webroot/rsrc/js/application/projects/projects-resource-editor.js
··· 1 - /** 2 - * @requires javelin-behavior 3 - * phabricator-prefab 4 - * multirow-row-manager 5 - * javelin-tokenizer 6 - * javelin-typeahead-preloaded-source 7 - * javelin-typeahead 8 - * javelin-dom 9 - * javelin-json 10 - * javelin-util 11 - * @provides javelin-behavior-projects-resource-editor 12 - * @javelin 13 - */ 14 - 15 - JX.behavior('projects-resource-editor', function(config) { 16 - 17 - var root = JX.$(config.root); 18 - var resources_table = JX.DOM.find(root, 'table', 'resources'); 19 - var manager = new JX.MultirowRowManager(resources_table); 20 - var resource_rows = []; 21 - 22 - for (var ii = 0; ii < config.state.length; ii++) { 23 - addRow(config.state[ii]); 24 - } 25 - 26 - function renderRow(data) { 27 - 28 - var template = JX.$N('div', JX.$H(config.tokenizerTemplate)).firstChild; 29 - template.id = ''; 30 - var datasource = new JX.TypeaheadPreloadedSource( 31 - config.tokenizerSource); 32 - var typeahead = new JX.Typeahead(template); 33 - typeahead.setDatasource(datasource); 34 - var tokenizer = new JX.Tokenizer(template); 35 - tokenizer.setTypeahead(typeahead); 36 - tokenizer.setLimit(1); 37 - tokenizer.start(); 38 - 39 - if (data.phid) { 40 - tokenizer.addToken(data.phid, data.name); 41 - } 42 - 43 - var role = JX.$N('input', {type: 'text', value : data.role || ''}); 44 - 45 - var ownership = JX.Prefab.renderSelect( 46 - {0 : 'Nonowner', 1 : 'Owner'}, 47 - data.owner || 0); 48 - 49 - var as_object = function() { 50 - var tokens = tokenizer.getTokens(); 51 - return { 52 - phid : JX.keys(tokens)[0] || null, 53 - role : role.value, 54 - owner : ownership.value 55 - }; 56 - } 57 - 58 - var r = []; 59 - r.push([null, JX.$N('label', {}, 'User:')]); 60 - r.push(['user-tokenizer', template]); 61 - r.push(['role-label', JX.$N('label', {}, 'Role:')]); 62 - r.push(['role', role]); 63 - r.push([null, ownership]); 64 - 65 - for (var ii = 0; ii < r.length; ii++) { 66 - r[ii] = JX.$N('td', {className : r[ii][0]}, r[ii][1]); 67 - } 68 - 69 - return { 70 - nodes : r, 71 - dataCallback : as_object 72 - }; 73 - } 74 - 75 - function onaddresource(e) { 76 - e.kill(); 77 - addRow({}); 78 - } 79 - 80 - function addRow(info) { 81 - var data = renderRow(info); 82 - var row = manager.addRow(data.nodes); 83 - var id = manager.getRowID(row); 84 - 85 - resource_rows[id] = data.dataCallback; 86 - } 87 - 88 - function onsubmit(e) { 89 - var result = []; 90 - for (var ii = 0; ii < resource_rows.length; ii++) { 91 - if (resource_rows[ii]) { 92 - var obj = resource_rows[ii](); 93 - result.push(obj); 94 - } 95 - } 96 - JX.$(config.input).value = JX.JSON.stringify(result); 97 - } 98 - 99 - JX.DOM.listen( 100 - root, 101 - 'click', 102 - 'add-resource', 103 - onaddresource); 104 - 105 - JX.DOM.listen( 106 - root, 107 - 'submit', 108 - null, 109 - onsubmit); 110 - 111 - manager.listen( 112 - 'row-removed', 113 - function(row_id) { 114 - delete resource_rows[row_id]; 115 - }); 116 - });