1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-29 18:22:41 +01:00

Differentiate between "Move Tasks to Column..." and "Move Tasks to Project..." in the workboard UI

Summary:
Depends on D20635. Ref T4900. Fixes T13316.

Currently, "Move Tasks to Column..." first prompts you to select a project, then prompts you for a column. The first step is prefilled with the current project, so the common case (moving to another column on the same board) requires you to confirm that you aren't doing an off-project move by clicking "Continue", then you can select a column.

This isn't a huge inconvenience and the workflow isn't terribly common, but it's surprising enough that it has come up a few times as a stumbling block. Particularly, we're suggesting to users that they're about to pick a column, then we're asking them to pick a project. The prompt also says "Project: XYZ", not "Project: Keep in current project" or something like that.

Smooth this out by splitting the action into two better-cued flows:

  - "Move Tasks to Project..." is the current flow: pick a project, then pick a column.
    - The project selection no longer defaults to the current project, since we now expect you to usually use this flow to move tasks to a different project.
  - "Move Tasks to Column..." prompts you to select a column on the same board.
    - This just skips step 1 of the workflow.
    - This now defaults to the current column, which isn't a useful selection, but is more clear.

In both cases, the action cue ("Move tasks to X...") now matches what the dialog actually asks you for ("Pick an X").

Test Plan:
  - Moved tasks across projects and columns within the same project.
  - Hit all (I think?) the error cases and got sensible error and recovery behavior.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13316, T4900

Differential Revision: https://secure.phabricator.com/D20636
This commit is contained in:
epriestley 2019-07-02 08:25:30 -07:00
parent 24b466cd62
commit 58e2fa0d47
2 changed files with 221 additions and 161 deletions

View file

