@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<?php
2
3final class PhabricatorProjectColumnBulkMoveController
4 extends PhabricatorProjectBoardController {
5
6 public function handleRequest(AphrontRequest $request) {
7 $viewer = $request->getViewer();
8
9 $response = $this->loadProject();
10 if ($response) {
11 return $response;
12 }
13
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();
20 $state = $this->getViewState();
21 $board_uri = $state->newWorkboardURI();
22
23 $layout_engine = $state->getLayoutEngine();
24
25 $board_phid = $src_project->getPHID();
26 $columns = $layout_engine->getColumns($board_phid);
27 $columns = mpull($columns, null, 'getID');
28
29 $column_id = $request->getURIData('columnID');
30 $src_column = idx($columns, $column_id);
31 if (!$src_column) {
32 return new Aphront404Response();
33 }
34
35 $move_task_phids = $layout_engine->getColumnObjectPHIDs(
36 $board_phid,
37 $src_column->getPHID());
38
39 $tasks = $state->getObjects();
40
41 $move_tasks = array_select_keys($tasks, $move_task_phids);
42
43 $can_bulk_edit = PhabricatorPolicyFilter::hasCapability(
44 $viewer,
45 PhabricatorApplication::getByClass(
46 PhabricatorManiphestApplication::class),
47 ManiphestBulkEditCapability::CAPABILITY);
48
49 if (!$can_bulk_edit) {
50 return $this->newDialog()
51 ->setTitle(pht('No Movable Tasks'))
52 ->appendParagraph(
53 pht(
54 'You do not have permission to bulk edit tasks.'))
55 ->addCancelButton($board_uri);
56 }
57
58 $move_tasks = id(new PhabricatorPolicyFilter())
59 ->setViewer($viewer)
60 ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
61 ->apply($move_tasks);
62
63 if (!$move_tasks) {
64 return $this->newDialog()
65 ->setTitle(pht('No Movable Tasks'))
66 ->appendParagraph(
67 pht(
68 'The selected column contains no visible tasks which you '.
69 'have permission to move.'))
70 ->addCancelButton($board_uri);
71 }
72
73 $dst_project_phid = null;
74 $dst_project = null;
75 $has_project = false;
76 if ($is_column_mode) {
77 $has_project = true;
78 $dst_project_phid = $src_project->getPHID();
79 } else {
80 if ($request->isFormOrHiSecPost()) {
81 $has_project = $request->getStr('hasProject');
82 if ($has_project) {
83 // We may read this from a tokenizer input as an array, or from a
84 // hidden input as a string.
85 $dst_project_phid = head($request->getArr('dstProjectPHID'));
86 if (!$dst_project_phid) {
87 $dst_project_phid = $request->getStr('dstProjectPHID');
88 }
89 }
90 }
91 }
92
93 $errors = array();
94 $hidden = array();
95
96 if ($has_project) {
97 if (!$dst_project_phid) {
98 $errors[] = pht('Choose a project to move tasks to.');
99 } else {
100 $dst_project = id(new PhabricatorProjectQuery())
101 ->setViewer($viewer)
102 ->withPHIDs(array($dst_project_phid))
103 ->executeOne();
104 if (!$dst_project) {
105 $errors[] = pht('Choose a valid project to move tasks to.');
106 }
107
108 if (!$dst_project->getHasWorkboard()) {
109 $errors[] = pht('You must choose a project with a workboard.');
110 $dst_project = null;
111 }
112 }
113 }
114
115 if ($dst_project) {
116 $same_project = ($src_project->getID() === $dst_project->getID());
117
118 $layout_engine = id(new PhabricatorBoardLayoutEngine())
119 ->setViewer($viewer)
120 ->setBoardPHIDs(array($dst_project->getPHID()))
121 ->setFetchAllBoards(true)
122 ->executeLayout();
123
124 $dst_columns = $layout_engine->getColumns($dst_project->getPHID());
125 $dst_columns = mpull($dst_columns, null, 'getPHID');
126
127 // Prevent moves to milestones or subprojects by selecting their
128 // columns, since the implications aren't obvious and this doesn't
129 // work the same way as normal column moves.
130 foreach ($dst_columns as $key => $dst_column) {
131 if ($dst_column->getProxyPHID()) {
132 unset($dst_columns[$key]);
133 }
134 }
135
136 $has_column = false;
137 $dst_column = null;
138
139 // If we're performing a move on the same board, default the
140 // control value to the current column.
141 if ($same_project) {
142 $dst_column_phid = $src_column->getPHID();
143 } else {
144 $dst_column_phid = null;
145 }
146
147 if ($request->isFormOrHiSecPost()) {
148 $has_column = $request->getStr('hasColumn');
149 if ($has_column) {
150 $dst_column_phid = $request->getStr('dstColumnPHID');
151 }
152 }
153
154 if ($has_column) {
155 $dst_column = idx($dst_columns, $dst_column_phid);
156 if (!$dst_column) {
157 $errors[] = pht('Choose a column to move tasks to.');
158 } else {
159 if ($dst_column->isHidden()) {
160 $errors[] = pht('You can not move tasks to a hidden column.');
161 $dst_column = null;
162 } else if ($dst_column->getPHID() === $src_column->getPHID()) {
163 $errors[] = pht('You can not move tasks from a column to itself.');
164 $dst_column = null;
165 }
166 }
167 }
168
169 if ($dst_column) {
170 foreach ($move_tasks as $move_task) {
171 $xactions = array();
172
173 // If we're switching projects, get out of the old project first
174 // and move to the new project.
175 if (!$same_project) {
176 $xactions[] = id(new ManiphestTransaction())
177 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
178 ->setMetadataValue(
179 'edge:type',
180 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
181 ->setNewValue(
182 array(
183 '-' => array(
184 $src_project->getPHID() => $src_project->getPHID(),
185 ),
186 '+' => array(
187 $dst_project->getPHID() => $dst_project->getPHID(),
188 ),
189 ));
190 }
191
192 $xactions[] = id(new ManiphestTransaction())
193 ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
194 ->setNewValue(
195 array(
196 array(
197 'columnPHID' => $dst_column->getPHID(),
198 ),
199 ));
200
201 $editor = id(new ManiphestTransactionEditor())
202 ->setActor($viewer)
203 ->setContinueOnMissingFields(true)
204 ->setContinueOnNoEffect(true)
205 ->setContentSourceFromRequest($request)
206 ->setCancelURI($board_uri);
207
208 $editor->applyTransactions($move_task, $xactions);
209 }
210
211 // If we did a move on the same workboard, redirect and preserve the
212 // state parameters. If we moved to a different workboard, go there
213 // with clean default state.
214 if ($same_project) {
215 $done_uri = $board_uri;
216 } else {
217 $done_uri = $dst_project->getWorkboardURI();
218 }
219
220 return id(new AphrontRedirectResponse())->setURI($done_uri);
221 }
222
223 $title = pht('Move Tasks to Column');
224
225 $form = id(new AphrontFormView())
226 ->setViewer($viewer);
227
228 // If we're moving between projects, add a reminder about which project
229 // you selected in the previous step.
230 if (!$is_column_mode) {
231 $form->appendControl(
232 id(new AphrontFormStaticControl())
233 ->setLabel(pht('Project'))
234 ->setValue($dst_project->getDisplayName()));
235 }
236
237 $column_options = array(
238 'visible' => array(),
239 'hidden' => array(),
240 );
241
242 $any_hidden = false;
243 foreach ($dst_columns as $column) {
244 if (!$column->isHidden()) {
245 $group = 'visible';
246 } else {
247 $group = 'hidden';
248 }
249
250 $phid = $column->getPHID();
251 $display_name = $column->getDisplayName();
252
253 $column_options[$group][$phid] = $display_name;
254 }
255
256 if ($column_options['hidden']) {
257 $column_options = array(
258 pht('Visible Columns') => $column_options['visible'],
259 pht('Hidden Columns') => $column_options['hidden'],
260 );
261 } else {
262 $column_options = $column_options['visible'];
263 }
264
265 $form->appendControl(
266 id(new AphrontFormSelectControl())
267 ->setName('dstColumnPHID')
268 ->setLabel(pht('Move to Column'))
269 ->setValue($dst_column_phid)
270 ->setOptions($column_options));
271
272 $submit = pht('Move Tasks');
273
274 $hidden['dstProjectPHID'] = $dst_project->getPHID();
275 $hidden['hasColumn'] = true;
276 $hidden['hasProject'] = true;
277 } else {
278 $title = pht('Move Tasks to Project');
279
280 if ($dst_project_phid) {
281 $dst_project_phid_value = array($dst_project_phid);
282 } else {
283 $dst_project_phid_value = array();
284 }
285
286 $form = id(new AphrontFormView())
287 ->setViewer($viewer)
288 ->appendControl(
289 id(new AphrontFormTokenizerControl())
290 ->setName('dstProjectPHID')
291 ->setLimit(1)
292 ->setLabel(pht('Move to Project'))
293 ->setValue($dst_project_phid_value)
294 ->setDatasource(new PhabricatorProjectDatasource()));
295
296 $submit = pht('Continue');
297
298 $hidden['hasProject'] = true;
299 }
300
301 $dialog = $this->newWorkboardDialog()
302 ->setWidth(AphrontDialogView::WIDTH_FORM)
303 ->setTitle($title)
304 ->setErrors($errors)
305 ->appendForm($form)
306 ->addSubmitButton($submit)
307 ->addCancelButton($board_uri);
308
309 foreach ($hidden as $key => $value) {
310 $dialog->addHiddenInput($key, $value);
311 }
312
313 return $dialog;
314 }
315
316}