@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 PhabricatorApplicationTransactionCommentView
4 extends AphrontView {
5
6 private $submitButtonName;
7 private $action;
8
9 private $previewPanelID;
10 private $previewTimelineID;
11 private $previewToggleID;
12 private $formID;
13 private $commentID;
14 private $draft;
15 private $requestURI;
16 private $showPreview = true;
17 private $object;
18 private $headerText;
19 private $noPermission;
20 private $fullWidth;
21 private $infoView;
22 private $editEngine;
23 private $editEngineLock;
24 private $noBorder;
25 private $requiresMFA;
26
27 private $currentVersion;
28 private $versionedDraft;
29 private $commentActions;
30 private $commentActionGroups = array();
31 private $transactionTimeline;
32
33 /**
34 * Set object in which this comment textarea field is displayed
35 */
36 public function setObject($object) {
37 $this->object = $object;
38 return $this;
39 }
40
41 /**
42 * Get object in which this comment textarea is displayed
43 */
44 public function getObject() {
45 return $this->object;
46 }
47
48 public function setShowPreview($show_preview) {
49 $this->showPreview = $show_preview;
50 return $this;
51 }
52
53 public function getShowPreview() {
54 return $this->showPreview;
55 }
56
57 public function setRequestURI(PhutilURI $request_uri) {
58 $this->requestURI = $request_uri;
59 return $this;
60 }
61 public function getRequestURI() {
62 return $this->requestURI;
63 }
64
65 public function setCurrentVersion($current_version) {
66 $this->currentVersion = $current_version;
67 return $this;
68 }
69
70 public function getCurrentVersion() {
71 return $this->currentVersion;
72 }
73
74 public function setVersionedDraft(
75 PhabricatorVersionedDraft $versioned_draft) {
76 $this->versionedDraft = $versioned_draft;
77 return $this;
78 }
79
80 public function getVersionedDraft() {
81 return $this->versionedDraft;
82 }
83
84 public function setDraft(PhabricatorDraft $draft) {
85 $this->draft = $draft;
86 return $this;
87 }
88
89 public function getDraft() {
90 return $this->draft;
91 }
92
93 public function setSubmitButtonName($submit_button_name) {
94 $this->submitButtonName = $submit_button_name;
95 return $this;
96 }
97
98 public function getSubmitButtonName() {
99 return $this->submitButtonName;
100 }
101
102 public function setAction($action) {
103 $this->action = $action;
104 return $this;
105 }
106
107 public function getAction() {
108 return $this->action;
109 }
110
111 public function setHeaderText($text) {
112 $this->headerText = $text;
113 return $this;
114 }
115
116 public function setFullWidth($fw) {
117 $this->fullWidth = $fw;
118 return $this;
119 }
120
121 public function setInfoView(PHUIInfoView $info_view) {
122 $this->infoView = $info_view;
123 return $this;
124 }
125
126 public function getInfoView() {
127 return $this->infoView;
128 }
129
130 /**
131 * @param array<PhabricatorEditEngineCommentAction> $comment_actions
132 */
133 public function setCommentActions(array $comment_actions) {
134 assert_instances_of(
135 $comment_actions,
136 PhabricatorEditEngineCommentAction::class);
137 $this->commentActions = $comment_actions;
138 return $this;
139 }
140
141 public function getCommentActions() {
142 return $this->commentActions;
143 }
144
145 /**
146 * @param array<PhabricatorEditEngineCommentActionGroup> $groups
147 */
148 public function setCommentActionGroups(array $groups) {
149 assert_instances_of(
150 $groups,
151 PhabricatorEditEngineCommentActionGroup::class);
152 $this->commentActionGroups = $groups;
153 return $this;
154 }
155
156 public function getCommentActionGroups() {
157 return $this->commentActionGroups;
158 }
159
160 public function setNoPermission($no_permission) {
161 $this->noPermission = $no_permission;
162 return $this;
163 }
164
165 public function getNoPermission() {
166 return $this->noPermission;
167 }
168
169 public function setEditEngine(PhabricatorEditEngine $edit_engine) {
170 $this->editEngine = $edit_engine;
171 return $this;
172 }
173
174 public function getEditEngine() {
175 return $this->editEngine;
176 }
177
178 public function setEditEngineLock(PhabricatorEditEngineLock $lock) {
179 $this->editEngineLock = $lock;
180 return $this;
181 }
182
183 public function getEditEngineLock() {
184 return $this->editEngineLock;
185 }
186
187 public function setRequiresMFA($requires_mfa) {
188 $this->requiresMFA = $requires_mfa;
189 return $this;
190 }
191
192 public function getRequiresMFA() {
193 return $this->requiresMFA;
194 }
195
196 public function setTransactionTimeline(
197 PhabricatorApplicationTransactionView $timeline) {
198
199 $timeline->setQuoteTargetID($this->getCommentID());
200 if ($this->getNoPermission() || $this->getEditEngineLock()) {
201 $timeline->setShouldTerminate(true);
202 }
203
204 $this->transactionTimeline = $timeline;
205 return $this;
206 }
207
208 public function render() {
209 if ($this->getNoPermission()) {
210 return null;
211 }
212
213 $lock = $this->getEditEngineLock();
214 if ($lock) {
215 return id(new PHUIInfoView())
216 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
217 ->setErrors(
218 array(
219 $lock->getLockedObjectDisplayText(),
220 ));
221 }
222
223 $viewer = $this->getViewer();
224 if (!$viewer->isLoggedIn()) {
225 $uri = id(new PhutilURI('/login/'))
226 ->replaceQueryParam('next', (string)$this->getRequestURI());
227 return id(new PHUIObjectBoxView())
228 ->setFlush(true)
229 ->appendChild(
230 javelin_tag(
231 'a',
232 array(
233 'class' => 'login-to-comment button',
234 'href' => $uri,
235 ),
236 pht('Log In to Comment')));
237 }
238
239 if ($this->getRequiresMFA()) {
240 if (!$viewer->getIsEnrolledInMultiFactor()) {
241 $viewer->updateMultiFactorEnrollment();
242 if (!$viewer->getIsEnrolledInMultiFactor()) {
243 $messages = array();
244 $messages[] = pht(
245 'You must provide multi-factor credentials to comment or make '.
246 'changes, but you do not have multi-factor authentication '.
247 'configured on your account.');
248 $messages[] = pht(
249 'To continue, configure multi-factor authentication in Settings.');
250
251 return id(new PHUIInfoView())
252 ->setSeverity(PHUIInfoView::SEVERITY_MFA)
253 ->setErrors($messages);
254 }
255 }
256 }
257
258 $data = array();
259
260 $comment = $this->renderCommentPanel();
261
262 if ($this->getShowPreview()) {
263 $preview = $this->renderPreviewPanel();
264 } else {
265 $preview = null;
266 }
267
268 if (!$this->getCommentActions()) {
269 Javelin::initBehavior(
270 'phabricator-transaction-comment-form',
271 array(
272 'formID' => $this->getFormID(),
273 'timelineID' => $this->getPreviewTimelineID(),
274 'panelID' => $this->getPreviewPanelID(),
275 'showPreview' => $this->getShowPreview(),
276 'actionURI' => $this->getAction(),
277 ));
278 }
279
280 require_celerity_resource('phui-comment-form-css');
281 $image_uri = $viewer->getProfileImageURI();
282 $image = javelin_tag(
283 'div',
284 array(
285 'style' => 'background-image: url('.$image_uri.')',
286 'class' => 'phui-comment-image',
287 'aural' => false,
288 ));
289 $wedge = phutil_tag(
290 'div',
291 array(
292 'class' => 'phui-timeline-wedge',
293 ),
294 '');
295
296 $badge_view = $this->renderBadgeView();
297
298 $anchor = id(new PhabricatorAnchorView())
299 ->setAnchorName('reply');
300
301 $comment_box = id(new PHUIObjectBoxView())
302 ->setFlush(true)
303 ->addClass('phui-comment-form-view')
304 ->addSigil('phui-comment-form')
305 ->appendChild($anchor)
306 ->appendChild(
307 phutil_tag(
308 'h2',
309 array(
310 'class' => 'aural-only',
311 ),
312 pht('Add Comment')))
313 ->appendChild($image)
314 ->appendChild($badge_view)
315 ->appendChild($wedge)
316 ->appendChild($comment);
317
318 return array($comment_box, $preview);
319 }
320
321 private function renderCommentPanel() {
322 $viewer = $this->getViewer();
323 $engine = $this->getEditEngine();
324 // In a few rare cases PhabricatorApplicationTransactionCommentView gets
325 // initiated in a View or Controller class. Don't crash in that case.
326 if ($engine) {
327 $placeholder_text = $engine
328 ->getCommentFieldPlaceholderText($this->getObject());
329 } else {
330 $placeholder_text = '';
331 }
332
333 $remarkup_control = id(new PhabricatorRemarkupControl())
334 ->setViewer($viewer)
335 ->setID($this->getCommentID())
336 ->addClass('phui-comment-fullwidth-control')
337 ->addClass('phui-comment-textarea-control')
338 ->setSurroundingObject($this->getObject())
339 ->setCanPin(true)
340 ->setPlaceholder($placeholder_text)
341 ->setName('comment');
342
343 $draft_comment = '';
344 $draft_metadata = array();
345 $draft_key = null;
346
347 $legacy_draft = $this->getDraft();
348 if ($legacy_draft) {
349 $draft_comment = $legacy_draft->getDraft();
350 $draft_key = $legacy_draft->getDraftKey();
351 }
352
353 $versioned_draft = $this->getVersionedDraft();
354 if ($versioned_draft) {
355 $draft_comment = $versioned_draft->getProperty(
356 'comment',
357 $draft_comment);
358 $draft_metadata = $versioned_draft->getProperty(
359 'metadata',
360 $draft_metadata);
361 }
362
363 $remarkup_control->setValue($draft_comment);
364
365 if (!is_array($draft_metadata)) {
366 $draft_metadata = array();
367 }
368 $remarkup_control->setRemarkupMetadata($draft_metadata);
369
370 if (!$this->getObject()->getPHID()) {
371 throw new PhutilInvalidStateException('setObjectPHID', 'render');
372 }
373
374 $version_key = PhabricatorVersionedDraft::KEY_VERSION;
375 $version_value = $this->getCurrentVersion();
376
377 $form = id(new AphrontFormView())
378 ->setViewer($viewer)
379 ->addSigil('transaction-append')
380 ->setWorkflow(true)
381 ->setFullWidth($this->fullWidth)
382 ->setMetadata(
383 array(
384 'objectPHID' => $this->getObject()->getPHID(),
385 ))
386 ->setAction($this->getAction())
387 ->setID($this->getFormID())
388 ->addHiddenInput('__draft__', $draft_key)
389 ->addHiddenInput($version_key, $version_value);
390
391 $comment_actions = $this->getCommentActions();
392 if ($comment_actions) {
393 $action_map = array();
394 $type_map = array();
395
396 $comment_actions = mpull($comment_actions, null, 'getKey');
397
398 $draft_actions = array();
399 $draft_keys = array();
400 if ($versioned_draft) {
401 $draft_actions = $versioned_draft->getProperty('actions', array());
402
403 if (!is_array($draft_actions)) {
404 $draft_actions = array();
405 }
406
407 foreach ($draft_actions as $action) {
408 $type = idx($action, 'type');
409 $comment_action = idx($comment_actions, $type);
410 if (!$comment_action) {
411 continue;
412 }
413
414 $value = idx($action, 'value');
415 $comment_action->setValue($value);
416
417 $draft_keys[] = $type;
418 }
419 }
420
421 foreach ($comment_actions as $key => $comment_action) {
422 $key = $comment_action->getKey();
423 $label = $comment_action->getLabel();
424
425 $action_map[$key] = array(
426 'key' => $key,
427 'label' => $label,
428 'type' => $comment_action->getPHUIXControlType(),
429 'spec' => $comment_action->getPHUIXControlSpecification(),
430 'initialValue' => $comment_action->getInitialValue(),
431 'groupKey' => $comment_action->getGroupKey(),
432 'conflictKey' => $comment_action->getConflictKey(),
433 'auralLabel' => pht('Remove Action: %s', $label),
434 'buttonText' => $comment_action->getSubmitButtonText(),
435 );
436
437 $type_map[$key] = $comment_action;
438 }
439
440 $options = $this->newCommentActionOptions($action_map);
441
442 $action_id = celerity_generate_unique_node_id();
443 $input_id = celerity_generate_unique_node_id();
444 $place_id = celerity_generate_unique_node_id();
445
446 $form->appendChild(
447 phutil_tag(
448 'input',
449 array(
450 'type' => 'hidden',
451 'name' => 'editengine.actions',
452 'id' => $input_id,
453 )));
454
455 $invisi_bar = phutil_tag(
456 'div',
457 array(
458 'id' => $place_id,
459 'class' => 'phui-comment-control-stack',
460 ));
461
462 $action_select = id(new AphrontFormSelectControl())
463 ->addClass('phui-comment-fullwidth-control')
464 ->addClass('phui-comment-action-control')
465 ->setAriaLabel(pht('Comment Action Options'))
466 ->setID($action_id)
467 ->setOptions($options);
468
469 $action_bar = phutil_tag(
470 'div',
471 array(
472 'class' => 'phui-comment-action-bar grouped',
473 ),
474 array(
475 $action_select,
476 ));
477
478 $form->appendChild($action_bar);
479
480 $info_view = $this->getInfoView();
481 if ($info_view) {
482 $form->appendChild($info_view);
483 }
484
485 if ($this->getRequiresMFA()) {
486 $message = pht(
487 'You will be required to provide multi-factor credentials to '.
488 'comment or make changes.');
489
490 $form->appendChild(
491 id(new PHUIInfoView())
492 ->setSeverity(PHUIInfoView::SEVERITY_MFA)
493 ->setErrors(array($message)));
494 }
495
496 $form->appendChild($invisi_bar);
497 $form->addClass('phui-comment-has-actions');
498
499 $timeline = $this->transactionTimeline;
500
501 $view_data = array();
502 if ($timeline) {
503 $view_data = $timeline->getViewData();
504 }
505
506 Javelin::initBehavior(
507 'comment-actions',
508 array(
509 'actionID' => $action_id,
510 'inputID' => $input_id,
511 'formID' => $this->getFormID(),
512 'placeID' => $place_id,
513 'panelID' => $this->getPreviewPanelID(),
514 'timelineID' => $this->getPreviewTimelineID(),
515 'actions' => $action_map,
516 'showPreview' => $this->getShowPreview(),
517 'actionURI' => $this->getAction(),
518 'drafts' => $draft_keys,
519 'defaultButtonText' => $this->getSubmitButtonName(),
520 'viewData' => $view_data,
521 ));
522 }
523
524 $submit_button = id(new AphrontFormSubmitControl())
525 ->addClass('phui-comment-fullwidth-control')
526 ->addClass('phui-comment-submit-control')
527 ->setValue($this->getSubmitButtonName());
528
529 $form
530 ->appendChild($remarkup_control)
531 ->appendChild(
532 id(new AphrontFormSubmitControl())
533 ->addClass('phui-comment-fullwidth-control')
534 ->addClass('phui-comment-submit-control')
535 ->addSigil('submit-transactions')
536 ->setValue($this->getSubmitButtonName()));
537
538 return $form;
539 }
540
541 private function renderPreviewPanel() {
542
543 $preview = id(new PHUITimelineView())
544 ->setID($this->getPreviewTimelineID());
545
546 return phutil_tag(
547 'div',
548 array(
549 'id' => $this->getPreviewPanelID(),
550 'style' => 'display: none',
551 'class' => 'phui-comment-preview-view',
552 ),
553 $preview);
554 }
555
556 private function getPreviewPanelID() {
557 if (!$this->previewPanelID) {
558 $this->previewPanelID = celerity_generate_unique_node_id();
559 }
560 return $this->previewPanelID;
561 }
562
563 private function getPreviewTimelineID() {
564 if (!$this->previewTimelineID) {
565 $this->previewTimelineID = celerity_generate_unique_node_id();
566 }
567 return $this->previewTimelineID;
568 }
569
570 public function setFormID($id) {
571 $this->formID = $id;
572 return $this;
573 }
574
575 private function getFormID() {
576 if (!$this->formID) {
577 $this->formID = celerity_generate_unique_node_id();
578 }
579 return $this->formID;
580 }
581
582 private function getCommentID() {
583 if (!$this->commentID) {
584 $this->commentID = celerity_generate_unique_node_id();
585 }
586 return $this->commentID;
587 }
588
589 private function newCommentActionOptions(array $action_map) {
590 $options = array();
591 $options['+'] = pht('Add Action...');
592
593 // Merge options into groups.
594 $groups = array();
595 foreach ($action_map as $key => $item) {
596 $group_key = $item['groupKey'] ?? '';
597 if (!isset($groups[$group_key])) {
598 $groups[$group_key] = array();
599 }
600 $groups[$group_key][$key] = $item;
601 }
602
603 $group_specs = $this->getCommentActionGroups();
604 $group_labels = mpull($group_specs, 'getLabel', 'getKey');
605
606 // Reorder groups to put them in the same order as the recognized
607 // group definitions.
608 $groups = array_select_keys($groups, array_keys($group_labels)) + $groups;
609
610 // Move options with no group to the end.
611 $default_group = idx($groups, '');
612 if ($default_group) {
613 unset($groups['']);
614 $groups[''] = $default_group;
615 }
616
617 foreach ($groups as $group_key => $group_items) {
618 if (strlen($group_key)) {
619 $group_label = idx($group_labels, $group_key, $group_key);
620 $options[$group_label] = ipull($group_items, 'label');
621 } else {
622 foreach ($group_items as $key => $item) {
623 $options[$key] = $item['label'];
624 }
625 }
626 }
627
628 return $options;
629 }
630
631 private function renderBadgeView() {
632 $user = $this->getUser();
633 $can_use_badges = PhabricatorApplication::isClassInstalledForViewer(
634 PhabricatorBadgesApplication::class,
635 $user);
636 if (!$can_use_badges) {
637 return null;
638 }
639
640 // Pull Badges from UserCache
641 $badges = $user->getRecentBadgeAwards();
642 $badge_view = null;
643 if ($badges) {
644 $badge_list = array();
645 foreach ($badges as $badge) {
646 $badge_view = id(new PHUIBadgeMiniView())
647 ->setIcon($badge['icon'])
648 ->setQuality($badge['quality'])
649 ->setHeader($badge['name'])
650 ->setTipDirection('E')
651 ->setHref('/badges/view/'.$badge['id'].'/');
652
653 $badge_list[] = $badge_view;
654 }
655 $flex = new PHUIBadgeBoxView();
656 $flex->addItems($badge_list);
657 $flex->setCollapsed(true);
658 $badge_view = phutil_tag(
659 'div',
660 array(
661 'class' => 'phui-timeline-badges',
662 ),
663 $flex);
664 }
665
666 return $badge_view;
667 }
668
669}