@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 PhabricatorProjectBoardImportController
4 extends PhabricatorProjectBoardController {
5
6 public function handleRequest(AphrontRequest $request) {
7 $viewer = $request->getViewer();
8 $project_id = $request->getURIData('projectID');
9
10 $project = id(new PhabricatorProjectQuery())
11 ->setViewer($viewer)
12 ->requireCapabilities(
13 array(
14 PhabricatorPolicyCapability::CAN_VIEW,
15 PhabricatorPolicyCapability::CAN_EDIT,
16 ))
17 ->withIDs(array($project_id))
18 ->executeOne();
19 if (!$project) {
20 return new Aphront404Response();
21 }
22 $this->setProject($project);
23
24 $project_id = $project->getID();
25 $board_uri = $this->getApplicationURI("board/{$project_id}/");
26
27 // See PHI1025. We only want to prevent the import if the board already has
28 // real columns. If it has proxy columns (for example, for milestones) you
29 // can still import columns from another board.
30 $columns = id(new PhabricatorProjectColumnQuery())
31 ->setViewer($viewer)
32 ->withProjectPHIDs(array($project->getPHID()))
33 ->withIsProxyColumn(false)
34 ->execute();
35 if ($columns) {
36 return $this->newDialog()
37 ->setTitle(pht('Workboard Already Has Columns'))
38 ->appendParagraph(
39 pht(
40 'You can not import columns into this workboard because it '.
41 'already has columns. You can only import into an empty '.
42 'workboard.'))
43 ->addCancelButton($board_uri);
44 }
45
46 if ($request->isFormPost()) {
47 $import_phid = $request->getArr('importProjectPHID');
48 $import_phid = reset($import_phid);
49
50 $import_columns = id(new PhabricatorProjectColumnQuery())
51 ->setViewer($viewer)
52 ->withProjectPHIDs(array($import_phid))
53 ->withIsProxyColumn(false)
54 ->execute();
55 if (!$import_columns) {
56 return $this->newDialog()
57 ->setTitle(pht('Source Workboard Has No Columns'))
58 ->appendParagraph(
59 pht(
60 'You can not import columns from that workboard because it has '.
61 'no importable columns.'))
62 ->addCancelButton($board_uri);
63 }
64
65 $table = id(new PhabricatorProjectColumn())
66 ->openTransaction();
67 foreach ($import_columns as $import_column) {
68 if ($import_column->isHidden()) {
69 continue;
70 }
71
72 $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer)
73 ->setSequence($import_column->getSequence())
74 ->setProjectPHID($project->getPHID())
75 ->setName($import_column->getName())
76 ->setProperties($import_column->getProperties())
77 ->save();
78 }
79 $xactions = array();
80 $xactions[] = id(new PhabricatorProjectTransaction())
81 ->setTransactionType(
82 PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
83 ->setNewValue(1);
84
85 id(new PhabricatorProjectTransactionEditor())
86 ->setActor($viewer)
87 ->setContentSourceFromRequest($request)
88 ->setContinueOnNoEffect(true)
89 ->setContinueOnMissingFields(true)
90 ->applyTransactions($project, $xactions);
91
92 $table->saveTransaction();
93
94 return id(new AphrontRedirectResponse())->setURI($board_uri);
95 }
96
97 // Default value. The Tokenizer wants an array of phids.
98 $tokenizer_value = array();
99
100 // Default to the previous milestone, if available.
101 $value_candidate = $this->getPreviousMilestoneIfHasImportableColumns(
102 $viewer, $project);
103
104 if ($value_candidate) {
105 $tokenizer_value[] = $value_candidate->getPHID();
106 }
107
108 $proj_selector = id(new AphrontFormTokenizerControl())
109 ->setName('importProjectPHID')
110 ->setUser($viewer)
111 ->setValue($tokenizer_value)
112 ->setDatasource(id(new PhabricatorProjectDatasource())
113 ->setParameters(array('mustHaveColumns' => true))
114 ->setLimit(1));
115
116 return $this->newDialog()
117 ->setTitle(pht('Import Columns'))
118 ->setWidth(AphrontDialogView::WIDTH_FORM)
119 ->appendParagraph(pht('Choose a project or a milestone to import '.
120 'columns from:'))
121 ->appendChild($proj_selector)
122 ->addCancelButton($board_uri)
123 ->addSubmitButton(pht('Import'));
124 }
125
126 /**
127 * Starting from a milestone, get the previous milestone,
128 * but only if it has at least one column that could be imported.
129 * @param PhabricatorUser $viewer Current user
130 * @param PhabricatorProject $milestone Current milestone with no
131 * workboard yet.
132 * Technically, this parameter
133 * could also be a project,
134 * and projects are silently
135 * rejected if passed here.
136 * @return PhabricatorProject|null The milestone preceding
137 * your specified milestone,
138 * but only if it has at least
139 * one importable column;
140 * null in any other case.
141 */
142 private function getPreviousMilestoneIfHasImportableColumns(
143 PhabricatorUser $viewer,
144 PhabricatorProject $milestone): ?PhabricatorProject {
145
146 // We can suggest something, only if you are creating workboard
147 // on a milestone.
148 if (!$milestone->isMilestone()) {
149 return null;
150 }
151
152 // Possible design choices:
153 // 1. Suggest the most recent milestone (excluding this specific one).
154 // CONS: Suggesting the most recent milestone doesn't make much sense,
155 // when I want to create a workboard from a milestone from
156 // a year ago.
157 // Side note: to suggest the 'most recent', we need at least one
158 // more query to find that number. It needs a "SELECT MAX(n)".
159 // 2. Suggest the precedent milestone.
160 // PRO: convenient when you already worked on a workboard, and you just
161 // create an additional milestone; convenient also when you create
162 // a workboard on a old milestone, so as to maintain the
163 // workboard structure of that time.
164 // Side note: finding the previous milestone is also extremely
165 // easy. We avoid the extra query "SELECT MAX(n)".
166 //
167 // So we go with: 2. Suggest the precedent milestone.
168 $previous_milestone_num = (int)$milestone->getMilestoneNumber();
169 $previous_milestone_num--;
170 if ($previous_milestone_num < 1) {
171 return null;
172 }
173
174 $previous_milestone = $this->getProjectMilestoneFromNumber(
175 $viewer,
176 $milestone->getParentProjectPHID(),
177 $previous_milestone_num);
178
179 if (!$previous_milestone) {
180 return null;
181 }
182
183 // Micro-optimization to avoid querying columns.
184 if (!$previous_milestone->getHasWorkboard()) {
185 return null;
186 }
187
188 // Check if this milestone has at least one
189 // existing column which could be imported, or we can cause the error
190 // 'Source Workboard Has No Columns',
191 // and it would be more confusing than useful.
192 $example_column = $this->getOneImportableProjectColumn(
193 $viewer,
194 $previous_milestone->getPHID());
195
196 if (!$example_column) {
197 return null;
198 }
199
200 return $previous_milestone;
201 }
202
203 /**
204 * Get the milestone of a project from its milestone number.
205 * @param PhabricatorUser $viewer Current user
206 * @param string $proj_phid Project PHID
207 * @param int $number Milestone number
208 */
209 private function getProjectMilestoneFromNumber(
210 PhabricatorUser $viewer,
211 string $proj_phid, int $number): ?PhabricatorProject {
212
213 $query_proj = new PhabricatorProjectQuery();
214 return $query_proj
215 ->setViewer($viewer)
216 ->withParentProjectPHIDs(array($proj_phid))
217 ->withIsMilestone(true)
218 ->withMilestoneNumberBetween($number, $number)
219 ->executeOne();
220 }
221
222 /**
223 * Get whatever non-proxy column on the workboard,
224 * if existing, except for the default "Backlog" column.
225 * @param PhabricatorUser $viewer Current user
226 * @param string $proj_phid Project PHID
227 * @return PhabricatorProjectColumn|null Column, or null
228 * if there are none.
229 */
230 private function getOneImportableProjectColumn(
231 PhabricatorUser $viewer,
232 string $proj_phid): ?PhabricatorProjectColumn {
233
234 // Query one (whatever) project column suitable for the import.
235 // This code is inspired from PhabricatorProjectDatasource,
236 // looking at its parameter 'mustHaveColumns'.
237 $query_columns = new PhabricatorProjectColumnQuery();
238 return $query_columns
239 ->setViewer($viewer)
240 ->withProjectPHIDs(array($proj_phid))
241 ->withIsProxyColumn(false)
242 ->setLimit(1)
243 ->executeOne();
244 }
245
246}