diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 688f894910..2ef3a110c1 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -258,7 +258,7 @@ celerity_register_resource_map(array( ), 'herald-rule-editor' => array( - 'uri' => '/res/ba957508/rsrc/js/application/herald/HeraldRuleEditor.js', + 'uri' => '/res/4d6dff2b/rsrc/js/application/herald/HeraldRuleEditor.js', 'type' => 'js', 'requires' => array( @@ -271,6 +271,7 @@ celerity_register_resource_map(array( 6 => 'javelin-typeahead-preloaded-source', 7 => 'javelin-stratcom', 8 => 'javelin-json', + 9 => 'phabricator-prefab', ), 'disk' => '/rsrc/js/application/herald/HeraldRuleEditor.js', ), @@ -665,6 +666,24 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/phriction/phriction-document-preview.js', ), + 'javelin-behavior-projects-resource-editor' => + array( + 'uri' => '/res/a54d5616/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/39aa51f7/rsrc/js/application/core/behavior-refresh-csrf.js', @@ -1094,6 +1113,18 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/objectselector/object-selector.css', ), + 'phabricator-prefab' => + array( + 'uri' => '/res/5784a112/rsrc/js/application/core/Prefab.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + ), + 'disk' => '/rsrc/js/application/core/Prefab.js', + ), 'phabricator-profile-css' => array( 'uri' => '/res/ebe1ac2f/rsrc/css/application/profile/profile-view.css', @@ -1169,6 +1200,15 @@ 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', + ), 'syntax-highlighting-css' => array( 'uri' => '/res/e5cc3d88/rsrc/css/core/syntax.css', diff --git a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php index ae9710eca2..db0514a5fb 100644 --- a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php @@ -49,8 +49,12 @@ class PhabricatorProjectProfileEditController $options = PhabricatorProjectStatus::getStatusMap(); + $affiliations = $project->loadAffiliations(); + $affiliations = mpull($affiliations, null, 'getUserPHID'); + $e_name = true; $errors = array(); + $state = null; if ($request->isFormPost()) { $project->setName($request->getStr('name')); $project->setStatus($request->getStr('status')); @@ -89,12 +93,97 @@ 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, + 'status' => $resource['status'], + '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->setStatus((string)$new['status']); + $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(), + 'status' => $affil->getStatus(), + 'role' => $affil->getRole(), + 'owner' => $affil->getIsOwner(), + ); } } @@ -109,8 +198,11 @@ class PhabricatorProjectProfileEditController $title = 'Edit Project'; $action = '/project/edit/'.$project->getID().'/'; + require_celerity_resource('project-edit-css'); + $form = new AphrontFormView(); $form + ->setID('project-edit-form') ->setUser($user) ->setAction($action) ->setEncType('multipart/form-data') @@ -141,11 +233,49 @@ class PhabricatorProjectProfileEditController id(new AphrontFormFileControl()) ->setLabel('Change Image') ->setName('image')) + ->appendChild( + '

Resources

'. + ''. + '
'. + '
'. + javelin_render_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button green', + 'sigil' => 'add-resource', + 'mustcapture' => true, + ), + 'Add New Resource'). + '
'. + '

'. + '
'. + 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); diff --git a/src/applications/project/controller/profileedit/__init__.php b/src/applications/project/controller/profileedit/__init__.php index 46a80365db..560d1d293f 100644 --- a/src/applications/project/controller/profileedit/__init__.php +++ b/src/applications/project/controller/profileedit/__init__.php @@ -13,8 +13,13 @@ phutil_require_module('phabricator', 'applications/files/transform'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'applications/project/constants/status'); phutil_require_module('phabricator', 'applications/project/controller/base'); +phutil_require_module('phabricator', 'applications/project/storage/affiliation'); phutil_require_module('phabricator', 'applications/project/storage/profile'); phutil_require_module('phabricator', 'applications/project/storage/project'); +phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/markup'); +phutil_require_module('phabricator', 'view/control/tokenizer'); phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/control/file'); phutil_require_module('phabricator', 'view/form/control/select'); diff --git a/webroot/rsrc/css/application/projects/project-edit.css b/webroot/rsrc/css/application/projects/project-edit.css new file mode 100644 index 0000000000..4847a725d4 --- /dev/null +++ b/webroot/rsrc/css/application/projects/project-edit.css @@ -0,0 +1,30 @@ +/** + * @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/core/Prefab.js b/webroot/rsrc/js/application/core/Prefab.js new file mode 100644 index 0000000000..fedbb35843 --- /dev/null +++ b/webroot/rsrc/js/application/core/Prefab.js @@ -0,0 +1,28 @@ +/** + * @provides phabricator-prefab + * @requires javelin-install + * javelin-util + * javelin-dom + * @javelin + */ + +/** + * Utilities for client-side rendering (the greatest thing in the world). + */ +JX.install('Prefab', { + + statics : { + renderSelect : function(map, selected, attrs) { + var select = JX.$N('select', attrs || {}); + for (var k in map) { + select.options[select.options.length] = new Option(map[k], k); + if (k == selected) { + select.value = k; + } + } + select.value = select.value || JX.keys(map)[0]; + return select; + } + } + +}); diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js index c058352310..da1aacd672 100644 --- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -8,6 +8,7 @@ * javelin-typeahead-preloaded-source * javelin-stratcom * javelin-json + * phabricator-prefab * @provides herald-rule-editor * @javelin */ @@ -350,20 +351,11 @@ JX.install('HeraldRuleEditor', { return [action_cell, target_cell]; }, _renderSelect : function(map, selected, sigil) { - var select = JX.$N( - 'select', - { - style : {width: '250px', margin: '0 .5em 0 0'}, - sigil : sigil - }); - for (var k in map) { - select.options[select.options.length] = new Option(map[k], k); - if (k == selected) { - select.value = k; - } - } - select.value = select.value || JX.keys(map)[0]; - return select; + var attrs = { + style : {width: '250px', margin: '0 .5em 0 0'}, + sigil : sigil + }; + return JX.Prefab.renderSelect(map, selected, attrs); } } }); diff --git a/webroot/rsrc/js/application/projects/projects-resource-editor.js b/webroot/rsrc/js/application/projects/projects-resource-editor.js new file mode 100644 index 0000000000..9a7e7d7611 --- /dev/null +++ b/webroot/rsrc/js/application/projects/projects-resource-editor.js @@ -0,0 +1,122 @@ +/** + * @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 status = JX.Prefab.renderSelect( + {'' : 'Current', 'former' : 'Former'}, + data.status || ''); + + 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, + status : status.value, + 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([null, status]); + 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]; + }); +});