@ -708,6 +708,36 @@ final class PhabricatorProjectBoardViewController
$column_items[] = id(new PhabricatorActionView()) $column_items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER); ->setType(PhabricatorActionView::TYPE_DIVIDER);
$query_uri = urisprintf('viewquery/%d/', $column->getID());
$query_uri = $state->newWorkboardURI($query_uri);
$column_items[] = id(new PhabricatorActionView())
->setName(pht('View Tasks as Query'))
->setIcon('fa-search')
->setHref($query_uri);
$column_move_uri = $state->newWorkboardURI(
urisprintf(
'bulkmove/%d/column/',
$column->getID()));
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-arrows-h')
->setName(pht('Move Tasks to Column...'))
->setHref($column_move_uri)
->setWorkflow(true);
$project_move_uri = $state->newWorkboardURI(
urisprintf(
'bulkmove/%d/project/',
$column->getID()));
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-arrows')
->setName(pht('Move Tasks to Project...'))
->setHref($project_move_uri)
->setWorkflow(true);
$bulk_edit_uri = $state->newWorkboardURI( $bulk_edit_uri = $state->newWorkboardURI(
urisprintf( urisprintf(
'bulk/%d/', 'bulk/%d/',
@ -719,29 +749,14 @@ final class PhabricatorProjectBoardViewController
ManiphestBulkEditCapability::CAPABILITY); ManiphestBulkEditCapability::CAPABILITY);
$column_items[] = id(new PhabricatorActionView()) $column_items[] = id(new PhabricatorActionView())
->setIcon('fa-list-ul') ->setIcon('fa-pencil-square-o')
->setName(pht('Bulk Edit Tasks...')) ->setName(pht('Bulk Edit Tasks...'))
->setHref($bulk_edit_uri) ->setHref($bulk_edit_uri)
->setDisabled(!$can_bulk_edit); ->setDisabled(!$can_bulk_edit);
$project_move_uri = $state->newWorkboardURI(
urisprintf(
'bulkmove/%d/project/',
$column->getID()));
$column_items[] = id(new PhabricatorActionView()) $column_items[] = id(new PhabricatorActionView())
->setIcon('fa-arrow-right') ->setType(PhabricatorActionView::TYPE_DIVIDER);
->setName(pht('Move Tasks to Column...'))
->setHref($project_move_uri)
->setWorkflow(true);
$query_uri = urisprintf('viewquery/%d/', $column->getID());
$query_uri = $state->newWorkboardURI($query_uri);
$column_items[] = id(new PhabricatorActionView())
->setName(pht('View as Query'))
->setIcon('fa-search')
->setHref($query_uri);
$edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/'; $edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/';
$column_items[] = id(new PhabricatorActionView()) $column_items[] = id(new PhabricatorActionView())

View file

@ -11,25 +11,30 @@ final class PhabricatorProjectColumnBulkMoveController
return $response; return $response;
} }
$project = $this->getProject(); // See T13316. If we're operating in "column" mode, we're going to skip
// the prompt for a project and just have the user select a target column.
// In "project" mode, we prompt them for a project first.
$is_column_mode = ($request->getURIData('mode') === 'column');
$src_project = $this->getProject();
$state = $this->getViewState(); $state = $this->getViewState();
$board_uri = $state->newWorkboardURI(); $board_uri = $state->newWorkboardURI();
$layout_engine = $state->getLayoutEngine(); $layout_engine = $state->getLayoutEngine();
$board_phid = $project->getPHID(); $board_phid = $src_project->getPHID();
$columns = $layout_engine->getColumns($board_phid); $columns = $layout_engine->getColumns($board_phid);
$columns = mpull($columns, null, 'getID'); $columns = mpull($columns, null, 'getID');
$column_id = $request->getURIData('columnID'); $column_id = $request->getURIData('columnID');
$move_column = idx($columns, $column_id); $src_column = idx($columns, $column_id);
if (!$move_column) { if (!$src_column) {
return new Aphront404Response(); return new Aphront404Response();
} }
$move_task_phids = $layout_engine->getColumnObjectPHIDs( $move_task_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid, $board_phid,
$move_column->getPHID()); $src_column->getPHID());
$tasks = $state->getObjects(); $tasks = $state->getObjects();
@ -50,81 +55,100 @@ final class PhabricatorProjectColumnBulkMoveController
->addCancelButton($board_uri); ->addCancelButton($board_uri);
} }
$move_project_phid = $project->getPHID(); $dst_project_phid = null;
$move_column_phid = null; $dst_project = null;
$move_project = null; $has_project = false;
$move_column = null; if ($is_column_mode) {
$columns = null; $has_project = true;
$errors = array(); $dst_project_phid = $src_project->getPHID();
} else {
if ($request->isFormOrHiSecPost()) { if ($request->isFormOrHiSecPost()) {
$move_project_phid = head($request->getArr('moveProjectPHID')); $has_project = $request->getStr('hasProject');
if (!$move_project_phid) { if ($has_project) {
$move_project_phid = $request->getStr('moveProjectPHID'); // We may read this from a tokenizer input as an array, or from a
// hidden input as a string.
$dst_project_phid = head($request->getArr('dstProjectPHID'));
if (!$dst_project_phid) {
$dst_project_phid = $request->getStr('dstProjectPHID');
}
}
}
} }
if (!$move_project_phid) { $errors = array();
if ($request->getBool('hasProject')) { $hidden = array();
if ($has_project) {
if (!$dst_project_phid) {
$errors[] = pht('Choose a project to move tasks to.'); $errors[] = pht('Choose a project to move tasks to.');
}
} else { } else {
$target_project = id(new PhabricatorProjectQuery()) $dst_project = id(new PhabricatorProjectQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs(array($move_project_phid)) ->withPHIDs(array($dst_project_phid))
->executeOne(); ->executeOne();
if (!$target_project) { if (!$dst_project) {
$errors[] = pht('You must choose a valid project.'); $errors[] = pht('Choose a valid project to move tasks to.');
} else if (!$project->getHasWorkboard()) { }
$errors[] = pht(
'You must choose a project with a workboard.'); if (!$dst_project->getHasWorkboard()) {
} else { $errors[] = pht('You must choose a project with a workboard.');
$move_project = $target_project; $dst_project = null;
}
} }
} }
if ($move_project) { if ($dst_project) {
$move_engine = id(new PhabricatorBoardLayoutEngine()) $same_project = ($src_project->getID() === $dst_project->getID());
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer) ->setViewer($viewer)
->setBoardPHIDs(array($move_project->getPHID())) ->setBoardPHIDs(array($dst_project->getPHID()))
->setFetchAllBoards(true) ->setFetchAllBoards(true)
->executeLayout(); ->executeLayout();
$columns = $move_engine->getColumns($move_project->getPHID()); $dst_columns = $layout_engine->getColumns($dst_project->getPHID());
$columns = mpull($columns, null, 'getPHID'); $dst_columns = mpull($columns, null, 'getPHID');
foreach ($columns as $key => $column) { $has_column = false;
if ($column->isHidden()) { $dst_column = null;
unset($columns[$key]);
// If we're performing a move on the same board, default the
// control value to the current column.
if ($same_project) {
$dst_column_phid = $src_column->getPHID();
} else {
$dst_column_phid = null;
}
if ($request->isFormOrHiSecPost()) {
$has_column = $request->getStr('hasColumn');
if ($has_column) {
$dst_column_phid = $request->getStr('dstColumnPHID');
} }
} }
$move_column_phid = $request->getStr('moveColumnPHID'); if ($has_column) {
if (!$move_column_phid) { $dst_column = idx($dst_columns, $dst_column_phid);
if ($request->getBool('hasColumn')) { if (!$dst_column) {
$errors[] = pht('Choose a column to move tasks to.'); $errors[] = pht('Choose a column to move tasks to.');
}
} else { } else {
if (empty($columns[$move_column_phid])) { if ($dst_column->isHidden()) {
$errors[] = pht( $errors[] = pht('You can not move tasks to a hidden column.');
'Choose a valid column on the target workboard to move '. $dst_column = null;
'tasks to.'); } else if ($dst_column->getPHID() === $src_column->getPHID()) {
} else if ($columns[$move_column_phid]->getID() == $column_id) { $errors[] = pht('You can not move tasks from a column to itself.');
$errors[] = pht( $dst_column = null;
'You can not move tasks from a column to itself.');
} else {
$move_column = $columns[$move_column_phid];
}
} }
} }
} }
if ($move_column && $move_project) { if ($dst_column) {
foreach ($move_tasks as $move_task) { foreach ($move_tasks as $move_task) {
$xactions = array(); $xactions = array();
// If we're switching projects, get out of the old project first // If we're switching projects, get out of the old project first
// and move to the new project. // and move to the new project.
if ($move_project->getID() != $project->getID()) { if (!$same_project) {
$xactions[] = id(new ManiphestTransaction()) $xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue( ->setMetadataValue(
@ -133,10 +157,10 @@ final class PhabricatorProjectColumnBulkMoveController
->setNewValue( ->setNewValue(
array( array(
'-' => array( '-' => array(
$project->getPHID() => $project->getPHID(), $src_project->getPHID() => $src_project->getPHID(),
), ),
'+' => array( '+' => array(
$move_project->getPHID() => $move_project->getPHID(), $dst_project->getPHID() => $dst_project->getPHID(),
), ),
)); ));
} }
@ -146,7 +170,7 @@ final class PhabricatorProjectColumnBulkMoveController
->setNewValue( ->setNewValue(
array( array(
array( array(
'columnPHID' => $move_column->getPHID(), 'columnPHID' => $dst_column->getPHID(),
), ),
)); ));
@ -160,60 +184,81 @@ final class PhabricatorProjectColumnBulkMoveController
$editor->applyTransactions($move_task, $xactions); $editor->applyTransactions($move_task, $xactions);
} }
return id(new AphrontRedirectResponse()) // If we did a move on the same workboard, redirect and preserve the
->setURI($board_uri); // state parameters. If we moved to a different workboard, go there
} // with clean default state.
if ($same_project) {
if ($move_project) { $done_uri = $board_uri;
$column_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setName('moveColumnPHID')
->setLabel(pht('Move to Column'))
->setValue($move_column_phid)
->setOptions(mpull($columns, 'getDisplayName', 'getPHID')));
return $this->newWorkboardDialog()
->setTitle(pht('Move Tasks'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->addHiddenInput('moveProjectPHID', $move_project->getPHID())
->addHiddenInput('hasColumn', true)
->addHiddenInput('hasProject', true)
->appendParagraph(
pht(
'Choose a column on the %s workboard to move tasks to:',
$viewer->renderHandle($move_project->getPHID())))
->appendForm($column_form)
->addSubmitButton(pht('Move Tasks'))
->addCancelButton($board_uri);
}
if ($move_project_phid) {
$move_project_phid_value = array($move_project_phid);
} else { } else {
$move_project_phid_value = array(); $done_uri = $dst_project->getWorkboardURI();
} }
$project_form = id(new AphrontFormView()) return id(new AphrontRedirectResponse())->setURI($done_uri);
}
$title = pht('Move Tasks to Column');
$form = id(new AphrontFormView())
->setViewer($viewer);
// If we're moving between projects, add a reminder about which project
// you selected in the previous step.
if (!$is_column_mode) {
$form->appendControl(
id(new AphrontFormStaticControl())
->setLabel(pht('Project'))
->setValue($dst_project->getDisplayName()));
}
$form->appendControl(
id(new AphrontFormSelectControl())
->setName('dstColumnPHID')
->setLabel(pht('Move to Column'))
->setValue($dst_column_phid)
->setOptions(mpull($dst_columns, 'getDisplayName', 'getPHID')));
$submit = pht('Move Tasks');
$hidden['dstProjectPHID'] = $dst_project->getPHID();
$hidden['hasColumn'] = true;
$hidden['hasProject'] = true;
} else {
$title = pht('Move Tasks to Project');
if ($dst_project_phid) {
$dst_project_phid_value = array($dst_project_phid);
} else {
$dst_project_phid_value = array();
}
$form = id(new AphrontFormView())
->setViewer($viewer) ->setViewer($viewer)
->appendControl( ->appendControl(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setName('moveProjectPHID') ->setName('dstProjectPHID')
->setLimit(1) ->setLimit(1)
->setLabel(pht('Move to Project')) ->setLabel(pht('Move to Project'))
->setValue($move_project_phid_value) ->setValue($dst_project_phid_value)
->setDatasource(new PhabricatorProjectDatasource())); ->setDatasource(new PhabricatorProjectDatasource()));
return $this->newWorkboardDialog() $submit = pht('Continue');
->setTitle(pht('Move Tasks'))
$hidden['hasProject'] = true;
}
$dialog = $this->newWorkboardDialog()
->setWidth(AphrontDialogView::WIDTH_FORM) ->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($title)
->setErrors($errors) ->setErrors($errors)
->addHiddenInput('hasProject', true) ->appendForm($form)
->appendForm($project_form) ->addSubmitButton($submit)
->addSubmitButton(pht('Continue'))
->addCancelButton($board_uri); ->addCancelButton($board_uri);
foreach ($hidden as $key => $value) {
$dialog->addHiddenInput($key, $value);
}
return $dialog;
} }
} }