@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 PhabricatorProjectColumn
4 extends PhabricatorProjectDAO
5 implements
6 PhabricatorApplicationTransactionInterface,
7 PhabricatorPolicyInterface,
8 PhabricatorDestructibleInterface,
9 PhabricatorExtendedPolicyInterface,
10 PhabricatorConduitResultInterface {
11
12 const STATUS_ACTIVE = 0;
13 const STATUS_HIDDEN = 1;
14
15 protected $name;
16 protected $status;
17 protected $projectPHID;
18 protected $proxyPHID;
19 protected $sequence;
20 protected $properties = array();
21 protected $triggerPHID;
22
23 private $project = self::ATTACHABLE;
24 private $proxy = self::ATTACHABLE;
25 private $trigger = self::ATTACHABLE;
26
27 public static function initializeNewColumn(PhabricatorUser $user) {
28 return id(new PhabricatorProjectColumn())
29 ->setName('')
30 ->setStatus(self::STATUS_ACTIVE)
31 ->attachProxy(null);
32 }
33
34 protected function getConfiguration() {
35 return array(
36 self::CONFIG_AUX_PHID => true,
37 self::CONFIG_SERIALIZATION => array(
38 'properties' => self::SERIALIZATION_JSON,
39 ),
40 self::CONFIG_COLUMN_SCHEMA => array(
41 'name' => 'text255',
42 'status' => 'uint32',
43 'sequence' => 'uint32',
44 'proxyPHID' => 'phid?',
45 'triggerPHID' => 'phid?',
46 ),
47 self::CONFIG_KEY_SCHEMA => array(
48 'key_status' => array(
49 'columns' => array('projectPHID', 'status', 'sequence'),
50 ),
51 'key_sequence' => array(
52 'columns' => array('projectPHID', 'sequence'),
53 ),
54 'key_proxy' => array(
55 'columns' => array('projectPHID', 'proxyPHID'),
56 'unique' => true,
57 ),
58 'key_trigger' => array(
59 'columns' => array('triggerPHID'),
60 ),
61 ),
62 ) + parent::getConfiguration();
63 }
64
65 public function generatePHID() {
66 return PhabricatorPHID::generateNewPHID(
67 PhabricatorProjectColumnPHIDType::TYPECONST);
68 }
69
70 public function attachProject(PhabricatorProject $project) {
71 $this->project = $project;
72 return $this;
73 }
74
75 public function getProject() {
76 return $this->assertAttached($this->project);
77 }
78
79 public function attachProxy($proxy) {
80 $this->proxy = $proxy;
81 return $this;
82 }
83
84 public function getProxy() {
85 return $this->assertAttached($this->proxy);
86 }
87
88 public function isDefaultColumn() {
89 return (bool)$this->getProperty('isDefault');
90 }
91
92 public function isHidden() {
93 $proxy = $this->getProxy();
94 if ($proxy) {
95 return $proxy->isArchived();
96 }
97
98 return ($this->getStatus() == self::STATUS_HIDDEN);
99 }
100
101 public function getDisplayName() {
102 $proxy = $this->getProxy();
103 if ($proxy) {
104 return $proxy->getProxyColumnName();
105 }
106
107 $name = $this->getName();
108 if (strlen($name)) {
109 return $name;
110 }
111
112 if ($this->isDefaultColumn()) {
113 return pht('Backlog');
114 }
115
116 return pht('Unnamed Column');
117 }
118
119 public function getDisplayType() {
120 if ($this->isDefaultColumn()) {
121 return pht('(Default)');
122 }
123 if ($this->isHidden()) {
124 return pht('(Hidden)');
125 }
126
127 return null;
128 }
129
130 public function getDisplayClass() {
131 $proxy = $this->getProxy();
132 if ($proxy) {
133 return $proxy->getProxyColumnClass();
134 }
135
136 return null;
137 }
138
139 public function getHeaderIcon() {
140 $proxy = $this->getProxy();
141 if ($proxy) {
142 return $proxy->getProxyColumnIcon();
143 }
144
145 if ($this->isHidden()) {
146 return 'fa-eye-slash';
147 }
148
149 return null;
150 }
151
152 public function getProperty($key, $default = null) {
153 return idx($this->properties, $key, $default);
154 }
155
156 public function setProperty($key, $value) {
157 $this->properties[$key] = $value;
158 return $this;
159 }
160
161 public function getPointLimit() {
162 return $this->getProperty('pointLimit');
163 }
164
165 public function setPointLimit($limit) {
166 $this->setProperty('pointLimit', $limit);
167 return $this;
168 }
169
170 public function getOrderingKey() {
171 $proxy = $this->getProxy();
172
173 // Normal columns and subproject columns go first, in a user-controlled
174 // order.
175
176 // All the milestone columns go last, in their sequential order.
177
178 if (!$proxy || !$proxy->isMilestone()) {
179 $group = 'A';
180 $sequence = $this->getSequence();
181 } else {
182 $group = 'B';
183 $sequence = $proxy->getMilestoneNumber();
184 }
185
186 return sprintf('%s%012d', $group, $sequence);
187 }
188
189 public function attachTrigger(?PhabricatorProjectTrigger $trigger = null) {
190 $this->trigger = $trigger;
191 return $this;
192 }
193
194 /**
195 * @return PhabricatorProjectTrigger|null
196 */
197 public function getTrigger() {
198 return $this->assertAttached($this->trigger);
199 }
200
201 public function canHaveTrigger() {
202 // Backlog columns and proxy (subproject / milestone) columns can't have
203 // triggers because cards routinely end up in these columns through tag
204 // edits rather than drag-and-drop and it would likely be confusing to
205 // have these triggers act only a small fraction of the time.
206
207 if ($this->isDefaultColumn()) {
208 return false;
209 }
210
211 if ($this->getProxy()) {
212 return false;
213 }
214
215 return true;
216 }
217
218 public function getWorkboardURI() {
219 return $this->getProject()->getWorkboardURI();
220 }
221
222 public function getDropEffects() {
223 $effects = array();
224
225 $proxy = $this->getProxy();
226 if ($proxy && $proxy->isMilestone()) {
227 $effects[] = id(new PhabricatorProjectDropEffect())
228 ->setIcon($proxy->getProxyColumnIcon())
229 ->setColor('violet')
230 ->setContent(
231 pht(
232 'Move to milestone %s.',
233 phutil_tag('strong', array(), $this->getDisplayName())));
234 } else {
235 $effects[] = id(new PhabricatorProjectDropEffect())
236 ->setIcon('fa-columns')
237 ->setColor('blue')
238 ->setContent(
239 pht(
240 'Move to column %s.',
241 phutil_tag('strong', array(), $this->getDisplayName())));
242 }
243
244
245 if ($this->canHaveTrigger()) {
246 $trigger = $this->getTrigger();
247 if ($trigger) {
248 foreach ($trigger->getDropEffects() as $trigger_effect) {
249 $effects[] = $trigger_effect;
250 }
251 }
252 }
253
254 return $effects;
255 }
256
257
258/* -( PhabricatorConduitResultInterface )---------------------------------- */
259
260 public function getFieldSpecificationsForConduit() {
261 return array(
262 id(new PhabricatorConduitSearchFieldSpecification())
263 ->setKey('name')
264 ->setType('string')
265 ->setDescription(pht('The display name of the column.')),
266 id(new PhabricatorConduitSearchFieldSpecification())
267 ->setKey('project')
268 ->setType('map<string, wild>')
269 ->setDescription(pht('The project the column belongs to.')),
270 id(new PhabricatorConduitSearchFieldSpecification())
271 ->setKey('proxyPHID')
272 ->setType('phid?')
273 ->setDescription(
274 pht(
275 'For columns that proxy another object (like a subproject or '.
276 'milestone), the PHID of the object they proxy.')),
277 id(new PhabricatorConduitSearchFieldSpecification())
278 ->setKey('isHidden')
279 ->setType('bool')
280 ->setDescription(pht('True if this column is hidden.')),
281 id(new PhabricatorConduitSearchFieldSpecification())
282 ->setKey('isDefaultColumn')
283 ->setType('bool')
284 ->setDescription(pht('True if this is the default column.')),
285 id(new PhabricatorConduitSearchFieldSpecification())
286 ->setKey('sequence')
287 ->setType('int')
288 ->setDescription(
289 pht('The sequence in which this column appears on the workboard.')),
290 );
291 }
292
293 public function getFieldValuesForConduit() {
294 return array(
295 'name' => $this->getDisplayName(),
296 'proxyPHID' => $this->getProxyPHID(),
297 'project' => $this->getProject()->getRefForConduit(),
298 'isHidden' => $this->isHidden(),
299 'isDefaultColumn' => $this->isDefaultColumn(),
300 'sequence' => (int)$this->getSequence(),
301 );
302 }
303
304 public function getConduitSearchAttachments() {
305 return array();
306 }
307
308 public function getRefForConduit() {
309 return array(
310 'id' => (int)$this->getID(),
311 'phid' => $this->getPHID(),
312 'name' => $this->getDisplayName(),
313 );
314 }
315
316
317/* -( PhabricatorApplicationTransactionInterface )------------------------- */
318
319
320 public function getApplicationTransactionEditor() {
321 return new PhabricatorProjectColumnTransactionEditor();
322 }
323
324 public function getApplicationTransactionTemplate() {
325 return new PhabricatorProjectColumnTransaction();
326 }
327
328
329/* -( PhabricatorPolicyInterface )----------------------------------------- */
330
331
332 public function getCapabilities() {
333 return array(
334 PhabricatorPolicyCapability::CAN_VIEW,
335 PhabricatorPolicyCapability::CAN_EDIT,
336 );
337 }
338
339 public function getPolicy($capability) {
340 // NOTE: Column policies are enforced as an extended policy which makes
341 // them the same as the project's policies.
342 switch ($capability) {
343 case PhabricatorPolicyCapability::CAN_VIEW:
344 return PhabricatorPolicies::getMostOpenPolicy();
345 case PhabricatorPolicyCapability::CAN_EDIT:
346 return PhabricatorPolicies::POLICY_USER;
347 }
348 }
349
350 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
351 return $this->getProject()->hasAutomaticCapability(
352 $capability,
353 $viewer);
354 }
355
356 public function describeAutomaticCapability($capability) {
357 return pht('Users must be able to see a project to see its board.');
358 }
359
360
361/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
362
363
364 public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
365 return array(
366 array($this->getProject(), $capability),
367 );
368 }
369
370
371/* -( PhabricatorDestructibleInterface )----------------------------------- */
372
373 public function destroyObjectPermanently(
374 PhabricatorDestructionEngine $engine) {
375
376 $this->openTransaction();
377 $this->delete();
378 $this->saveTransaction();
379 }
380
381}