mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 00:32:42 +01:00
Define bulk edits in terms of EditEngine, not hard-coded ad-hoc definitions
Summary: Depends on D18862. See PHI173. Ref T13025. Fixes T10005. This redefines bulk edits in terms of EditEngine fields, rather than hard-coding the whole thing. Only text fields -- and, specifically, only the "Title" field -- are supported after this change. Followup changes will add more bulk edit parameter types and broader field support. However, the title field now works without any Maniphest-specific code, outside of the small amount of binding code in the `ManiphestBulkEditor` subclass. Test Plan: Used the bulk edit workflow to change the titles of tasks. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13025, T10005 Differential Revision: https://secure.phabricator.com/D18863
This commit is contained in:
parent
6ef45d8245
commit
09e71a4082
17 changed files with 455 additions and 288 deletions
|
@ -81,7 +81,6 @@ return array(
|
|||
'rsrc/css/application/harbormaster/harbormaster.css' => 'f491c9f4',
|
||||
'rsrc/css/application/herald/herald-test.css' => 'a52e323e',
|
||||
'rsrc/css/application/herald/herald.css' => 'cd8d0134',
|
||||
'rsrc/css/application/maniphest/batch-editor.css' => 'b0f0b6d5',
|
||||
'rsrc/css/application/maniphest/report.css' => '9b9580b7',
|
||||
'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b',
|
||||
'rsrc/css/application/maniphest/task-summary.css' => '11cc5344',
|
||||
|
@ -143,6 +142,7 @@ return array(
|
|||
'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3',
|
||||
'rsrc/css/phui/phui-big-info-view.css' => 'acc3492c',
|
||||
'rsrc/css/phui/phui-box.css' => '4bd6cdb9',
|
||||
'rsrc/css/phui/phui-bulk-editor.css' => '1fe728a8',
|
||||
'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
|
||||
'rsrc/css/phui/phui-cms.css' => '504b4b23',
|
||||
'rsrc/css/phui/phui-comment-form.css' => 'ac68149f',
|
||||
|
@ -419,7 +419,6 @@ return array(
|
|||
'rsrc/js/application/herald/HeraldRuleEditor.js' => '2dff5579',
|
||||
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
|
||||
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
|
||||
'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7',
|
||||
'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ad54037e',
|
||||
'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876',
|
||||
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
|
||||
|
@ -477,6 +476,7 @@ return array(
|
|||
'rsrc/js/core/behavior-audio-source.js' => '59b251eb',
|
||||
'rsrc/js/core/behavior-autofocus.js' => '7319e029',
|
||||
'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c',
|
||||
'rsrc/js/core/behavior-bulk-editor.js' => '5e178556',
|
||||
'rsrc/js/core/behavior-choose-control.js' => '327a00d1',
|
||||
'rsrc/js/core/behavior-copy.js' => 'b0b8f86d',
|
||||
'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96',
|
||||
|
@ -532,7 +532,7 @@ return array(
|
|||
'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac',
|
||||
'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03',
|
||||
'rsrc/js/phuix/PHUIXExample.js' => '68af71ca',
|
||||
'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671',
|
||||
'rsrc/js/phuix/PHUIXFormControl.js' => '68bb05aa',
|
||||
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
|
||||
),
|
||||
'symbols' => array(
|
||||
|
@ -595,6 +595,7 @@ return array(
|
|||
'javelin-behavior-audio-source' => '59b251eb',
|
||||
'javelin-behavior-audit-preview' => 'd835b03a',
|
||||
'javelin-behavior-badge-view' => '8ff5e24c',
|
||||
'javelin-behavior-bulk-editor' => '5e178556',
|
||||
'javelin-behavior-bulk-job-reload' => 'edf8a145',
|
||||
'javelin-behavior-calendar-month-view' => 'fe33e256',
|
||||
'javelin-behavior-choose-control' => '327a00d1',
|
||||
|
@ -642,7 +643,6 @@ return array(
|
|||
'javelin-behavior-lightbox-attachments' => '560f41da',
|
||||
'javelin-behavior-line-chart' => 'e4232876',
|
||||
'javelin-behavior-load-blame' => '42126667',
|
||||
'javelin-behavior-maniphest-batch-editor' => '782ab6e7',
|
||||
'javelin-behavior-maniphest-batch-selector' => 'ad54037e',
|
||||
'javelin-behavior-maniphest-list-editor' => 'a9f88de2',
|
||||
'javelin-behavior-maniphest-subpriority-editor' => '71237763',
|
||||
|
@ -756,7 +756,6 @@ return array(
|
|||
'javelin-workboard-column' => '758b4758',
|
||||
'javelin-workboard-controller' => '26167537',
|
||||
'javelin-workflow' => '1e911d0f',
|
||||
'maniphest-batch-editor' => 'b0f0b6d5',
|
||||
'maniphest-report-css' => '9b9580b7',
|
||||
'maniphest-task-edit-css' => 'fda62a9b',
|
||||
'maniphest-task-summary-css' => '11cc5344',
|
||||
|
@ -823,6 +822,7 @@ return array(
|
|||
'phui-basic-nav-view-css' => '98c11ab3',
|
||||
'phui-big-info-view-css' => 'acc3492c',
|
||||
'phui-box-css' => '4bd6cdb9',
|
||||
'phui-bulk-editor-css' => '1fe728a8',
|
||||
'phui-button-bar-css' => 'f1ff5494',
|
||||
'phui-button-css' => '1863cc6e',
|
||||
'phui-button-simple-css' => '8e1baf68',
|
||||
|
@ -884,7 +884,7 @@ return array(
|
|||
'phuix-autocomplete' => 'e0731603',
|
||||
'phuix-button-view' => '8a91e1ac',
|
||||
'phuix-dropdown-menu' => '04b2ae03',
|
||||
'phuix-form-control-view' => '83e03671',
|
||||
'phuix-form-control-view' => '68bb05aa',
|
||||
'phuix-icon-view' => 'bff6884b',
|
||||
'policy-css' => '957ea14c',
|
||||
'policy-edit-css' => '815c66f7',
|
||||
|
@ -1387,6 +1387,15 @@ return array(
|
|||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
),
|
||||
'5e178556' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'phabricator-prefab',
|
||||
'multirow-row-manager',
|
||||
'javelin-json',
|
||||
'phuix-form-control-view',
|
||||
),
|
||||
'5e2634b9' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-aphlict',
|
||||
|
@ -1436,6 +1445,10 @@ return array(
|
|||
'javelin-dom',
|
||||
'phuix-button-view',
|
||||
),
|
||||
'68bb05aa' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
),
|
||||
'69adf288' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
|
@ -1524,14 +1537,6 @@ return array(
|
|||
'javelin-request',
|
||||
'javelin-util',
|
||||
),
|
||||
'782ab6e7' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'phabricator-prefab',
|
||||
'multirow-row-manager',
|
||||
'javelin-json',
|
||||
),
|
||||
'7927a7d3' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-quicksand',
|
||||
|
@ -1570,10 +1575,6 @@ return array(
|
|||
'javelin-behavior',
|
||||
'javelin-scrollbar',
|
||||
),
|
||||
'83e03671' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
),
|
||||
'8499b6ab' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
|
|
@ -222,6 +222,8 @@ phutil_register_library_map(array(
|
|||
'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
|
||||
'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
|
||||
'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php',
|
||||
'BulkParameterType' => 'applications/transactions/bulk/type/BulkParameterType.php',
|
||||
'BulkStringParameterType' => 'applications/transactions/bulk/type/BulkStringParameterType.php',
|
||||
'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php',
|
||||
'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php',
|
||||
'CelerityAPI' => 'applications/celerity/CelerityAPI.php',
|
||||
|
@ -5242,6 +5244,8 @@ phutil_register_library_map(array(
|
|||
'AuditConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
|
||||
'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability',
|
||||
'BulkParameterType' => 'Phobject',
|
||||
'BulkStringParameterType' => 'BulkParameterType',
|
||||
'CalendarTimeUtil' => 'Phobject',
|
||||
'CalendarTimeUtilTestCase' => 'PhabricatorTestCase',
|
||||
'CelerityAPI' => 'Phobject',
|
||||
|
|
|
@ -18,6 +18,10 @@ final class ManiphestTaskBulkEngine
|
|||
return new ManiphestTaskSearchEngine();
|
||||
}
|
||||
|
||||
public function newEditEngine() {
|
||||
return new ManiphestEditEngine();
|
||||
}
|
||||
|
||||
public function getDoneURI() {
|
||||
$board_uri = $this->getBoardURI();
|
||||
if ($board_uri) {
|
||||
|
|
|
@ -178,6 +178,7 @@ EODOCS
|
|||
id(new PhabricatorTextEditField())
|
||||
->setKey('title')
|
||||
->setLabel(pht('Title'))
|
||||
->setBulkEditLabel(pht('Set title to'))
|
||||
->setDescription(pht('Name of the task.'))
|
||||
->setConduitDescription(pht('Rename the task.'))
|
||||
->setConduitTypeDescription(pht('New task name.'))
|
||||
|
|
|
@ -10,7 +10,10 @@ abstract class PhabricatorBulkEngine extends Phobject {
|
|||
private $editableList;
|
||||
private $targetList;
|
||||
|
||||
private $rootFormID;
|
||||
|
||||
abstract public function newSearchEngine();
|
||||
abstract public function newEditEngine();
|
||||
|
||||
public function getCancelURI() {
|
||||
$saved_query = $this->savedQuery;
|
||||
|
@ -118,7 +121,7 @@ abstract class PhabricatorBulkEngine extends Phobject {
|
|||
array(
|
||||
'action' => $this->getBulkURI(),
|
||||
'method' => 'POST',
|
||||
'id' => 'maniphest-batch-edit-form',
|
||||
'id' => $this->getRootFormID(),
|
||||
),
|
||||
array(
|
||||
$this->newContextInputs(),
|
||||
|
@ -290,95 +293,60 @@ abstract class PhabricatorBulkEngine extends Phobject {
|
|||
|
||||
private function newBulkActionForm() {
|
||||
$viewer = $this->getViewer();
|
||||
$input_id = celerity_generate_unique_node_id();
|
||||
|
||||
$edit_engine = id($this->newEditEngine())
|
||||
->setViewer($viewer);
|
||||
|
||||
$edit_map = $edit_engine->newBulkEditMap();
|
||||
|
||||
require_celerity_resource('phui-bulk-editor-css');
|
||||
|
||||
Javelin::initBehavior(
|
||||
'bulk-editor',
|
||||
array(
|
||||
'rootNodeID' => $this->getRootFormID(),
|
||||
'inputNodeID' => $input_id,
|
||||
'edits' => $edit_map,
|
||||
));
|
||||
|
||||
$cancel_uri = $this->getCancelURI();
|
||||
|
||||
$template = new AphrontTokenizerTemplateView();
|
||||
$template = $template->render();
|
||||
|
||||
$projects_source = new PhabricatorProjectDatasource();
|
||||
$mailable_source = new PhabricatorMetaMTAMailableDatasource();
|
||||
$mailable_source->setViewer($viewer);
|
||||
$owner_source = new ManiphestAssigneeDatasource();
|
||||
$owner_source->setViewer($viewer);
|
||||
$spaces_source = id(new PhabricatorSpacesNamespaceDatasource())
|
||||
->setViewer($viewer);
|
||||
|
||||
require_celerity_resource('maniphest-batch-editor');
|
||||
|
||||
Javelin::initBehavior(
|
||||
'maniphest-batch-editor',
|
||||
array(
|
||||
'root' => 'maniphest-batch-edit-form',
|
||||
'tokenizerTemplate' => $template,
|
||||
'sources' => array(
|
||||
'project' => array(
|
||||
'src' => $projects_source->getDatasourceURI(),
|
||||
'placeholder' => $projects_source->getPlaceholderText(),
|
||||
'browseURI' => $projects_source->getBrowseURI(),
|
||||
),
|
||||
'owner' => array(
|
||||
'src' => $owner_source->getDatasourceURI(),
|
||||
'placeholder' => $owner_source->getPlaceholderText(),
|
||||
'browseURI' => $owner_source->getBrowseURI(),
|
||||
'limit' => 1,
|
||||
),
|
||||
'cc' => array(
|
||||
'src' => $mailable_source->getDatasourceURI(),
|
||||
'placeholder' => $mailable_source->getPlaceholderText(),
|
||||
'browseURI' => $mailable_source->getBrowseURI(),
|
||||
),
|
||||
'spaces' => array(
|
||||
'src' => $spaces_source->getDatasourceURI(),
|
||||
'placeholder' => $spaces_source->getPlaceholderText(),
|
||||
'browseURI' => $spaces_source->getBrowseURI(),
|
||||
'limit' => 1,
|
||||
),
|
||||
),
|
||||
'input' => 'batch-form-actions',
|
||||
'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
|
||||
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
|
||||
));
|
||||
|
||||
$form = id(new PHUIFormLayoutView())
|
||||
->setUser($viewer);
|
||||
|
||||
$form->appendChild(
|
||||
phutil_tag(
|
||||
'input',
|
||||
array(
|
||||
'type' => 'hidden',
|
||||
'name' => 'actions',
|
||||
'id' => 'batch-form-actions',
|
||||
)));
|
||||
|
||||
$form->appendChild(
|
||||
id(new PHUIFormInsetView())
|
||||
->setTitle(pht('Bulk Edit Actions'))
|
||||
->setRightButton(
|
||||
javelin_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '#',
|
||||
'class' => 'button button-green',
|
||||
'sigil' => 'add-action',
|
||||
'mustcapture' => true,
|
||||
),
|
||||
pht('Add Another Action')))
|
||||
->setContent(
|
||||
javelin_tag(
|
||||
'table',
|
||||
array(
|
||||
'sigil' => 'maniphest-batch-actions',
|
||||
'class' => 'maniphest-batch-actions-table',
|
||||
),
|
||||
'')))
|
||||
return id(new PHUIFormLayoutView())
|
||||
->setViewer($viewer)
|
||||
->appendChild(
|
||||
phutil_tag(
|
||||
'input',
|
||||
array(
|
||||
'type' => 'hidden',
|
||||
'name' => 'xactions',
|
||||
'id' => $input_id,
|
||||
)))
|
||||
->appendChild(
|
||||
id(new PHUIFormInsetView())
|
||||
->setTitle(pht('Bulk Edit Actions'))
|
||||
->setRightButton(
|
||||
javelin_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '#',
|
||||
'class' => 'button button-green',
|
||||
'sigil' => 'add-action',
|
||||
'mustcapture' => true,
|
||||
),
|
||||
pht('Add Another Action')))
|
||||
->setContent(
|
||||
javelin_tag(
|
||||
'table',
|
||||
array(
|
||||
'sigil' => 'bulk-actions',
|
||||
'class' => 'bulk-edit-table',
|
||||
),
|
||||
'')))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue(pht('Apply Bulk Edit'))
|
||||
->addCancelButton($cancel_uri));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
private function buildEditResponse() {
|
||||
|
@ -405,31 +373,33 @@ abstract class PhabricatorBulkEngine extends Phobject {
|
|||
'You have not selected any objects to edit.'));
|
||||
}
|
||||
|
||||
$raw_actions = $request->getStr('actions');
|
||||
if ($raw_actions) {
|
||||
$actions = phutil_json_decode($raw_actions);
|
||||
$raw_xactions = $request->getStr('xactions');
|
||||
if ($raw_xactions) {
|
||||
$raw_xactions = phutil_json_decode($raw_xactions);
|
||||
} else {
|
||||
$actions = array();
|
||||
$raw_xactions = array();
|
||||
}
|
||||
|
||||
if (!$actions) {
|
||||
if (!$raw_xactions) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'You have not chosen any edits to apply.'));
|
||||
}
|
||||
|
||||
$edit_engine = id($this->newEditEngine())
|
||||
->setViewer($viewer);
|
||||
|
||||
$xactions = $edit_engine->newRawBulkTransactions($raw_xactions);
|
||||
|
||||
$cancel_uri = $this->getCancelURI();
|
||||
$done_uri = $this->getDoneURI();
|
||||
|
||||
$job = PhabricatorWorkerBulkJob::initializeNewJob(
|
||||
$viewer,
|
||||
// TODO: This is a Maniphest-specific job type for now, but will become
|
||||
// a generic one so it gets to live here for now instead of in the task
|
||||
// specific BulkEngine subclass.
|
||||
new ManiphestTaskEditBulkJobType(),
|
||||
new PhabricatorEditEngineBulkJobType(),
|
||||
array(
|
||||
'taskPHIDs' => mpull($objects, 'getPHID'),
|
||||
'actions' => $actions,
|
||||
'objectPHIDs' => mpull($objects, 'getPHID'),
|
||||
'xactions' => $xactions,
|
||||
'cancelURI' => $cancel_uri,
|
||||
'doneURI' => $done_uri,
|
||||
));
|
||||
|
@ -451,4 +421,12 @@ abstract class PhabricatorBulkEngine extends Phobject {
|
|||
->setURI($job->getMonitorURI());
|
||||
}
|
||||
|
||||
private function getRootFormID() {
|
||||
if (!$this->rootFormID) {
|
||||
$this->rootFormID = celerity_generate_unique_node_id();
|
||||
}
|
||||
|
||||
return $this->rootFormID;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
abstract class BulkParameterType extends Phobject {
|
||||
|
||||
private $viewer;
|
||||
|
||||
final public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getViewer() {
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
abstract public function getPHUIXControlType();
|
||||
|
||||
public function getPHUIXControlSpecification() {
|
||||
return array(
|
||||
'value' => null,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class BulkStringParameterType
|
||||
extends BulkParameterType {
|
||||
|
||||
public function getPHUIXControlType() {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
}
|
|
@ -2421,6 +2421,71 @@ abstract class PhabricatorEditEngine
|
|||
}
|
||||
|
||||
|
||||
/* -( Bulk Edits )--------------------------------------------------------- */
|
||||
|
||||
|
||||
final public function newBulkEditMap() {
|
||||
$config = $this->loadDefaultConfiguration();
|
||||
if (!$config) {
|
||||
throw new Exception(
|
||||
pht('No default edit engine configuration for bulk edit.'));
|
||||
}
|
||||
|
||||
$object = $this->newEditableObject();
|
||||
$fields = $this->buildEditFields($object);
|
||||
|
||||
$edit_types = $this->getBulkEditTypesFromFields($fields);
|
||||
|
||||
$map = array();
|
||||
foreach ($edit_types as $key => $type) {
|
||||
$bulk_type = $type->getBulkParameterType();
|
||||
if ($bulk_type === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bulk_label = $type->getBulkEditLabel();
|
||||
if ($bulk_label === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[] = array(
|
||||
'label' => $bulk_label,
|
||||
'xaction' => $type->getTransactionType(),
|
||||
'control' => array(
|
||||
'type' => $bulk_type->getPHUIXControlType(),
|
||||
'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
|
||||
final public function newRawBulkTransactions(array $xactions) {
|
||||
return $xactions;
|
||||
}
|
||||
|
||||
private function getBulkEditTypesFromFields(array $fields) {
|
||||
$types = array();
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$field_types = $field->getBulkEditTypes();
|
||||
|
||||
if ($field_types === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($field_types as $field_type) {
|
||||
$field_type->setField($field);
|
||||
$types[$field_type->getEditType()] = $field_type;
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ abstract class PhabricatorEditField extends Phobject {
|
|||
private $previewPanel;
|
||||
private $controlID;
|
||||
private $controlInstructions;
|
||||
private $bulkEditLabel;
|
||||
|
||||
private $description;
|
||||
private $conduitDescription;
|
||||
|
@ -45,6 +46,7 @@ abstract class PhabricatorEditField extends Phobject {
|
|||
private $isConduitOnly = false;
|
||||
|
||||
private $conduitEditTypes;
|
||||
private $bulkEditTypes;
|
||||
|
||||
public function setKey($key) {
|
||||
$this->key = $key;
|
||||
|
@ -64,6 +66,15 @@ abstract class PhabricatorEditField extends Phobject {
|
|||
return $this->label;
|
||||
}
|
||||
|
||||
public function setBulkEditLabel($bulk_edit_label) {
|
||||
$this->bulkEditLabel = $bulk_edit_label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBulkEditLabel() {
|
||||
return $this->bulkEditLabel;
|
||||
}
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
|
@ -625,6 +636,22 @@ abstract class PhabricatorEditField extends Phobject {
|
|||
return new AphrontStringHTTPParameterType();
|
||||
}
|
||||
|
||||
protected function getBulkParameterType() {
|
||||
$type = $this->newBulkParameterType();
|
||||
|
||||
if (!$type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type->setViewer($this->getViewer());
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
protected function newBulkParameterType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getConduitParameterType() {
|
||||
$type = $this->newConduitParameterType();
|
||||
|
||||
|
@ -657,8 +684,15 @@ abstract class PhabricatorEditField extends Phobject {
|
|||
return null;
|
||||
}
|
||||
|
||||
return id(new PhabricatorSimpleEditType())
|
||||
$edit_type = id(new PhabricatorSimpleEditType())
|
||||
->setConduitParameterType($parameter_type);
|
||||
|
||||
$bulk_type = $this->getBulkParameterType();
|
||||
if ($bulk_type) {
|
||||
$edit_type->setBulkParameterType($bulk_type);
|
||||
}
|
||||
|
||||
return $edit_type;
|
||||
}
|
||||
|
||||
protected function getEditType() {
|
||||
|
@ -718,6 +752,31 @@ abstract class PhabricatorEditField extends Phobject {
|
|||
return array($edit_type);
|
||||
}
|
||||
|
||||
final public function getBulkEditTypes() {
|
||||
if ($this->bulkEditTypes === null) {
|
||||
$edit_types = $this->newBulkEditTypes();
|
||||
$edit_types = mpull($edit_types, null, 'getEditType');
|
||||
|
||||
foreach ($edit_types as $edit_type) {
|
||||
$edit_type->setEditField($this);
|
||||
}
|
||||
|
||||
$this->bulkEditTypes = $edit_types;
|
||||
}
|
||||
|
||||
return $this->bulkEditTypes;
|
||||
}
|
||||
|
||||
protected function newBulkEditTypes() {
|
||||
$edit_type = $this->getEditType();
|
||||
|
||||
if (!$edit_type) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array($edit_type);
|
||||
}
|
||||
|
||||
public function getCommentAction() {
|
||||
$label = $this->getCommentActionLabel();
|
||||
if ($label === null) {
|
||||
|
|
|
@ -104,6 +104,10 @@ abstract class PhabricatorPHIDListEditField
|
|||
return $type;
|
||||
}
|
||||
|
||||
protected function newBulkEditTypes() {
|
||||
return $this->newConduitEditTypes();
|
||||
}
|
||||
|
||||
protected function newConduitEditTypes() {
|
||||
if (!$this->getUseEdgeTransactions()) {
|
||||
return parent::newConduitEditTypes();
|
||||
|
|
|
@ -29,4 +29,8 @@ final class PhabricatorTextEditField
|
|||
return new ConduitStringParameterType();
|
||||
}
|
||||
|
||||
protected function newBulkParameterType() {
|
||||
return new BulkStringParameterType();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ abstract class PhabricatorEditType extends Phobject {
|
|||
private $conduitTypeDescription;
|
||||
private $conduitParameterType;
|
||||
|
||||
private $bulkParameterType;
|
||||
private $bulkEditLabel;
|
||||
|
||||
public function setLabel($label) {
|
||||
$this->label = $label;
|
||||
return $this;
|
||||
|
@ -23,6 +26,19 @@ abstract class PhabricatorEditType extends Phobject {
|
|||
return $this->label;
|
||||
}
|
||||
|
||||
public function setBulkEditLabel($bulk_edit_label) {
|
||||
$this->bulkEditLabel = $bulk_edit_label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBulkEditLabel() {
|
||||
if ($this->bulkEditLabel !== null) {
|
||||
return $this->bulkEditLabel;
|
||||
}
|
||||
|
||||
return $this->getField()->getBulkEditLabel();
|
||||
}
|
||||
|
||||
public function setField(PhabricatorEditField $field) {
|
||||
$this->field = $field;
|
||||
return $this;
|
||||
|
@ -85,6 +101,30 @@ abstract class PhabricatorEditType extends Phobject {
|
|||
return $this->editField;
|
||||
}
|
||||
|
||||
|
||||
/* -( Bulk )--------------------------------------------------------------- */
|
||||
|
||||
|
||||
protected function newBulkParameterType() {
|
||||
if ($this->bulkParameterType) {
|
||||
return clone $this->bulkParameterType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public function setBulkParameterType(BulkParameterType $type) {
|
||||
$this->bulkParameterType = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function getBulkParameterType() {
|
||||
return $this->newBulkParameterType();
|
||||
}
|
||||
|
||||
|
||||
/* -( Conduit )------------------------------------------------------------ */
|
||||
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
/**
|
||||
* @provides maniphest-batch-editor
|
||||
*/
|
||||
.maniphest-batch-actions-table {
|
||||
width: 100%;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.maniphest-batch-actions-table td {
|
||||
padding: 4px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.batch-editor-input {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
22
webroot/rsrc/css/phui/phui-bulk-editor.css
Normal file
22
webroot/rsrc/css/phui/phui-bulk-editor.css
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* @provides phui-bulk-editor-css
|
||||
*/
|
||||
|
||||
.bulk-edit-table {
|
||||
width: 100%;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.bulk-edit-table td {
|
||||
padding: 4px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bulk-edit-input {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bulk-edit-input input {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
/**
|
||||
* @provides javelin-behavior-maniphest-batch-editor
|
||||
* @requires javelin-behavior
|
||||
* javelin-dom
|
||||
* javelin-util
|
||||
* phabricator-prefab
|
||||
* multirow-row-manager
|
||||
* javelin-json
|
||||
*/
|
||||
|
||||
JX.behavior('maniphest-batch-editor', function(config) {
|
||||
var root = JX.$(config.root);
|
||||
var editor_table = JX.DOM.find(root, 'table', 'maniphest-batch-actions');
|
||||
var manager = new JX.MultirowRowManager(editor_table);
|
||||
var action_rows = [];
|
||||
|
||||
function renderRow() {
|
||||
var action_select = JX.Prefab.renderSelect(
|
||||
{
|
||||
'add_project': 'Add Projects',
|
||||
'remove_project' : 'Remove Projects',
|
||||
'priority': 'Change Priority',
|
||||
'status': 'Change Status',
|
||||
'add_comment': 'Comment',
|
||||
'assign': 'Assign',
|
||||
'add_ccs' : 'Add CCs',
|
||||
'remove_ccs' : 'Remove CCs',
|
||||
'space': 'Shift to Space'
|
||||
});
|
||||
|
||||
var proj_tokenizer = build_tokenizer(config.sources.project);
|
||||
var owner_tokenizer = build_tokenizer(config.sources.owner);
|
||||
var cc_tokenizer = build_tokenizer(config.sources.cc);
|
||||
var space_tokenizer = build_tokenizer(config.sources.spaces);
|
||||
|
||||
var priority_select = JX.Prefab.renderSelect(config.priorityMap);
|
||||
var status_select = JX.Prefab.renderSelect(config.statusMap);
|
||||
var comment_input = JX.$N('input', {style: {width: '100%'}});
|
||||
|
||||
var cell = JX.$N('td', {className: 'batch-editor-input'});
|
||||
var vfunc = null;
|
||||
|
||||
function update() {
|
||||
switch (action_select.value) {
|
||||
case 'add_project':
|
||||
case 'remove_project':
|
||||
JX.DOM.setContent(cell, proj_tokenizer.template);
|
||||
vfunc = function() {
|
||||
return JX.keys(proj_tokenizer.object.getTokens());
|
||||
};
|
||||
break;
|
||||
case 'add_ccs':
|
||||
case 'remove_ccs':
|
||||
JX.DOM.setContent(cell, cc_tokenizer.template);
|
||||
vfunc = function() {
|
||||
return JX.keys(cc_tokenizer.object.getTokens());
|
||||
};
|
||||
break;
|
||||
case 'assign':
|
||||
JX.DOM.setContent(cell, owner_tokenizer.template);
|
||||
vfunc = function() {
|
||||
return JX.keys(owner_tokenizer.object.getTokens());
|
||||
};
|
||||
break;
|
||||
case 'space':
|
||||
JX.DOM.setContent(cell, space_tokenizer.template);
|
||||
vfunc = function() {
|
||||
return JX.keys(space_tokenizer.object.getTokens());
|
||||
};
|
||||
break;
|
||||
case 'add_comment':
|
||||
JX.DOM.setContent(cell, comment_input);
|
||||
vfunc = function() {
|
||||
return comment_input.value;
|
||||
};
|
||||
break;
|
||||
case 'priority':
|
||||
JX.DOM.setContent(cell, priority_select);
|
||||
vfunc = function() { return priority_select.value; };
|
||||
break;
|
||||
case 'status':
|
||||
JX.DOM.setContent(cell, status_select);
|
||||
vfunc = function() { return status_select.value; };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
JX.DOM.listen(action_select, 'change', null, update);
|
||||
update();
|
||||
|
||||
return {
|
||||
nodes : [JX.$N('td', {}, action_select), cell],
|
||||
dataCallback : function() {
|
||||
return {
|
||||
action: action_select.value,
|
||||
value: vfunc()
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function onaddaction(e) {
|
||||
e.kill();
|
||||
addRow({});
|
||||
}
|
||||
|
||||
function addRow(info) {
|
||||
var data = renderRow(info);
|
||||
var row = manager.addRow(data.nodes);
|
||||
var id = manager.getRowID(row);
|
||||
|
||||
action_rows[id] = data.dataCallback;
|
||||
}
|
||||
|
||||
function onsubmit() {
|
||||
var input = JX.$(config.input);
|
||||
|
||||
var actions = [];
|
||||
for (var k in action_rows) {
|
||||
actions.push(action_rows[k]());
|
||||
}
|
||||
|
||||
input.value = JX.JSON.stringify(actions);
|
||||
}
|
||||
|
||||
addRow({});
|
||||
|
||||
JX.DOM.listen(
|
||||
root,
|
||||
'click',
|
||||
'add-action',
|
||||
onaddaction);
|
||||
|
||||
JX.DOM.listen(
|
||||
root,
|
||||
'submit',
|
||||
null,
|
||||
onsubmit);
|
||||
|
||||
manager.listen(
|
||||
'row-removed',
|
||||
function(row_id) {
|
||||
delete action_rows[row_id];
|
||||
});
|
||||
|
||||
function build_tokenizer(tconfig) {
|
||||
var built = JX.Prefab.newTokenizerFromTemplate(
|
||||
config.tokenizerTemplate,
|
||||
JX.copy({}, tconfig));
|
||||
built.tokenizer.start();
|
||||
|
||||
return {
|
||||
object: built.tokenizer,
|
||||
template: built.node
|
||||
};
|
||||
}
|
||||
|
||||
});
|
113
webroot/rsrc/js/core/behavior-bulk-editor.js
Normal file
113
webroot/rsrc/js/core/behavior-bulk-editor.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* @provides javelin-behavior-bulk-editor
|
||||
* @requires javelin-behavior
|
||||
* javelin-dom
|
||||
* javelin-util
|
||||
* phabricator-prefab
|
||||
* multirow-row-manager
|
||||
* javelin-json
|
||||
* phuix-form-control-view
|
||||
*/
|
||||
|
||||
JX.behavior('bulk-editor', function(config) {
|
||||
|
||||
var root = JX.$(config.rootNodeID);
|
||||
var editor_table = JX.DOM.find(root, 'table', 'bulk-actions');
|
||||
|
||||
var manager = new JX.MultirowRowManager(editor_table);
|
||||
var action_rows = [];
|
||||
|
||||
var option_map = {};
|
||||
var option_order = [];
|
||||
var spec_map = {};
|
||||
|
||||
for (var ii = 0; ii < config.edits.length; ii++) {
|
||||
var edit = config.edits[ii];
|
||||
|
||||
option_map[edit.xaction] = edit.label;
|
||||
option_order.push(edit.xaction);
|
||||
|
||||
spec_map[edit.xaction] = edit;
|
||||
}
|
||||
|
||||
function renderRow() {
|
||||
var action_select = JX.Prefab.renderSelect(
|
||||
option_map,
|
||||
null,
|
||||
null,
|
||||
option_order);
|
||||
|
||||
var cell = JX.$N('td', {className: 'bulk-edit-input'});
|
||||
var vfunc = null;
|
||||
|
||||
function update() {
|
||||
var spec = spec_map[action_select.value];
|
||||
var control = spec.control;
|
||||
|
||||
var phuix = new JX.PHUIXFormControl()
|
||||
.setControl(control.type, control.spec);
|
||||
|
||||
JX.DOM.setContent(cell, phuix.getRawInputNode());
|
||||
|
||||
vfunc = JX.bind(phuix, phuix.getValue);
|
||||
}
|
||||
|
||||
JX.DOM.listen(action_select, 'change', null, update);
|
||||
update();
|
||||
|
||||
return {
|
||||
nodes : [JX.$N('td', {}, action_select), cell],
|
||||
dataCallback : function() {
|
||||
return {
|
||||
type: action_select.value,
|
||||
value: vfunc()
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function onaddaction(e) {
|
||||
e.kill();
|
||||
addRow({});
|
||||
}
|
||||
|
||||
function addRow(info) {
|
||||
var data = renderRow(info);
|
||||
var row = manager.addRow(data.nodes);
|
||||
var id = manager.getRowID(row);
|
||||
|
||||
action_rows[id] = data.dataCallback;
|
||||
}
|
||||
|
||||
function onsubmit() {
|
||||
var input = JX.$(config.inputNodeID);
|
||||
|
||||
var actions = [];
|
||||
for (var k in action_rows) {
|
||||
actions.push(action_rows[k]());
|
||||
}
|
||||
|
||||
input.value = JX.JSON.stringify(actions);
|
||||
}
|
||||
|
||||
addRow({});
|
||||
|
||||
JX.DOM.listen(
|
||||
root,
|
||||
'click',
|
||||
'add-action',
|
||||
onaddaction);
|
||||
|
||||
JX.DOM.listen(
|
||||
root,
|
||||
'submit',
|
||||
null,
|
||||
onsubmit);
|
||||
|
||||
manager.listen(
|
||||
'row-removed',
|
||||
function(row_id) {
|
||||
delete action_rows[row_id];
|
||||
});
|
||||
|
||||
});
|
|
@ -14,6 +14,7 @@ JX.install('PHUIXFormControl', {
|
|||
_className: null,
|
||||
_valueSetCallback: null,
|
||||
_valueGetCallback: null,
|
||||
_rawInputNode: null,
|
||||
|
||||
setLabel: function(label) {
|
||||
JX.DOM.setContent(this._getLabelNode(), label);
|
||||
|
@ -53,6 +54,9 @@ JX.install('PHUIXFormControl', {
|
|||
case 'checkboxes':
|
||||
input = this._newCheckboxes(spec);
|
||||
break;
|
||||
case 'text':
|
||||
input = this._newText(spec);
|
||||
break;
|
||||
default:
|
||||
// TODO: Default or better error?
|
||||
JX.$E('Bad Input Type');
|
||||
|
@ -62,6 +66,7 @@ JX.install('PHUIXFormControl', {
|
|||
JX.DOM.setContent(node, input.node);
|
||||
this._valueGetCallback = input.get;
|
||||
this._valueSetCallback = input.set;
|
||||
this._rawInputNode = input.node;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
@ -75,6 +80,10 @@ JX.install('PHUIXFormControl', {
|
|||
return this._valueGetCallback();
|
||||
},
|
||||
|
||||
getRawInputNode: function() {
|
||||
return this._rawInputNode;
|
||||
},
|
||||
|
||||
getNode: function() {
|
||||
if (!this._node) {
|
||||
|
||||
|
@ -281,6 +290,10 @@ JX.install('PHUIXFormControl', {
|
|||
},
|
||||
|
||||
_newPoints: function(spec) {
|
||||
return this._newText();
|
||||
},
|
||||
|
||||
_newText: function(spec) {
|
||||
var attrs = {
|
||||
type: 'text',
|
||||
value: spec.value
|
||||
|
|
Loading…
Reference in a new issue