@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.

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

+207 -147
+32 -17
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 708 708 $column_items[] = id(new PhabricatorActionView()) 709 709 ->setType(PhabricatorActionView::TYPE_DIVIDER); 710 710 711 + $query_uri = urisprintf('viewquery/%d/', $column->getID()); 712 + $query_uri = $state->newWorkboardURI($query_uri); 713 + 714 + $column_items[] = id(new PhabricatorActionView()) 715 + ->setName(pht('View Tasks as Query')) 716 + ->setIcon('fa-search') 717 + ->setHref($query_uri); 718 + 719 + $column_move_uri = $state->newWorkboardURI( 720 + urisprintf( 721 + 'bulkmove/%d/column/', 722 + $column->getID())); 723 + 724 + $column_items[] = id(new PhabricatorActionView()) 725 + ->setIcon('fa-arrows-h') 726 + ->setName(pht('Move Tasks to Column...')) 727 + ->setHref($column_move_uri) 728 + ->setWorkflow(true); 729 + 730 + $project_move_uri = $state->newWorkboardURI( 731 + urisprintf( 732 + 'bulkmove/%d/project/', 733 + $column->getID())); 734 + 735 + $column_items[] = id(new PhabricatorActionView()) 736 + ->setIcon('fa-arrows') 737 + ->setName(pht('Move Tasks to Project...')) 738 + ->setHref($project_move_uri) 739 + ->setWorkflow(true); 740 + 711 741 $bulk_edit_uri = $state->newWorkboardURI( 712 742 urisprintf( 713 743 'bulk/%d/', ··· 719 749 ManiphestBulkEditCapability::CAPABILITY); 720 750 721 751 $column_items[] = id(new PhabricatorActionView()) 722 - ->setIcon('fa-list-ul') 752 + ->setIcon('fa-pencil-square-o') 723 753 ->setName(pht('Bulk Edit Tasks...')) 724 754 ->setHref($bulk_edit_uri) 725 755 ->setDisabled(!$can_bulk_edit); 726 - 727 - $project_move_uri = $state->newWorkboardURI( 728 - urisprintf( 729 - 'bulkmove/%d/project/', 730 - $column->getID())); 731 756 732 757 $column_items[] = id(new PhabricatorActionView()) 733 - ->setIcon('fa-arrow-right') 734 - ->setName(pht('Move Tasks to Column...')) 735 - ->setHref($project_move_uri) 736 - ->setWorkflow(true); 737 - 738 - $query_uri = urisprintf('viewquery/%d/', $column->getID()); 739 - $query_uri = $state->newWorkboardURI($query_uri); 758 + ->setType(PhabricatorActionView::TYPE_DIVIDER); 740 759 741 - $column_items[] = id(new PhabricatorActionView()) 742 - ->setName(pht('View as Query')) 743 - ->setIcon('fa-search') 744 - ->setHref($query_uri); 745 760 746 761 $edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/'; 747 762 $column_items[] = id(new PhabricatorActionView())
+175 -130
src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php
··· 11 11 return $response; 12 12 } 13 13 14 - $project = $this->getProject(); 14 + // See T13316. If we're operating in "column" mode, we're going to skip 15 + // the prompt for a project and just have the user select a target column. 16 + // In "project" mode, we prompt them for a project first. 17 + $is_column_mode = ($request->getURIData('mode') === 'column'); 18 + 19 + $src_project = $this->getProject(); 15 20 $state = $this->getViewState(); 16 21 $board_uri = $state->newWorkboardURI(); 17 22 18 23 $layout_engine = $state->getLayoutEngine(); 19 24 20 - $board_phid = $project->getPHID(); 25 + $board_phid = $src_project->getPHID(); 21 26 $columns = $layout_engine->getColumns($board_phid); 22 27 $columns = mpull($columns, null, 'getID'); 23 28 24 29 $column_id = $request->getURIData('columnID'); 25 - $move_column = idx($columns, $column_id); 26 - if (!$move_column) { 30 + $src_column = idx($columns, $column_id); 31 + if (!$src_column) { 27 32 return new Aphront404Response(); 28 33 } 29 34 30 35 $move_task_phids = $layout_engine->getColumnObjectPHIDs( 31 36 $board_phid, 32 - $move_column->getPHID()); 37 + $src_column->getPHID()); 33 38 34 39 $tasks = $state->getObjects(); 35 40 ··· 50 55 ->addCancelButton($board_uri); 51 56 } 52 57 53 - $move_project_phid = $project->getPHID(); 54 - $move_column_phid = null; 55 - $move_project = null; 56 - $move_column = null; 57 - $columns = null; 58 + $dst_project_phid = null; 59 + $dst_project = null; 60 + $has_project = false; 61 + if ($is_column_mode) { 62 + $has_project = true; 63 + $dst_project_phid = $src_project->getPHID(); 64 + } else { 65 + if ($request->isFormOrHiSecPost()) { 66 + $has_project = $request->getStr('hasProject'); 67 + if ($has_project) { 68 + // We may read this from a tokenizer input as an array, or from a 69 + // hidden input as a string. 70 + $dst_project_phid = head($request->getArr('dstProjectPHID')); 71 + if (!$dst_project_phid) { 72 + $dst_project_phid = $request->getStr('dstProjectPHID'); 73 + } 74 + } 75 + } 76 + } 77 + 58 78 $errors = array(); 79 + $hidden = array(); 59 80 60 - if ($request->isFormOrHiSecPost()) { 61 - $move_project_phid = head($request->getArr('moveProjectPHID')); 62 - if (!$move_project_phid) { 63 - $move_project_phid = $request->getStr('moveProjectPHID'); 64 - } 65 - 66 - if (!$move_project_phid) { 67 - if ($request->getBool('hasProject')) { 68 - $errors[] = pht('Choose a project to move tasks to.'); 69 - } 81 + if ($has_project) { 82 + if (!$dst_project_phid) { 83 + $errors[] = pht('Choose a project to move tasks to.'); 70 84 } else { 71 - $target_project = id(new PhabricatorProjectQuery()) 85 + $dst_project = id(new PhabricatorProjectQuery()) 72 86 ->setViewer($viewer) 73 - ->withPHIDs(array($move_project_phid)) 87 + ->withPHIDs(array($dst_project_phid)) 74 88 ->executeOne(); 75 - if (!$target_project) { 76 - $errors[] = pht('You must choose a valid project.'); 77 - } else if (!$project->getHasWorkboard()) { 78 - $errors[] = pht( 79 - 'You must choose a project with a workboard.'); 80 - } else { 81 - $move_project = $target_project; 89 + if (!$dst_project) { 90 + $errors[] = pht('Choose a valid project to move tasks to.'); 91 + } 92 + 93 + if (!$dst_project->getHasWorkboard()) { 94 + $errors[] = pht('You must choose a project with a workboard.'); 95 + $dst_project = null; 82 96 } 83 97 } 98 + } 84 99 85 - if ($move_project) { 86 - $move_engine = id(new PhabricatorBoardLayoutEngine()) 87 - ->setViewer($viewer) 88 - ->setBoardPHIDs(array($move_project->getPHID())) 89 - ->setFetchAllBoards(true) 90 - ->executeLayout(); 100 + if ($dst_project) { 101 + $same_project = ($src_project->getID() === $dst_project->getID()); 102 + 103 + $layout_engine = id(new PhabricatorBoardLayoutEngine()) 104 + ->setViewer($viewer) 105 + ->setBoardPHIDs(array($dst_project->getPHID())) 106 + ->setFetchAllBoards(true) 107 + ->executeLayout(); 91 108 92 - $columns = $move_engine->getColumns($move_project->getPHID()); 93 - $columns = mpull($columns, null, 'getPHID'); 109 + $dst_columns = $layout_engine->getColumns($dst_project->getPHID()); 110 + $dst_columns = mpull($columns, null, 'getPHID'); 94 111 95 - foreach ($columns as $key => $column) { 96 - if ($column->isHidden()) { 97 - unset($columns[$key]); 98 - } 112 + $has_column = false; 113 + $dst_column = null; 114 + 115 + // If we're performing a move on the same board, default the 116 + // control value to the current column. 117 + if ($same_project) { 118 + $dst_column_phid = $src_column->getPHID(); 119 + } else { 120 + $dst_column_phid = null; 121 + } 122 + 123 + if ($request->isFormOrHiSecPost()) { 124 + $has_column = $request->getStr('hasColumn'); 125 + if ($has_column) { 126 + $dst_column_phid = $request->getStr('dstColumnPHID'); 99 127 } 128 + } 100 129 101 - $move_column_phid = $request->getStr('moveColumnPHID'); 102 - if (!$move_column_phid) { 103 - if ($request->getBool('hasColumn')) { 104 - $errors[] = pht('Choose a column to move tasks to.'); 105 - } 130 + if ($has_column) { 131 + $dst_column = idx($dst_columns, $dst_column_phid); 132 + if (!$dst_column) { 133 + $errors[] = pht('Choose a column to move tasks to.'); 106 134 } else { 107 - if (empty($columns[$move_column_phid])) { 108 - $errors[] = pht( 109 - 'Choose a valid column on the target workboard to move '. 110 - 'tasks to.'); 111 - } else if ($columns[$move_column_phid]->getID() == $column_id) { 112 - $errors[] = pht( 113 - 'You can not move tasks from a column to itself.'); 114 - } else { 115 - $move_column = $columns[$move_column_phid]; 135 + if ($dst_column->isHidden()) { 136 + $errors[] = pht('You can not move tasks to a hidden column.'); 137 + $dst_column = null; 138 + } else if ($dst_column->getPHID() === $src_column->getPHID()) { 139 + $errors[] = pht('You can not move tasks from a column to itself.'); 140 + $dst_column = null; 116 141 } 117 142 } 118 143 } 119 - } 120 144 121 - if ($move_column && $move_project) { 122 - foreach ($move_tasks as $move_task) { 123 - $xactions = array(); 145 + if ($dst_column) { 146 + foreach ($move_tasks as $move_task) { 147 + $xactions = array(); 148 + 149 + // If we're switching projects, get out of the old project first 150 + // and move to the new project. 151 + if (!$same_project) { 152 + $xactions[] = id(new ManiphestTransaction()) 153 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 154 + ->setMetadataValue( 155 + 'edge:type', 156 + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) 157 + ->setNewValue( 158 + array( 159 + '-' => array( 160 + $src_project->getPHID() => $src_project->getPHID(), 161 + ), 162 + '+' => array( 163 + $dst_project->getPHID() => $dst_project->getPHID(), 164 + ), 165 + )); 166 + } 124 167 125 - // If we're switching projects, get out of the old project first 126 - // and move to the new project. 127 - if ($move_project->getID() != $project->getID()) { 128 168 $xactions[] = id(new ManiphestTransaction()) 129 - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 130 - ->setMetadataValue( 131 - 'edge:type', 132 - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) 169 + ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) 133 170 ->setNewValue( 134 171 array( 135 - '-' => array( 136 - $project->getPHID() => $project->getPHID(), 137 - ), 138 - '+' => array( 139 - $move_project->getPHID() => $move_project->getPHID(), 172 + array( 173 + 'columnPHID' => $dst_column->getPHID(), 140 174 ), 141 175 )); 142 - } 143 176 144 - $xactions[] = id(new ManiphestTransaction()) 145 - ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) 146 - ->setNewValue( 147 - array( 148 - array( 149 - 'columnPHID' => $move_column->getPHID(), 150 - ), 151 - )); 177 + $editor = id(new ManiphestTransactionEditor()) 178 + ->setActor($viewer) 179 + ->setContinueOnMissingFields(true) 180 + ->setContinueOnNoEffect(true) 181 + ->setContentSourceFromRequest($request) 182 + ->setCancelURI($board_uri); 152 183 153 - $editor = id(new ManiphestTransactionEditor()) 154 - ->setActor($viewer) 155 - ->setContinueOnMissingFields(true) 156 - ->setContinueOnNoEffect(true) 157 - ->setContentSourceFromRequest($request) 158 - ->setCancelURI($board_uri); 184 + $editor->applyTransactions($move_task, $xactions); 185 + } 186 + 187 + // If we did a move on the same workboard, redirect and preserve the 188 + // state parameters. If we moved to a different workboard, go there 189 + // with clean default state. 190 + if ($same_project) { 191 + $done_uri = $board_uri; 192 + } else { 193 + $done_uri = $dst_project->getWorkboardURI(); 194 + } 159 195 160 - $editor->applyTransactions($move_task, $xactions); 196 + return id(new AphrontRedirectResponse())->setURI($done_uri); 161 197 } 162 198 163 - return id(new AphrontRedirectResponse()) 164 - ->setURI($board_uri); 165 - } 199 + $title = pht('Move Tasks to Column'); 166 200 167 - if ($move_project) { 168 - $column_form = id(new AphrontFormView()) 169 - ->setViewer($viewer) 170 - ->appendControl( 201 + $form = id(new AphrontFormView()) 202 + ->setViewer($viewer); 203 + 204 + // If we're moving between projects, add a reminder about which project 205 + // you selected in the previous step. 206 + if (!$is_column_mode) { 207 + $form->appendControl( 208 + id(new AphrontFormStaticControl()) 209 + ->setLabel(pht('Project')) 210 + ->setValue($dst_project->getDisplayName())); 211 + } 212 + 213 + $form->appendControl( 171 214 id(new AphrontFormSelectControl()) 172 - ->setName('moveColumnPHID') 215 + ->setName('dstColumnPHID') 173 216 ->setLabel(pht('Move to Column')) 174 - ->setValue($move_column_phid) 175 - ->setOptions(mpull($columns, 'getDisplayName', 'getPHID'))); 217 + ->setValue($dst_column_phid) 218 + ->setOptions(mpull($dst_columns, 'getDisplayName', 'getPHID'))); 176 219 177 - return $this->newWorkboardDialog() 178 - ->setTitle(pht('Move Tasks')) 179 - ->setWidth(AphrontDialogView::WIDTH_FORM) 180 - ->setErrors($errors) 181 - ->addHiddenInput('moveProjectPHID', $move_project->getPHID()) 182 - ->addHiddenInput('hasColumn', true) 183 - ->addHiddenInput('hasProject', true) 184 - ->appendParagraph( 185 - pht( 186 - 'Choose a column on the %s workboard to move tasks to:', 187 - $viewer->renderHandle($move_project->getPHID()))) 188 - ->appendForm($column_form) 189 - ->addSubmitButton(pht('Move Tasks')) 190 - ->addCancelButton($board_uri); 191 - } 220 + $submit = pht('Move Tasks'); 192 221 193 - if ($move_project_phid) { 194 - $move_project_phid_value = array($move_project_phid); 222 + $hidden['dstProjectPHID'] = $dst_project->getPHID(); 223 + $hidden['hasColumn'] = true; 224 + $hidden['hasProject'] = true; 195 225 } else { 196 - $move_project_phid_value = array(); 197 - } 226 + $title = pht('Move Tasks to Project'); 198 227 199 - $project_form = id(new AphrontFormView()) 200 - ->setViewer($viewer) 201 - ->appendControl( 202 - id(new AphrontFormTokenizerControl()) 203 - ->setName('moveProjectPHID') 204 - ->setLimit(1) 205 - ->setLabel(pht('Move to Project')) 206 - ->setValue($move_project_phid_value) 207 - ->setDatasource(new PhabricatorProjectDatasource())); 228 + if ($dst_project_phid) { 229 + $dst_project_phid_value = array($dst_project_phid); 230 + } else { 231 + $dst_project_phid_value = array(); 232 + } 208 233 209 - return $this->newWorkboardDialog() 210 - ->setTitle(pht('Move Tasks')) 234 + $form = id(new AphrontFormView()) 235 + ->setViewer($viewer) 236 + ->appendControl( 237 + id(new AphrontFormTokenizerControl()) 238 + ->setName('dstProjectPHID') 239 + ->setLimit(1) 240 + ->setLabel(pht('Move to Project')) 241 + ->setValue($dst_project_phid_value) 242 + ->setDatasource(new PhabricatorProjectDatasource())); 243 + 244 + $submit = pht('Continue'); 245 + 246 + $hidden['hasProject'] = true; 247 + } 248 + 249 + $dialog = $this->newWorkboardDialog() 211 250 ->setWidth(AphrontDialogView::WIDTH_FORM) 251 + ->setTitle($title) 212 252 ->setErrors($errors) 213 - ->addHiddenInput('hasProject', true) 214 - ->appendForm($project_form) 215 - ->addSubmitButton(pht('Continue')) 253 + ->appendForm($form) 254 + ->addSubmitButton($submit) 216 255 ->addCancelButton($board_uri); 256 + 257 + foreach ($hidden as $key => $value) { 258 + $dialog->addHiddenInput($key, $value); 259 + } 260 + 261 + return $dialog; 217 262 } 218 263 219 264 }