Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php
index 46877168e7..e11c977436 100644
--- a/src/applications/maniphest/editor/ManiphestEditEngine.php
+++ b/src/applications/maniphest/editor/ManiphestEditEngine.php
@@ -1,549 +1,557 @@
<?php
final class ManiphestEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'maniphest.task';
public function getEngineName() {
return pht('Maniphest Tasks');
}
public function getSummaryHeader() {
return pht('Configure Maniphest Task Forms');
}
public function getSummaryText() {
return pht('Configure how users create and edit tasks.');
}
public function getEngineApplicationClass() {
return PhabricatorManiphestApplication::class;
}
public function isDefaultQuickCreateEngine() {
return true;
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())->addInt(100);
}
protected function newEditableObject() {
return ManiphestTask::initializeNewTask($this->getViewer());
}
protected function newObjectQuery() {
return id(new ManiphestTaskQuery());
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Task');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Task: %s', $object->getTitle());
}
protected function getObjectEditShortText($object) {
return $object->getMonogram();
}
protected function getObjectCreateShortText() {
return pht('Create Task');
}
protected function getObjectName() {
return pht('Task');
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('task/edit/');
}
protected function getCommentViewHeaderText($object) {
return pht('Weigh In');
}
protected function getCommentViewButtonText($object) {
return pht('Set Sail for Adventure');
}
+ public function getCommentFieldPlaceholderText($object) {
+ if ($object->getStatus() === ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) {
+ return pht('This task is closed as a duplicate. '.
+ 'Only comment if you think that this task is not a duplicate.');
+ }
+ return '';
+ }
+
protected function getObjectViewURI($object) {
return '/'.$object->getMonogram();
}
protected function buildCustomEditFields($object) {
$status_map = $this->getTaskStatusMap($object);
$priority_map = $this->getTaskPriorityMap($object);
$alias_map = ManiphestTaskPriority::getTaskPriorityAliasMap();
if ($object->isClosed()) {
$default_status = ManiphestTaskStatus::getDefaultStatus();
} else {
$default_status = ManiphestTaskStatus::getDefaultClosedStatus();
}
if ($object->getOwnerPHID()) {
$owner_value = array($object->getOwnerPHID());
} else {
$owner_value = array($this->getViewer()->getPHID());
}
$column_documentation = pht(<<<EODOCS
You can use this transaction type to create a task into a particular workboard
column, or move an existing task between columns.
The transaction value can be specified in several forms. Some are simpler but
less powerful, while others are more complex and more powerful.
The simplest valid value is a single column PHID:
```lang=json
"PHID-PCOL-1111"
```
This will move the task into that column, or create the task into that column
if you are creating a new task. If the task is currently on the board, it will
be moved out of any exclusive columns. If the task is not currently on the
board, it will be added to the board.
You can also perform multiple moves at the same time by passing a list of
PHIDs:
```lang=json
["PHID-PCOL-2222", "PHID-PCOL-3333"]
```
This is equivalent to performing each move individually.
The most complex and most powerful form uses a dictionary to provide additional
information about the move, including an optional specific position within the
column.
The target column should be identified as `columnPHID`, and you may select a
position by passing either `beforePHIDs` or `afterPHIDs`, specifying the PHIDs
of tasks currently in the column that you want to move this task before or
after:
```lang=json
[
{
"columnPHID": "PHID-PCOL-4444",
"beforePHIDs": ["PHID-TASK-5555"]
}
]
```
When you specify multiple PHIDs, the task will be moved adjacent to the first
valid PHID found in either of the lists. This allows positional moves to
generally work as users expect even if the client view of the board has fallen
out of date and some of the nearby tasks have moved elsewhere.
EODOCS
);
$column_map = $this->getColumnMap($object);
$fields = array(
id(new PhabricatorHandlesEditField())
->setKey('parent')
->setLabel(pht('Parent Task'))
->setDescription(pht('Task to make this a subtask of.'))
->setConduitDescription(pht('Create as a subtask of another task.'))
->setConduitTypeDescription(pht('PHID of the parent task.'))
->setAliases(array('parentPHID'))
->setTransactionType(ManiphestTaskParentTransaction::TRANSACTIONTYPE)
->setHandleParameterType(new ManiphestTaskListHTTPParameterType())
->setSingleValue(null)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false),
id(new PhabricatorColumnsEditField())
->setKey('column')
->setLabel(pht('Column'))
->setDescription(pht('Create a task in a workboard column.'))
->setConduitDescription(
pht('Move a task to one or more workboard columns.'))
->setConduitTypeDescription(
pht('List of columns to move the task to.'))
->setConduitDocumentation($column_documentation)
->setAliases(array('columnPHID', 'columns', 'columnPHIDs'))
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setCommentActionLabel(pht('Move on Workboard'))
->setCommentActionOrder(2000)
->setColumnMap($column_map),
id(new PhabricatorTextEditField())
->setKey('title')
->setLabel(pht('Title'))
->setBulkEditLabel(pht('Set title to'))
->setDescription(pht('Name of the task.'))
->setConduitDescription(pht('Rename the task.'))
->setConduitTypeDescription(pht('New task name.'))
->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setValue($object->getTitle()),
id(new PhabricatorUsersEditField())
->setKey('owner')
->setAliases(array('ownerPHID', 'assign', 'assigned'))
->setLabel(pht('Assigned To'))
->setBulkEditLabel(pht('Assign to'))
->setDescription(pht('User who is responsible for the task.'))
->setConduitDescription(pht('Reassign the task.'))
->setConduitTypeDescription(
pht('New task owner, or `null` to unassign.'))
->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setIsNullable(true)
->setSingleValue($object->getOwnerPHID())
->setCommentActionLabel(pht('Assign / Claim'))
->setCommentActionValue($owner_value),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setBulkEditLabel(pht('Set status to'))
->setDescription(pht('Status of the task.'))
->setConduitDescription(pht('Change the task status.'))
->setConduitTypeDescription(pht('New task status constant.'))
->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getStatus())
->setOptions($status_map)
->setCommentActionLabel(pht('Change Status'))
->setCommentActionValue($default_status),
id(new PhabricatorSelectEditField())
->setKey('priority')
->setLabel(pht('Priority'))
->setBulkEditLabel(pht('Set priority to'))
->setDescription(pht('Priority of the task.'))
->setConduitDescription(pht('Change the priority of the task.'))
->setConduitTypeDescription(pht('New task priority constant.'))
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getPriorityKeyword())
->setOptions($priority_map)
->setOptionAliases($alias_map)
->setCommentActionLabel(pht('Change Priority')),
);
if (ManiphestTaskPoints::getIsEnabled()) {
$points_label = ManiphestTaskPoints::getPointsLabel();
$action_label = ManiphestTaskPoints::getPointsActionLabel();
$fields[] = id(new PhabricatorPointsEditField())
->setKey('points')
->setLabel($points_label)
->setBulkEditLabel($action_label)
->setDescription(pht('Point value of the task.'))
->setConduitDescription(pht('Change the task point value.'))
->setConduitTypeDescription(pht('New task point value.'))
->setTransactionType(ManiphestTaskPointsTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getPoints())
->setCommentActionLabel($action_label);
}
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setBulkEditLabel(pht('Set description to'))
->setDescription(pht('Task description.'))
->setConduitDescription(pht('Update the task description.'))
->setConduitTypeDescription(pht('New task description.'))
->setTransactionType(ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE)
->setValue($object->getDescription())
->setPreviewPanel(
id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview')));
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST;
$src_phid = $object->getPHID();
if ($src_phid) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(
array(
$parent_type,
$subtask_type,
$commit_type,
));
$edge_query->execute();
$parent_phids = $edge_query->getDestinationPHIDs(
array($src_phid),
array($parent_type));
$subtask_phids = $edge_query->getDestinationPHIDs(
array($src_phid),
array($subtask_type));
$commit_phids = $edge_query->getDestinationPHIDs(
array($src_phid),
array($commit_type));
} else {
$parent_phids = array();
$subtask_phids = array();
$commit_phids = array();
}
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('parents')
->setLabel(pht('Parents'))
->setDescription(pht('Parent tasks.'))
->setConduitDescription(pht('Change the parents of this task.'))
->setConduitTypeDescription(pht('List of parent task PHIDs.'))
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $parent_type)
->setValue($parent_phids);
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('subtasks')
->setLabel(pht('Subtasks'))
->setDescription(pht('Subtasks.'))
->setConduitDescription(pht('Change the subtasks of this task.'))
->setConduitTypeDescription(pht('List of subtask PHIDs.'))
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $subtask_type)
->setValue($subtask_phids);
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('commits')
->setLabel(pht('Commits'))
->setDescription(pht('Related commits.'))
->setConduitDescription(pht('Change the related commits for this task.'))
->setConduitTypeDescription(pht('List of related commit PHIDs.'))
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $commit_type)
->setValue($commit_phids);
return $fields;
}
private function getTaskStatusMap(ManiphestTask $task) {
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$current_status = $task->getStatus();
// If the current status is something we don't recognize (maybe an older
// status which was deleted), put a dummy entry in the status map so that
// saving the form doesn't destroy any data by accident.
if (idx($status_map, $current_status) === null) {
$status_map[$current_status] = pht('<Unknown: %s>', $current_status);
}
$dup_status = ManiphestTaskStatus::getDuplicateStatus();
foreach ($status_map as $status => $status_name) {
// Always keep the task's current status.
if ($status == $current_status) {
continue;
}
// Don't allow tasks to be changed directly into "Closed, Duplicate"
// status. Instead, you have to merge them. See T4819.
if ($status == $dup_status) {
unset($status_map[$status]);
continue;
}
// Don't let new or existing tasks be moved into a disabled status.
if (ManiphestTaskStatus::isDisabledStatus($status)) {
unset($status_map[$status]);
continue;
}
}
return $status_map;
}
private function getTaskPriorityMap(ManiphestTask $task) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$priority_keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
$current_priority = $task->getPriority();
$results = array();
foreach ($priority_map as $priority => $priority_name) {
$disabled = ManiphestTaskPriority::isDisabledPriority($priority);
if ($disabled && !($priority == $current_priority)) {
continue;
}
$keyword = head(idx($priority_keywords, $priority));
$results[$keyword] = $priority_name;
}
// If the current value isn't a legitimate one, put it in the dropdown
// anyway so saving the form doesn't cause any side effects.
if (idx($priority_map, $current_priority) === null) {
$results[ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD] = pht(
'<Unknown: %s>',
$current_priority);
}
return $results;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
$response_type = $request->getStr('responseType');
$is_card = ($response_type === 'card');
if ($is_card) {
// Reload the task to make sure we pick up the final task state.
$viewer = $this->getViewer();
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($object->getID()))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
return $this->buildCardResponse($task);
}
return parent::newEditResponse($request, $object, $xactions);
}
private function buildCardResponse(ManiphestTask $task) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $request->getViewer();
$column_phid = $request->getStr('columnPHID');
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withPHIDs(array($column_phid))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
$board_phid = $column->getProjectPHID();
$object_phid = $task->getPHID();
$order = $request->getStr('order');
if ($order) {
$ordering = PhabricatorProjectColumnOrder::getOrderByKey($order);
$ordering = id(clone $ordering)
->setViewer($viewer);
} else {
$ordering = null;
}
$engine = id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->setBoardPHID($board_phid)
->setUpdatePHIDs(array($object_phid))
->setVisiblePHIDs($visible_phids);
if ($ordering) {
$engine->setOrdering($ordering);
}
return $engine->buildResponse();
}
private function getColumnMap(ManiphestTask $task) {
$phid = $task->getPHID();
if (!$phid) {
return array();
}
$board_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if (!$board_phids) {
return array();
}
$viewer = $this->getViewer();
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs($board_phids)
->setObjectPHIDs(array($task->getPHID()))
->executeLayout();
$map = array();
foreach ($board_phids as $board_phid) {
$in_columns = $layout_engine->getObjectColumns($board_phid, $phid);
$in_columns = mpull($in_columns, null, 'getPHID');
$all_columns = $layout_engine->getColumns($board_phid);
if (!$all_columns) {
// This could be a project with no workboard, or a project the viewer
// does not have permission to see.
continue;
}
$board = head($all_columns)->getProject();
$options = array();
foreach ($all_columns as $column) {
$name = $column->getDisplayName();
$is_hidden = $column->isHidden();
$is_selected = isset($in_columns[$column->getPHID()]);
// Don't show hidden, subproject or milestone columns in this map
// unless the object is currently in the column.
$skip_column = ($is_hidden || $column->getProxyPHID());
if ($skip_column) {
if (!$is_selected) {
continue;
}
}
if ($is_hidden) {
$name = pht('(%s)', $name);
}
if ($is_selected) {
$name = pht("\xE2\x97\x8F %s", $name);
} else {
$name = pht("\xE2\x97\x8B %s", $name);
}
$option = array(
'key' => $column->getPHID(),
'label' => $name,
'selected' => (bool)$is_selected,
);
$options[] = $option;
}
$map[] = array(
'label' => $board->getDisplayName(),
'options' => $options,
);
}
$map = isort($map, 'label');
$map = array_values($map);
return $map;
}
}
diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php
index f4695d120c..36a8ec8c81 100644
--- a/src/applications/pholio/controller/PholioMockViewController.php
+++ b/src/applications/pholio/controller/PholioMockViewController.php
@@ -1,237 +1,237 @@
<?php
final class PholioMockViewController extends PholioController {
private $maniphestTaskPHIDs = array();
private function setManiphestTaskPHIDs($maniphest_task_phids) {
$this->maniphestTaskPHIDs = $maniphest_task_phids;
return $this;
}
private function getManiphestTaskPHIDs() {
return $this->maniphestTaskPHIDs;
}
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$image_id = $request->getURIData('imageID');
$mock = id(new PholioMockQuery())
->setViewer($viewer)
->withIDs(array($id))
->needImages(true)
->needInlineComments(true)
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$mock->getPHID(),
PholioMockHasTaskEdgeType::EDGECONST);
$this->setManiphestTaskPHIDs($phids);
$title = $mock->getName();
if ($mock->isClosed()) {
$header_icon = 'fa-ban';
$header_name = pht('Closed');
$header_color = 'dark';
} else {
$header_icon = 'fa-square-o';
$header_name = pht('Open');
$header_color = 'bluegrey';
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setStatus($header_icon, $header_color, $header_name)
->setPolicyObject($mock)
->setHeaderIcon('fa-camera-retro');
$timeline = $this->buildTransactionTimeline(
$mock,
new PholioTransactionQuery());
$timeline->setMock($mock);
$timeline->setQuoteRef($mock->getMonogram());
$curtain = $this->buildCurtainView($mock);
$details = $this->buildDescriptionView($mock);
require_celerity_resource('pholio-css');
require_celerity_resource('pholio-inline-comments-css');
$comment_form_id = celerity_generate_unique_node_id();
$mock_view = id(new PholioMockImagesView())
->setRequestURI($request->getRequestURI())
->setCommentFormID($comment_form_id)
->setUser($viewer)
->setMock($mock)
->setImageID($image_id);
$output = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($mock_view);
$add_comment = $this->buildAddCommentView($mock, $comment_form_id);
$add_comment->setTransactionTimeline($timeline);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($mock->getMonogram(), $mock->getURI());
$crumbs->setBorder(true);
$thumb_grid = id(new PholioMockThumbGridView())
->setUser($viewer)
->setMock($mock);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$output,
$thumb_grid,
$details,
$timeline,
$add_comment,
));
return $this->newPage()
->setTitle(pht('%s %s', $mock->getMonogram(), $title))
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($mock->getPHID()))
->addQuicksandConfig(
array('mockViewConfig' => $mock_view->getBehaviorConfig()))
->appendChild($view);
}
private function buildCurtainView(PholioMock $mock) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($mock);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$mock,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Mock'))
->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($mock->isClosed()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-check')
->setName(pht('Open Mock'))
->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(true));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-ban')
->setName(pht('Close Mock'))
->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(true));
}
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$mock);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
if ($this->getManiphestTaskPHIDs()) {
$curtain->newPanel()
->setHeaderText(pht('Maniphest Tasks'))
->appendChild(
$viewer->renderHandleList($this->getManiphestTaskPHIDs()));
}
$curtain->newPanel()
->setHeaderText(pht('Authored By'))
->appendChild($this->buildAuthorPanel($mock));
return $curtain;
}
private function buildDescriptionView(PholioMock $mock) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$description = $mock->getDescription();
if (strlen($description)) {
$properties->addTextContent(
new PHUIRemarkupView($viewer, $description));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Mock Description'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
return null;
}
private function buildAuthorPanel(PholioMock $mock) {
$viewer = $this->getViewer();
$author_phid = $mock->getAuthorPHID();
$handles = $viewer->loadHandles(array($author_phid));
$author_uri = $handles[$author_phid]->getImageURI();
$author_href = $handles[$author_phid]->getURI();
$author = $viewer->renderHandle($author_phid)->render();
$content = phutil_tag('strong', array(), $author);
$date = phabricator_date($mock->getDateCreated(), $viewer);
$content = pht('%s, %s', $content, $date);
$authored_by = id(new PHUIHeadThingView())
->setImage($author_uri)
->setImageHref($author_href)
->setContent($content);
return $authored_by;
}
private function buildAddCommentView(PholioMock $mock, $comment_form_id) {
$viewer = $this->getViewer();
$draft = PhabricatorDraft::newFromUserAndKey($viewer, $mock->getPHID());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$title = $is_serious
? pht('Add Comment')
: pht('History Beckons');
$form = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
- ->setObjectPHID($mock->getPHID())
+ ->setObject($mock)
->setFormID($comment_form_id)
->setDraft($draft)
->setHeaderText($title)
->setSubmitButtonName(pht('Add Comment'))
->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/'))
->setRequestURI($this->getRequest()->getRequestURI());
return $form;
}
}
diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php
index 2cd555204c..63fe4e3539 100644
--- a/src/applications/ponder/controller/PonderQuestionViewController.php
+++ b/src/applications/ponder/controller/PonderQuestionViewController.php
@@ -1,276 +1,276 @@
<?php
final class PonderQuestionViewController extends PonderController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$question = id(new PonderQuestionQuery())
->setViewer($viewer)
->withIDs(array($id))
->needAnswers(true)
->needProjectPHIDs(true)
->executeOne();
if (!$question) {
return new Aphront404Response();
}
$answers = $this->buildAnswers($question);
$answer_add_panel = id(new PonderAddAnswerView())
->setQuestion($question)
->setUser($viewer)
->setActionURI('/ponder/answer/add/');
$header = new PHUIHeaderView();
$header->setHeader($question->getTitle());
$header->setUser($viewer);
$header->setPolicyObject($question);
$header->setHeaderIcon('fa-university');
if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) {
$header->setStatus('fa-square-o', 'bluegrey', pht('Open'));
} else {
$text = PonderQuestionStatus::getQuestionStatusFullName(
$question->getStatus());
$icon = PonderQuestionStatus::getQuestionStatusIcon(
$question->getStatus());
$header->setStatus($icon, 'dark', $text);
}
$curtain = $this->buildCurtain($question);
$details = $this->buildPropertySectionView($question);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$question,
PhabricatorPolicyCapability::CAN_EDIT);
$content_id = celerity_generate_unique_node_id();
$timeline = $this->buildTransactionTimeline(
$question,
id(new PonderQuestionTransactionQuery())
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)));
$xactions = $timeline->getTransactions();
$add_comment = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
- ->setObjectPHID($question->getPHID())
+ ->setObject($question)
->setShowPreview(false)
->setAction($this->getApplicationURI("/question/comment/{$id}/"))
->setSubmitButtonName(pht('Comment'));
$add_comment = phutil_tag_div(
'ponder-question-add-comment-view', $add_comment);
$comment_view = phutil_tag(
'div',
array(
'id' => $content_id,
'style' => 'display: none;',
),
array(
$timeline,
$add_comment,
));
$footer = id(new PonderFooterView())
->setContentID($content_id)
->setCount(count($xactions));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb('Q'.$id, '/Q'.$id);
$crumbs->setBorder(true);
$subheader = $this->buildSubheaderView($question);
$answer_wiki = null;
if ($question->getAnswerWiki()) {
$wiki = new PHUIRemarkupView($viewer, $question->getAnswerWiki());
$answer_wiki = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeaderText(pht('Answer Summary'))
->appendChild($wiki)
->addClass('ponder-answer-wiki');
}
require_celerity_resource('ponder-view-css');
$ponder_content = phutil_tag(
'div',
array(
'class' => 'ponder-question-content',
),
array(
$answer_wiki,
$footer,
$comment_view,
$answers,
$answer_add_panel,
));
$ponder_view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
->setCurtain($curtain)
->setMainColumn($ponder_content)
->addPropertySection(pht('Details'), $details)
->addClass('ponder-question-view');
$page_objects = array_merge(
array($question->getPHID()),
mpull($question->getAnswers(), 'getPHID'));
return $this->newPage()
->setTitle('Q'.$question->getID().' '.$question->getTitle())
->setCrumbs($crumbs)
->setPageObjectPHIDs($page_objects)
->appendChild($ponder_view);
}
private function buildCurtain(PonderQuestion $question) {
$viewer = $this->getViewer();
$id = $question->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$question,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $this->newCurtainView($question);
if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) {
$name = pht('Close Question');
$icon = 'fa-check-square-o';
} else {
$name = pht('Reopen Question');
$icon = 'fa-square-o';
}
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Question'))
->setHref($this->getApplicationURI("/question/edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName($name)
->setIcon($icon)
->setWorkflow(true)
->setDisabled(!$can_edit)
->setHref($this->getApplicationURI("/question/status/{$id}/")));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-list')
->setName(pht('View History'))
->setHref($this->getApplicationURI("/question/history/{$id}/")));
return $curtain;
}
private function buildSubheaderView(
PonderQuestion $question) {
$viewer = $this->getViewer();
$asker = $viewer->renderHandle($question->getAuthorPHID())->render();
$date = phabricator_datetime($question->getDateCreated(), $viewer);
$asker = phutil_tag('strong', array(), $asker);
$author = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($question->getAuthorPHID()))
->needProfileImage(true)
->executeOne();
$image_uri = $author->getProfileImageURI();
$image_href = '/p/'.$author->getUsername();
$content = pht('Asked by %s on %s.', $asker, $date);
return id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
}
private function buildPropertySectionView(
PonderQuestion $question) {
$viewer = $this->getViewer();
$question_details = PhabricatorMarkupEngine::renderOneObject(
$question,
$question->getMarkupField(),
$viewer);
if (!$question_details) {
$question_details = phutil_tag(
'em',
array(),
pht('No further details for this question.'));
}
$question_details = phutil_tag_div(
'phabricator-remarkup ml', $question_details);
return $question_details;
}
/**
* This is fairly non-standard; building N timelines at once (N = number of
* answers) is tricky business.
*
* TODO - re-factor this to ajax in one answer panel at a time in a more
* standard fashion. This is necessary to scale this application.
*/
private function buildAnswers(PonderQuestion $question) {
$viewer = $this->getViewer();
$answers = $question->getAnswers();
if ($answers) {
$author_phids = mpull($answers, 'getAuthorPHID');
$handles = $this->loadViewerHandles($author_phids);
$view = array();
foreach ($answers as $answer) {
$id = $answer->getID();
$handle = $handles[$answer->getAuthorPHID()];
$timeline = $this->buildTransactionTimeline(
$answer,
id(new PonderAnswerTransactionQuery())
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)));
$xactions = $timeline->getTransactions();
$view[] = id(new PonderAnswerView())
->setUser($viewer)
->setAnswer($answer)
->setTransactions($xactions)
->setTimeline($timeline)
->setHandle($handle);
}
$header = id(new PHUIHeaderView())
->setHeader('Answers');
return id(new PHUIBoxView())
->addClass('ponder-answer-section')
->appendChild($header)
->appendChild($view);
}
return null;
}
}
diff --git a/src/applications/ponder/view/PonderAnswerView.php b/src/applications/ponder/view/PonderAnswerView.php
index 474661381c..a16e7bfbd4 100644
--- a/src/applications/ponder/view/PonderAnswerView.php
+++ b/src/applications/ponder/view/PonderAnswerView.php
@@ -1,177 +1,177 @@
<?php
final class PonderAnswerView extends AphrontTagView {
private $answer;
private $transactions;
private $timeline;
private $handle;
public function setAnswer($answer) {
$this->answer = $answer;
return $this;
}
public function setTransactions($transactions) {
$this->transactions = $transactions;
return $this;
}
public function setTimeline($timeline) {
$this->timeline = $timeline;
return $this;
}
public function setHandle($handle) {
$this->handle = $handle;
return $this;
}
protected function getTagAttributes() {
return array(
'class' => 'ponder-answer-view',
);
}
protected function getTagContent() {
require_celerity_resource('ponder-view-css');
$answer = $this->answer;
$viewer = $this->getUser();
$status = $answer->getStatus();
$author_phid = $answer->getAuthorPHID();
$actions = $this->buildAnswerActions();
$handle = $this->handle;
$id = $answer->getID();
if ($status == PonderAnswerStatus::ANSWER_STATUS_HIDDEN) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$answer,
PhabricatorPolicyCapability::CAN_EDIT);
$message = array();
$message[] = phutil_tag(
'em',
array(),
pht('This answer has been hidden.'));
if ($can_edit) {
$message[] = phutil_tag(
'a',
array(
'href' => "/ponder/answer/edit/{$id}/",
),
pht('Edit Answer'));
}
$message = phutil_implode_html(' ', $message);
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild($message);
}
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Actions'))
->setHref('#')
->setIcon('fa-bars')
->setDropdownMenu($actions);
$header_name = phutil_tag(
'a',
array(
'href' => $handle->getURI(),
),
$handle->getName());
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setEpoch($answer->getDateModified())
->setHeader($header_name)
->addActionLink($action_button)
->setImage($handle->getImageURI())
->setImageURL($handle->getURI());
$content = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
PhabricatorMarkupEngine::renderOneObject(
$answer,
$answer->getMarkupField(),
$viewer));
$anchor = id(new PhabricatorAnchorView())
->setAnchorName("A$id");
$content_id = celerity_generate_unique_node_id();
$footer = id(new PonderFooterView())
->setContentID($content_id)
->setCount(count($this->transactions));
$content = phutil_tag_div(
'ponder-answer-content', array($anchor, $content, $footer));
$answer_view = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::GREY)
->addClass('ponder-answer')
->appendChild($content);
$comment_view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
- ->setObjectPHID($answer->getPHID())
+ ->setObject($answer)
->setShowPreview(false)
->setHeaderText(pht('Answer Comment'))
->setAction("/ponder/answer/comment/{$id}/")
->setSubmitButtonName(pht('Comment'));
$hidden_view = phutil_tag(
'div',
array(
'id' => $content_id,
'style' => 'display: none;',
),
array(
$this->timeline,
$comment_view,
));
return array(
$answer_view,
$hidden_view,
);
}
private function buildAnswerActions() {
$viewer = $this->getUser();
$answer = $this->answer;
$id = $answer->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$answer,
PhabricatorPolicyCapability::CAN_EDIT);
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($answer);
$view->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Answer'))
->setHref("/ponder/answer/edit/{$id}/")
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$view->addAction(
id(new PhabricatorActionView())
->setIcon('fa-list')
->setName(pht('View History'))
->setHref("/ponder/answer/history/{$id}/"));
return $view;
}
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
index 7ed83fb13f..1844a5f03c 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
@@ -1,164 +1,164 @@
<?php
final class PhabricatorSlowvotePollController
extends PhabricatorSlowvoteController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$poll = id(new PhabricatorSlowvoteQuery())
->setViewer($viewer)
->withIDs(array($id))
->needOptions(true)
->needChoices(true)
->needViewerChoices(true)
->executeOne();
if (!$poll) {
return new Aphront404Response();
}
$poll_view = id(new SlowvoteEmbedView())
->setUser($viewer)
->setPoll($poll);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'pollID' => $poll->getID(),
'contentHTML' => $poll_view->render(),
));
}
$status = $poll->getStatusObject();
$header_icon = $status->getHeaderTagIcon();
$header_color = $status->getHeaderTagColor();
$header_name = $status->getName();
$header = id(new PHUIHeaderView())
->setHeader($poll->getQuestion())
->setUser($viewer)
->setStatus($header_icon, $header_color, $header_name)
->setPolicyObject($poll)
->setHeaderIcon('fa-bar-chart');
$curtain = $this->buildCurtain($poll);
$subheader = $this->buildSubheaderView($poll);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($poll->getMonogram());
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$poll,
new PhabricatorSlowvoteTransactionQuery());
$add_comment = $this->buildCommentForm($poll);
$poll_content = array(
$poll_view,
$timeline,
$add_comment,
);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
->setCurtain($curtain)
->setMainColumn($poll_content);
return $this->newPage()
->setTitle(
pht(
'%s %s',
$poll->getMonogram(),
$poll->getQuestion()))
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($poll->getPHID()))
->appendChild($view);
}
private function buildCurtain(PhabricatorSlowvotePoll $poll) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$poll,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $this->newCurtainView($poll);
$is_closed = $poll->isClosed();
$close_poll_text = $is_closed ? pht('Reopen Poll') : pht('Close Poll');
$close_poll_icon = $is_closed ? 'fa-check' : 'fa-ban';
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Poll'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI('edit/'.$poll->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName($close_poll_text)
->setIcon($close_poll_icon)
->setHref($this->getApplicationURI('close/'.$poll->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(true));
return $curtain;
}
private function buildSubheaderView(
PhabricatorSlowvotePoll $poll) {
$viewer = $this->getViewer();
$author = $viewer->renderHandle($poll->getAuthorPHID())->render();
$date = phabricator_datetime($poll->getDateCreated(), $viewer);
$author = phutil_tag('strong', array(), $author);
$person = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($poll->getAuthorPHID()))
->needProfileImage(true)
->executeOne();
$image_uri = $person->getProfileImageURI();
$image_href = '/p/'.$person->getUsername();
$content = pht('Asked by %s on %s.', $author, $date);
return id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
}
private function buildCommentForm(PhabricatorSlowvotePoll $poll) {
$viewer = $this->getRequest()->getUser();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$add_comment_header = $is_serious
? pht('Add Comment')
: pht('Enter Deliberations');
$draft = PhabricatorDraft::newFromUserAndKey($viewer, $poll->getPHID());
return id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
- ->setObjectPHID($poll->getPHID())
+ ->setObject($poll)
->setDraft($draft)
->setHeaderText($add_comment_header)
->setAction($this->getApplicationURI('/comment/'.$poll->getID().'/'))
->setSubmitButtonName(pht('Add Comment'));
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php
index f70e172a99..a696fb6cf7 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngine.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php
@@ -1,2753 +1,2764 @@
<?php
/**
* @task fields Managing Fields
* @task text Display Text
* @task config Edit Engine Configuration
* @task uri Managing URIs
* @task load Creating and Loading Objects
* @task web Responding to Web Requests
* @task edit Responding to Edit Requests
* @task http Responding to HTTP Parameter Requests
* @task conduit Responding to Conduit Requests
*/
abstract class PhabricatorEditEngine
extends Phobject
implements PhabricatorPolicyInterface {
const EDITENGINECONFIG_DEFAULT = 'default';
const SUBTYPE_DEFAULT = 'default';
private $viewer;
private $controller;
private $isCreate;
private $editEngineConfiguration;
private $contextParameters = array();
private $targetObject;
private $page;
private $pages;
private $navigation;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
$this->setViewer($controller->getViewer());
return $this;
}
final public function getController() {
return $this->controller;
}
final public function getEngineKey() {
$key = $this->getPhobjectClassConstant('ENGINECONST', 64);
if (strpos($key, '/') !== false) {
throw new Exception(
pht(
'EditEngine ("%s") contains an invalid key character "/".',
get_class($this)));
}
return $key;
}
final public function getApplication() {
$app_class = $this->getEngineApplicationClass();
return PhabricatorApplication::getByClass($app_class);
}
final public function addContextParameter($key) {
$this->contextParameters[] = $key;
return $this;
}
public function isEngineConfigurable() {
return true;
}
public function isEngineExtensible() {
return true;
}
public function isDefaultQuickCreateEngine() {
return false;
}
public function getDefaultQuickCreateFormKeys() {
$keys = array();
if ($this->isDefaultQuickCreateEngine()) {
$keys[] = self::EDITENGINECONFIG_DEFAULT;
}
foreach ($keys as $idx => $key) {
$keys[$idx] = $this->getEngineKey().'/'.$key;
}
return $keys;
}
public static function splitFullKey($full_key) {
return explode('/', $full_key, 2);
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())
->addString($this->getObjectCreateShortText());
}
/**
* Force the engine to edit a particular object.
*/
public function setTargetObject($target_object) {
$this->targetObject = $target_object;
return $this;
}
public function getTargetObject() {
return $this->targetObject;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
public function getNavigation() {
return $this->navigation;
}
/* -( Managing Fields )---------------------------------------------------- */
abstract public function getEngineApplicationClass();
abstract protected function buildCustomEditFields($object);
public function getFieldsForConfig(
PhabricatorEditEngineConfiguration $config) {
$object = $this->newEditableObject();
$this->editEngineConfiguration = $config;
// This is mostly making sure that we fill in default values.
$this->setIsCreate(true);
return $this->buildEditFields($object);
}
final protected function buildEditFields($object) {
$viewer = $this->getViewer();
$fields = $this->buildCustomEditFields($object);
foreach ($fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
}
$fields = mpull($fields, null, 'getKey');
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
// See T13248. Create a template object to provide to extensions. We
// adjust the template to have the intended subtype, so that extensions
// may change behavior based on the form subtype.
$template_object = clone $object;
if ($this->getIsCreate()) {
if ($this->supportsSubtypes()) {
$config = $this->getEditEngineConfiguration();
$subtype = $config->getSubtype();
$template_object->setSubtype($subtype);
}
}
foreach ($extensions as $extension) {
$extension->setViewer($viewer);
if (!$extension->supportsObject($this, $template_object)) {
continue;
}
$extension_fields = $extension->buildCustomEditFields(
$this,
$template_object);
// TODO: Validate this in more detail with a more tailored error.
assert_instances_of($extension_fields, 'PhabricatorEditField');
foreach ($extension_fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
$group_key = $field->getBulkEditGroupKey();
if ($group_key === null) {
$field->setBulkEditGroupKey('extension');
}
}
$extension_fields = mpull($extension_fields, null, 'getKey');
foreach ($extension_fields as $key => $field) {
$fields[$key] = $field;
}
}
$config = $this->getEditEngineConfiguration();
$fields = $this->willConfigureFields($object, $fields);
$fields = $config->applyConfigurationToFields($this, $object, $fields);
$fields = $this->applyPageToFields($object, $fields);
return $fields;
}
protected function willConfigureFields($object, array $fields) {
return $fields;
}
final public function supportsSubtypes() {
try {
$object = $this->newEditableObject();
} catch (Exception $ex) {
return false;
}
return ($object instanceof PhabricatorEditEngineSubtypeInterface);
}
final public function newSubtypeMap() {
return $this->newEditableObject()->newEditEngineSubtypeMap();
}
/* -( Display Text )------------------------------------------------------- */
/**
* @task text
*/
abstract public function getEngineName();
/**
* @task text
*/
abstract protected function getObjectCreateTitleText($object);
/**
* @task text
*/
protected function getFormHeaderText($object) {
$config = $this->getEditEngineConfiguration();
return $config->getName();
}
/**
* @task text
*/
abstract protected function getObjectEditTitleText($object);
/**
* @task text
*/
abstract protected function getObjectCreateShortText();
/**
* @task text
*/
abstract protected function getObjectName();
/**
* @task text
*/
abstract protected function getObjectEditShortText($object);
/**
* @task text
*/
protected function getObjectCreateButtonText($object) {
return $this->getObjectCreateTitleText($object);
}
/**
* @task text
*/
protected function getObjectEditButtonText($object) {
return pht('Save Changes');
}
/**
* @task text
*/
protected function getCommentViewSeriousHeaderText($object) {
return pht('Take Action');
}
/**
* @task text
*/
protected function getCommentViewSeriousButtonText($object) {
return pht('Submit');
}
/**
* @task text
*/
protected function getCommentViewHeaderText($object) {
return $this->getCommentViewSeriousHeaderText($object);
}
/**
* @task text
*/
protected function getCommentViewButtonText($object) {
return $this->getCommentViewSeriousButtonText($object);
}
/**
* @task text
*/
protected function getPageHeader($object) {
return null;
}
-
+ /**
+ * Set default placeholder plain text in the comment textarea of the engine.
+ * To be overwritten by conditions defined in the child EditEngine class.
+ *
+ * @param object Object in which the comment textarea is displayed.
+ * @return string Placeholder text to display in the comment textarea.
+ * @task text
+ */
+ public function getCommentFieldPlaceholderText($object) {
+ return '';
+ }
/**
* Return a human-readable header describing what this engine is used to do,
* like "Configure Maniphest Task Forms".
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryHeader();
/**
* Return a human-readable summary of what this engine is used to do.
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryText();
/* -( Edit Engine Configuration )------------------------------------------ */
protected function supportsEditEngineConfiguration() {
return true;
}
final protected function getEditEngineConfiguration() {
return $this->editEngineConfiguration;
}
public function newConfigurationQuery() {
return id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($this->getViewer())
->withEngineKeys(array($this->getEngineKey()));
}
private function loadEditEngineConfigurationWithQuery(
PhabricatorEditEngineConfigurationQuery $query,
$sort_method) {
if ($sort_method) {
$results = $query->execute();
$results = msort($results, $sort_method);
$result = head($results);
} else {
$result = $query->executeOne();
}
if (!$result) {
return null;
}
$this->editEngineConfiguration = $result;
return $result;
}
private function loadEditEngineConfigurationWithIdentifier($identifier) {
$query = $this->newConfigurationQuery()
->withIdentifiers(array($identifier));
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultConfiguration() {
$query = $this->newConfigurationQuery()
->withIdentifiers(
array(
self::EDITENGINECONFIG_DEFAULT,
))
->withIgnoreDatabaseConfigurations(true);
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultCreateConfiguration() {
$query = $this->newConfigurationQuery()
->withIsDefault(true)
->withIsDisabled(false);
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getCreateSortKey');
}
public function loadDefaultEditConfiguration($object) {
$query = $this->newConfigurationQuery()
->withIsEdit(true)
->withIsDisabled(false);
// If this object supports subtyping, we edit it with a form of the same
// subtype: so "bug" tasks get edited with "bug" forms.
if ($object instanceof PhabricatorEditEngineSubtypeInterface) {
$query->withSubtypes(
array(
$object->getEditEngineSubtype(),
));
}
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getEditSortKey');
}
final public function getBuiltinEngineConfigurations() {
$configurations = $this->newBuiltinEngineConfigurations();
if (!$configurations) {
throw new Exception(
pht(
'EditEngine ("%s") returned no builtin engine configurations, but '.
'an edit engine must have at least one configuration.',
get_class($this)));
}
assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration');
$has_default = false;
foreach ($configurations as $config) {
if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) {
$has_default = true;
}
}
if (!$has_default) {
$first = head($configurations);
if (!$first->getBuiltinKey()) {
$first
->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT)
->setIsDefault(true)
->setIsEdit(true);
$first_name = $first->getName();
if ($first_name === null || $first_name === '') {
$first->setName($this->getObjectCreateShortText());
}
} else {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but none are marked as default and the first configuration has '.
'a different builtin key already. Mark a builtin as default or '.
'omit the key from the first configuration',
get_class($this)));
}
}
$builtins = array();
foreach ($configurations as $key => $config) {
$builtin_key = $config->getBuiltinKey();
if ($builtin_key === null) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but one (with key "%s") is missing a builtin key. Provide a '.
'builtin key for each configuration (you can omit it from the '.
'first configuration in the list to automatically assign the '.
'default key).',
get_class($this),
$key));
}
if (isset($builtins[$builtin_key])) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but at least two specify the same builtin key ("%s"). Engines '.
'must have unique builtin keys.',
get_class($this),
$builtin_key));
}
$builtins[$builtin_key] = $config;
}
return $builtins;
}
protected function newBuiltinEngineConfigurations() {
return array(
$this->newConfiguration(),
);
}
final protected function newConfiguration() {
return PhabricatorEditEngineConfiguration::initializeNewConfiguration(
$this->getViewer(),
$this);
}
/* -( Managing URIs )------------------------------------------------------ */
/**
* @task uri
*/
abstract protected function getObjectViewURI($object);
/**
* @task uri
*/
protected function getObjectCreateCancelURI($object) {
return $this->getApplication()->getApplicationURI();
}
/**
* @task uri
*/
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('edit/');
}
/**
* @task uri
*/
protected function getObjectEditCancelURI($object) {
return $this->getObjectViewURI($object);
}
/**
* @task uri
*/
public function getCreateURI($form_key) {
try {
$create_uri = $this->getEditURI(null, "form/{$form_key}/");
} catch (Exception $ex) {
$create_uri = null;
}
return $create_uri;
}
/**
* @task uri
*/
public function getEditURI($object = null, $path = null) {
$parts = array();
$parts[] = $this->getEditorURI();
if ($object && $object->getID()) {
$parts[] = $object->getID().'/';
}
if ($path !== null) {
$parts[] = $path;
}
return implode('', $parts);
}
public function getEffectiveObjectViewURI($object) {
if ($this->getIsCreate()) {
return $this->getObjectViewURI($object);
}
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectViewURI($object);
}
public function getEffectiveObjectEditDoneURI($object) {
return $this->getEffectiveObjectViewURI($object);
}
public function getEffectiveObjectEditCancelURI($object) {
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectEditCancelURI($object);
}
/* -( Creating and Loading Objects )--------------------------------------- */
/**
* Initialize a new object for creation.
*
* @return object Newly initialized object.
* @task load
*/
abstract protected function newEditableObject();
/**
* Build an empty query for objects.
*
* @return PhabricatorPolicyAwareQuery Query.
* @task load
*/
abstract protected function newObjectQuery();
/**
* Test if this workflow is creating a new object or editing an existing one.
*
* @return bool True if a new object is being created.
* @task load
*/
final public function getIsCreate() {
return $this->isCreate;
}
/**
* Initialize a new object for object creation via Conduit.
*
* @return object Newly initialized object.
* @param list<wild> Raw transactions.
* @task load
*/
protected function newEditableObjectFromConduit(array $raw_xactions) {
return $this->newEditableObject();
}
/**
* Initialize a new object for documentation creation.
*
* @return object Newly initialized object.
* @task load
*/
protected function newEditableObjectForDocumentation() {
return $this->newEditableObject();
}
/**
* Flag this workflow as a create or edit.
*
* @param bool True if this is a create workflow.
* @return this
* @task load
*/
private function setIsCreate($is_create) {
$this->isCreate = $is_create;
return $this;
}
/**
* Try to load an object by ID, PHID, or monogram. This is done primarily
* to make Conduit a little easier to use.
*
* @param wild ID, PHID, or monogram.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object Corresponding editable object.
* @task load
*/
private function newObjectFromIdentifier(
$identifier,
array $capabilities = array()) {
if (is_int($identifier) || ctype_digit($identifier)) {
$object = $this->newObjectFromID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with ID "%s".',
$identifier));
}
return $object;
}
$type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
if (phid_get_type($identifier) != $type_unknown) {
$object = $this->newObjectFromPHID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with PHID "%s".',
$identifier));
}
return $object;
}
$target = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames(array($identifier))
->executeOne();
if (!$target) {
throw new Exception(
pht(
'Monogram "%s" does not identify a valid object.',
$identifier));
}
$expect = $this->newEditableObject();
$expect_class = get_class($expect);
$target_class = get_class($target);
if ($expect_class !== $target_class) {
throw new Exception(
pht(
'Monogram "%s" identifies an object of the wrong type. Loaded '.
'object has class "%s", but this editor operates on objects of '.
'type "%s".',
$identifier,
$target_class,
$expect_class));
}
// Load the object by PHID using this engine's standard query. This makes
// sure it's really valid, goes through standard policy check logic, and
// picks up any `need...()` clauses we want it to load with.
$object = $this->newObjectFromPHID($target->getPHID(), $capabilities);
if (!$object) {
throw new Exception(
pht(
'Failed to reload object identified by monogram "%s" when '.
'querying by PHID.',
$identifier));
}
return $object;
}
/**
* Load an object by ID.
*
* @param int Object ID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromID($id, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withIDs(array($id));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object by PHID.
*
* @param phid Object PHID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromPHID($phid, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withPHIDs(array($phid));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object given a configured query.
*
* @param PhabricatorPolicyAwareQuery Configured query.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromQuery(
PhabricatorPolicyAwareQuery $query,
array $capabilities = array()) {
$viewer = $this->getViewer();
if (!$capabilities) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
$object = $query
->setViewer($viewer)
->requireCapabilities($capabilities)
->executeOne();
if (!$object) {
return null;
}
return $object;
}
/**
* Verify that an object is appropriate for editing.
*
* @param wild Loaded value.
* @return void
* @task load
*/
private function validateObject($object) {
if (!$object || !is_object($object)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object must '.
'actually be an object, but is of some other type ("%s").',
get_class($this),
gettype($object)));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object (of '.
'class "%s") must implement "%s", but does not.',
get_class($this),
get_class($object),
'PhabricatorApplicationTransactionInterface'));
}
}
/* -( Responding to Web Requests )----------------------------------------- */
final public function buildResponse() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$action = $this->getEditAction();
$capabilities = array();
$use_default = false;
$require_create = true;
switch ($action) {
case 'comment':
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$use_default = true;
break;
case 'parameters':
$use_default = true;
break;
case 'nodefault':
case 'nocreate':
case 'nomanage':
$require_create = false;
break;
default:
break;
}
$object = $this->getTargetObject();
if (!$object) {
$id = $request->getURIData('id');
if ($id) {
$this->setIsCreate(false);
$object = $this->newObjectFromID($id, $capabilities);
if (!$object) {
return new Aphront404Response();
}
} else {
// Make sure the viewer has permission to create new objects of
// this type if we're going to create a new object.
if ($require_create) {
$this->requireCreateCapability();
}
$this->setIsCreate(true);
$object = $this->newEditableObject();
}
} else {
$id = $object->getID();
}
$this->validateObject($object);
if ($use_default) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return new Aphront404Response();
}
} else {
$form_key = $request->getURIData('formKey');
if (phutil_nonempty_string($form_key)) {
$config = $this->loadEditEngineConfigurationWithIdentifier($form_key);
if (!$config) {
return new Aphront404Response();
}
if ($id && !$config->getIsEdit()) {
return $this->buildNotEditFormRespose($object, $config);
}
} else {
if ($id) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return $this->buildNoEditResponse($object);
}
} else {
$config = $this->loadDefaultCreateConfiguration();
if (!$config) {
return $this->buildNoCreateResponse($object);
}
}
}
}
if ($config->getIsDisabled()) {
return $this->buildDisabledFormResponse($object, $config);
}
$page_key = $request->getURIData('pageKey');
if (!phutil_nonempty_string($page_key)) {
$pages = $this->getPages($object);
if ($pages) {
$page_key = head_key($pages);
}
}
if (phutil_nonempty_string($page_key)) {
$page = $this->selectPage($object, $page_key);
if (!$page) {
return new Aphront404Response();
}
}
switch ($action) {
case 'parameters':
return $this->buildParametersResponse($object);
case 'nodefault':
return $this->buildNoDefaultResponse($object);
case 'nocreate':
return $this->buildNoCreateResponse($object);
case 'nomanage':
return $this->buildNoManageResponse($object);
case 'comment':
return $this->buildCommentResponse($object);
default:
return $this->buildEditResponse($object);
}
}
private function buildCrumbs($object, $final = false) {
$controller = $this->getController();
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
if ($this->getIsCreate()) {
$create_text = $this->getObjectCreateShortText();
if ($final) {
$crumbs->addTextCrumb($create_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($create_text, $edit_uri);
}
} else {
$crumbs->addTextCrumb(
$this->getObjectEditShortText($object),
$this->getEffectiveObjectViewURI($object));
$edit_text = pht('Edit');
if ($final) {
$crumbs->addTextCrumb($edit_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($edit_text, $edit_uri);
}
}
return $crumbs;
}
private function buildEditResponse($object) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$template = $object->getApplicationTransactionTemplate();
$page_state = new PhabricatorEditEnginePageState();
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
$page_state->setIsCreate(true);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
$config = $this->getEditEngineConfiguration()
->attachEngine($this);
// NOTE: Don't prompt users to override locks when creating objects,
// even if the default settings would create a locked object.
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact &&
!$this->getIsCreate() &&
!$request->getBool('editEngine') &&
!$request->getBool('overrideLock')) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->getController()
->newDialog()
->addHiddenInput('overrideLock', true)
->setDisableWorkflowOnSubmit(true)
->addCancelButton($cancel_uri);
return $lock->willPromptUserForLockOverrideWithDialog($dialog);
}
$validation_exception = null;
if ($request->isFormOrHisecPost() && $request->getBool('editEngine')) {
$page_state->setIsSubmit(true);
$submit_fields = $fields;
foreach ($submit_fields as $key => $field) {
if (!$field->shouldGenerateTransactionsFromSubmit()) {
unset($submit_fields[$key]);
continue;
}
}
// Before we read the submitted values, store a copy of what we would
// use if the form was empty so we can figure out which transactions are
// just setting things to their default values for the current form.
$defaults = array();
foreach ($submit_fields as $key => $field) {
$defaults[$key] = $field->getValueForTransaction();
}
foreach ($submit_fields as $key => $field) {
$field->setIsSubmittedForm(true);
if (!$field->shouldReadValueFromSubmit()) {
continue;
}
$field->readValueFromSubmit($request);
}
$xactions = array();
if ($this->getIsCreate()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
if ($this->supportsSubtypes()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE)
->setNewValue($config->getSubtype());
}
}
foreach ($submit_fields as $key => $field) {
$field_value = $field->getValueForTransaction();
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field_value,
));
foreach ($type_xactions as $type_xaction) {
$default = $defaults[$key];
if ($default === $field->getValueForTransaction()) {
$type_xaction->setIsDefaultTransaction(true);
}
$xactions[] = $type_xaction;
}
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setCancelURI($cancel_uri)
->setContinueOnNoEffect(true);
try {
$xactions = $this->willApplyTransactions($object, $xactions);
$editor->applyTransactions($object, $xactions);
$this->didApplyTransactions($object, $xactions);
return $this->newEditResponse($request, $object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
foreach ($fields as $field) {
$message = $this->getValidationExceptionShortMessage($ex, $field);
if ($message === null) {
continue;
}
$field->setControlError($message);
}
$page_state->setIsError(true);
}
} else {
if ($this->getIsCreate()) {
$template = $request->getStr('template');
if (phutil_nonempty_string($template)) {
$template_object = $this->newObjectFromIdentifier(
$template,
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
if (!$template_object) {
return new Aphront404Response();
}
} else {
$template_object = null;
}
if ($template_object) {
$copy_fields = $this->buildEditFields($template_object);
$copy_fields = mpull($copy_fields, null, 'getKey');
foreach ($copy_fields as $copy_key => $copy_field) {
if (!$copy_field->getIsCopyable()) {
unset($copy_fields[$copy_key]);
}
}
} else {
$copy_fields = array();
}
foreach ($fields as $field) {
if (!$field->shouldReadValueFromRequest()) {
continue;
}
$field_key = $field->getKey();
if (isset($copy_fields[$field_key])) {
$field->readValueFromField($copy_fields[$field_key]);
}
$field->readValueFromRequest($request);
}
}
}
$action_button = $this->buildEditFormActionButton($object);
if ($this->getIsCreate()) {
$header_text = $this->getFormHeaderText($object);
} else {
$header_text = $this->getObjectEditTitleText($object);
}
$show_preview = !$request->isAjax();
if ($show_preview) {
$previews = array();
foreach ($fields as $field) {
$preview = $field->getPreviewPanel();
if (!$preview) {
continue;
}
$control_id = $field->getControlID();
$preview
->setControlID($control_id)
->setPreviewURI('/transactions/remarkuppreview/');
$previews[] = $preview;
}
} else {
$previews = array();
}
$form = $this->buildEditForm($object, $fields);
$crumbs = $this->buildCrumbs($object, $final = true);
$crumbs->setBorder(true);
if ($request->isAjax()) {
return $this->getController()
->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_text)
->setValidationException($validation_exception)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
$box_header = id(new PHUIHeaderView())
->setHeader($header_text);
if ($action_button) {
$box_header->addActionLink($action_button);
}
$request_submit_key = $request->getSubmitKey();
$engine_submit_key = $this->getEditEngineSubmitKey();
if ($request_submit_key === $engine_submit_key) {
$page_state->setIsSubmit(true);
$page_state->setIsSave(true);
}
$head = $this->newEditFormHeadContent($page_state);
$tail = $this->newEditFormTailContent($page_state);
$box = id(new PHUIObjectBoxView())
->setUser($viewer)
->setHeader($box_header)
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->appendChild($form);
$content = array(
$head,
$box,
$previews,
$tail,
);
$view = new PHUITwoColumnView();
$page_header = $this->getPageHeader($object);
if ($page_header) {
$view->setHeader($page_header);
}
$view->setFooter($content);
$page = $controller->newPage()
->setTitle($header_text)
->setCrumbs($crumbs)
->appendChild($view);
$navigation = $this->getNavigation();
if ($navigation) {
$page->setNavigation($navigation);
}
return $page;
}
protected function newEditFormHeadContent(
PhabricatorEditEnginePageState $state) {
return null;
}
protected function newEditFormTailContent(
PhabricatorEditEnginePageState $state) {
return null;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
$submit_cookie = PhabricatorCookies::COOKIE_SUBMIT;
$submit_key = $this->getEditEngineSubmitKey();
$request->setTemporaryCookie($submit_cookie, $submit_key);
return id(new AphrontRedirectResponse())
->setURI($this->getEffectiveObjectEditDoneURI($object));
}
private function getEditEngineSubmitKey() {
return 'edit-engine/'.$this->getEngineKey();
}
private function buildEditForm($object, array $fields) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->willBuildEditForm($object, $fields);
$request_path = $request->getPath();
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($request_path)
->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($param));
}
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
if ($requires_mfa) {
$message = pht(
'You will be required to provide multi-factor credentials to make '.
'changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
// TODO: This should also set workflow on the form, so the user doesn't
// lose any form data if they "Cancel". However, Maniphest currently
// overrides "newEditResponse()" if the request is Ajax and returns a
// bag of view data. This can reasonably be cleaned up when workboards
// get their next iteration.
}
foreach ($fields as $field) {
if (!$field->getIsFormField()) {
continue;
}
$field->appendToForm($form);
}
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
if (!$request->isAjax()) {
$buttons = id(new AphrontFormSubmitControl())
->setValue($submit_button);
if ($cancel_uri) {
$buttons->addCancelButton($cancel_uri);
}
$form->appendControl($buttons);
}
return $form;
}
protected function willBuildEditForm($object, array $fields) {
return $fields;
}
private function buildEditFormActionButton($object) {
if (!$this->isEngineConfigurable()) {
return null;
}
$viewer = $this->getViewer();
$action_view = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($this->buildEditFormActions($object) as $action) {
$action_view->addAction($action);
}
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Configure Form'))
->setHref('#')
->setIcon('fa-gear')
->setDropdownMenu($action_view);
return $action_button;
}
private function buildEditFormActions($object) {
$actions = array();
if ($this->supportsEditEngineConfiguration()) {
$engine_key = $this->getEngineKey();
$config = $this->getEditEngineConfiguration();
$can_manage = PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$config,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_manage) {
$manage_uri = $config->getURI();
} else {
$manage_uri = $this->getEditURI(null, 'nomanage/');
}
$view_uri = "/transactions/editengine/{$engine_key}/";
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Configuration'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('View Form Configurations'))
->setIcon('fa-list-ul')
->setHref($view_uri);
$actions[] = id(new PhabricatorActionView())
->setName(pht('Edit Form Configuration'))
->setIcon('fa-pencil')
->setHref($manage_uri)
->setDisabled(!$can_manage)
->setWorkflow(!$can_manage);
}
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Documentation'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('Using HTTP Parameters'))
->setIcon('fa-book')
->setHref($this->getEditURI($object, 'parameters/'));
$doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms');
$actions[] = id(new PhabricatorActionView())
->setName(pht('User Guide: Customizing Forms'))
->setIcon('fa-book')
->setHref($doc_href);
return $actions;
}
public function newNUXButton($text) {
$specs = $this->newCreateActionSpecifications(array());
$head = head($specs);
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($head['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow'])
->setColor(PHUIButtonView::GREEN);
}
final public function addActionToCrumbs(
PHUICrumbsView $crumbs,
array $parameters = array()) {
$viewer = $this->getViewer();
$specs = $this->newCreateActionSpecifications($parameters);
$head = head($specs);
$menu_uri = $head['uri'];
$dropdown = null;
if (count($specs) > 1) {
$menu_icon = 'fa-caret-square-o-down';
$menu_name = $this->getObjectCreateShortText();
$workflow = false;
$disabled = false;
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($specs as $spec) {
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($spec['name'])
->setIcon($spec['icon'])
->setHref($spec['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow']));
}
} else {
$menu_icon = $head['icon'];
$menu_name = $head['name'];
$workflow = $head['workflow'];
$disabled = $head['disabled'];
}
$action = id(new PHUIListItemView())
->setName($menu_name)
->setHref($menu_uri)
->setIcon($menu_icon)
->setWorkflow($workflow)
->setDisabled($disabled);
if ($dropdown) {
$action->setDropdownMenu($dropdown);
}
$crumbs->addAction($action);
}
/**
* Build a raw description of available "Create New Object" UI options so
* other methods can build menus or buttons.
*/
public function newCreateActionSpecifications(array $parameters) {
$viewer = $this->getViewer();
$can_create = $this->hasCreateCapability();
if ($can_create) {
$configs = $this->loadUsableConfigurationsForCreate();
} else {
$configs = array();
}
$disabled = false;
$workflow = false;
$menu_icon = 'fa-plus-square';
$specs = array();
if (!$configs) {
if ($viewer->isLoggedIn()) {
$disabled = true;
} else {
// If the viewer isn't logged in, assume they'll get hit with a login
// dialog and are likely able to create objects after they log in.
$disabled = false;
}
$workflow = true;
if ($can_create) {
$create_uri = $this->getEditURI(null, 'nodefault/');
} else {
$create_uri = $this->getEditURI(null, 'nocreate/');
}
$specs[] = array(
'name' => $this->getObjectCreateShortText(),
'uri' => $create_uri,
'icon' => $menu_icon,
'disabled' => $disabled,
'workflow' => $workflow,
);
} else {
foreach ($configs as $config) {
$config_uri = $config->getCreateURI();
if ($parameters) {
$config_uri = (string)new PhutilURI($config_uri, $parameters);
}
$specs[] = array(
'name' => $config->getDisplayName(),
'uri' => $config_uri,
'icon' => 'fa-plus',
'disabled' => false,
'workflow' => false,
);
}
}
return $specs;
}
final public function buildEditEngineCommentView($object) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
// TODO: This just nukes the entire comment form if you don't have access
// to any edit forms. We might want to tailor this UX a bit.
return id(new PhabricatorApplicationTransactionCommentView())
->setNoPermission(true);
}
$viewer = $this->getViewer();
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return id(new PhabricatorApplicationTransactionCommentView())
->setEditEngineLock($lock);
}
$object_phid = $object->getPHID();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$header_text = $this->getCommentViewSeriousHeaderText($object);
$button_text = $this->getCommentViewSeriousButtonText($object);
} else {
$header_text = $this->getCommentViewHeaderText($object);
$button_text = $this->getCommentViewButtonText($object);
}
$comment_uri = $this->getEditURI($object, 'comment/');
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
$view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
- ->setObjectPHID($object_phid)
->setHeaderText($header_text)
->setAction($comment_uri)
->setRequiresMFA($requires_mfa)
+ ->setObject($object)
+ ->setEditEngine($this)
->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft(
$object_phid,
$viewer->getPHID());
if ($draft) {
$view->setVersionedDraft($draft);
}
$view->setCurrentVersion($this->loadDraftVersion($object));
$fields = $this->buildEditFields($object);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$comment_actions = array();
foreach ($fields as $field) {
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
continue;
}
}
$comment_action = $field->getCommentAction();
if (!$comment_action) {
continue;
}
$key = $comment_action->getKey();
// TODO: Validate these better.
$comment_actions[$key] = $comment_action;
}
$comment_actions = msortv($comment_actions, 'getSortVector');
$view->setCommentActions($comment_actions);
$comment_groups = $this->newCommentActionGroups();
$view->setCommentActionGroups($comment_groups);
return $view;
}
protected function loadDraftVersion($object) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
return null;
}
$template = $object->getApplicationTransactionTemplate();
$conn_r = $template->establishConnection('r');
// Find the most recent transaction the user has written. We'll use this
// as a version number to make sure that out-of-date drafts get discarded.
$result = queryfx_one(
$conn_r,
'SELECT id AS version FROM %T
WHERE objectPHID = %s AND authorPHID = %s
ORDER BY id DESC LIMIT 1',
$template->getTableName(),
$object->getPHID(),
$viewer->getPHID());
if ($result) {
return (int)$result['version'];
} else {
return null;
}
}
/* -( Responding to HTTP Parameter Requests )------------------------------ */
/**
* Respond to a request for documentation on HTTP parameters.
*
* @param object Editable object.
* @return AphrontResponse Response object.
* @task http
*/
private function buildParametersResponse($object) {
$controller = $this->getController();
$viewer = $this->getViewer();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$crumbs = $this->buildCrumbs($object);
$crumbs->addTextCrumb(pht('HTTP Parameters'));
$crumbs->setBorder(true);
$header_text = pht(
'HTTP Parameters: %s',
$this->getObjectCreateShortText());
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView())
->setUser($viewer)
->setFields($fields);
$document = id(new PHUIDocumentView())
->setUser($viewer)
->setHeader($header)
->appendChild($help_view);
return $controller->newPage()
->setTitle(pht('HTTP Parameters'))
->setCrumbs($crumbs)
->appendChild($document);
}
private function buildError($object, $title, $body) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$dialog = $this->getController()
->newDialog()
->addCancelButton($cancel_uri);
if ($title !== null) {
$dialog->setTitle($title);
}
if ($body !== null) {
$dialog->appendParagraph($body);
}
return $dialog;
}
private function buildNoDefaultResponse($object) {
return $this->buildError(
$object,
pht('No Default Create Forms'),
pht(
'This application is not configured with any forms for creating '.
'objects that are visible to you and enabled.'));
}
private function buildNoCreateResponse($object) {
return $this->buildError(
$object,
pht('No Create Permission'),
pht('You do not have permission to create these objects.'));
}
private function buildNoManageResponse($object) {
return $this->buildError(
$object,
pht('No Manage Permission'),
pht(
'You do not have permission to configure forms for this '.
'application.'));
}
private function buildNoEditResponse($object) {
return $this->buildError(
$object,
pht('No Edit Forms'),
pht(
'You do not have access to any forms which are enabled and marked '.
'as edit forms.'));
}
private function buildNotEditFormRespose($object, $config) {
return $this->buildError(
$object,
pht('Not an Edit Form'),
pht(
'This form ("%s") is not marked as an edit form, so '.
'it can not be used to edit objects.',
$config->getName()));
}
private function buildDisabledFormResponse($object, $config) {
return $this->buildError(
$object,
pht('Form Disabled'),
pht(
'This form ("%s") has been disabled, so it can not be used.',
$config->getName()));
}
private function buildLockedObjectResponse($object) {
$dialog = $this->buildError($object, null, null);
$viewer = $this->getViewer();
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return $lock->willBlockUserInteractionWithDialog($dialog);
}
private function buildCommentResponse($object) {
$viewer = $this->getViewer();
if ($this->getIsCreate()) {
return new Aphront404Response();
}
$controller = $this->getController();
$request = $controller->getRequest();
// NOTE: We handle hisec inside the transaction editor with "Sign With MFA"
// comment actions.
if (!$request->isFormOrHisecPost()) {
return new Aphront400Response();
}
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
return $this->buildLockedObjectResponse($object);
}
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return new Aphront404Response();
}
$fields = $this->buildEditFields($object);
$is_preview = $request->isPreviewRequest();
$view_uri = $this->getEffectiveObjectViewURI($object);
$template = $object->getApplicationTransactionTemplate();
$comment_template = $template->getApplicationTransactionCommentObject();
$comment_text = $request->getStr('comment');
$comment_metadata = $request->getStr('comment_metadata');
if (strlen($comment_metadata)) {
$comment_metadata = phutil_json_decode($comment_metadata);
}
$actions = $request->getStr('editengine.actions');
if ($actions) {
$actions = phutil_json_decode($actions);
}
if ($is_preview) {
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$request_version = $request->getInt($version_key);
$current_version = $this->loadDraftVersion($object);
if ($request_version >= $current_version) {
$draft = PhabricatorVersionedDraft::loadOrCreateDraft(
$object->getPHID(),
$viewer->getPHID(),
$current_version);
$draft
->setProperty('comment', $comment_text)
->setProperty('metadata', $comment_metadata)
->setProperty('actions', $actions)
->save();
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft($draft)
->synchronize();
}
}
}
$xactions = array();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if ($actions) {
$action_map = array();
foreach ($actions as $action) {
$type = idx($action, 'type');
if (!$type) {
continue;
}
if (empty($fields[$type])) {
continue;
}
$action_map[$type] = $action;
}
foreach ($action_map as $type => $action) {
$field = $fields[$type];
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
// If you don't have edit permission on the object, you're limited in
// which actions you can take via the comment form. Most actions
// need edit permission, but some actions (like "Accept Revision")
// can be applied by anyone with view permission.
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
// We know the user doesn't have the capability, so this will
// raise a policy exception.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
if (array_key_exists('initialValue', $action)) {
$field->setInitialValue($action['initialValue']);
}
$field->readValueFromComment(idx($action, 'value'));
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field->getValueForTransaction(),
));
foreach ($type_xactions as $type_xaction) {
$xactions[] = $type_xaction;
}
}
}
$auto_xactions = $this->newAutomaticCommentTransactions($object);
foreach ($auto_xactions as $xaction) {
$xactions[] = $xaction;
}
if (strlen($comment_text) || !$xactions) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->setMetadataValue('remarkup.control', $comment_metadata)
->attachComment(
id(clone $comment_template)
->setContent($comment_text));
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->setCancelURI($view_uri)
->setRaiseWarnings(!$request->getBool('editEngine.warnings'))
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
return id(new PhabricatorApplicationTransactionValidationResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionWarningException $ex) {
return id(new PhabricatorApplicationTransactionWarningResponse())
->setObject($object)
->setCancelURI($view_uri)
->setException($ex);
}
if (!$is_preview) {
PhabricatorVersionedDraft::purgeDrafts(
$object->getPHID(),
$viewer->getPHID());
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft(null)
->synchronize();
}
}
if ($request->isAjax() && $is_preview) {
$preview_content = $this->newCommentPreviewContent($object, $xactions);
$raw_view_data = $request->getStr('viewData');
try {
$view_data = phutil_json_decode($raw_view_data);
} catch (Exception $ex) {
$view_data = array();
}
return id(new PhabricatorApplicationTransactionResponse())
->setObject($object)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview)
->setViewData($view_data)
->setPreviewContent($preview_content);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
protected function newDraftEngine($object) {
$viewer = $this->getViewer();
if ($object instanceof PhabricatorDraftInterface) {
$engine = $object->newDraftEngine();
} else {
$engine = new PhabricatorBuiltinDraftEngine();
}
return $engine
->setObject($object)
->setViewer($viewer);
}
/* -( Conduit )------------------------------------------------------------ */
/**
* Respond to a Conduit edit request.
*
* This method accepts a list of transactions to apply to an object, and
* either edits an existing object or creates a new one.
*
* @task conduit
*/
final public function buildConduitResponse(ConduitAPIRequest $request) {
$viewer = $this->getViewer();
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht(
'Unable to load configuration for this EditEngine ("%s").',
get_class($this)));
}
$raw_xactions = $this->getRawConduitTransactions($request);
$identifier = $request->getValue('objectIdentifier');
if ($identifier) {
$this->setIsCreate(false);
// After T13186, each transaction can individually weaken or replace the
// capabilities required to apply it, so we no longer need CAN_EDIT to
// attempt to apply transactions to objects. In practice, almost all
// transactions require CAN_EDIT so we won't get very far if we don't
// have it.
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$object = $this->newObjectFromIdentifier(
$identifier,
$capabilities);
} else {
$this->requireCreateCapability();
$this->setIsCreate(true);
$object = $this->newEditableObjectFromConduit($raw_xactions);
}
$this->validateObject($object);
$fields = $this->buildEditFields($object);
$types = $this->getConduitEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$xactions = $this->getConduitTransactions(
$request,
$raw_xactions,
$types,
$template);
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true);
if (!$this->getIsCreate()) {
$editor->setContinueOnMissingFields(true);
}
$xactions = $editor->applyTransactions($object, $xactions);
$xactions_struct = array();
foreach ($xactions as $xaction) {
$xactions_struct[] = array(
'phid' => $xaction->getPHID(),
);
}
return array(
'object' => array(
'id' => (int)$object->getID(),
'phid' => $object->getPHID(),
),
'transactions' => $xactions_struct,
);
}
private function getRawConduitTransactions(ConduitAPIRequest $request) {
$transactions_key = 'transactions';
$xactions = $request->getValue($transactions_key);
if (!is_array($xactions)) {
throw new Exception(
pht(
'Parameter "%s" is not a list of transactions.',
$transactions_key));
}
foreach ($xactions as $key => $xaction) {
if (!is_array($xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is not a dictionary.',
$transactions_key,
$key));
}
if (!array_key_exists('type', $xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is missing a "type" field. Each '.
'transaction must have a type field.',
$transactions_key,
$key));
}
if (!array_key_exists('value', $xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is missing a "value" field. Each '.
'transaction must have a value field.',
$transactions_key,
$key));
}
}
return $xactions;
}
/**
* Generate transactions which can be applied from edit actions in a Conduit
* request.
*
* @param ConduitAPIRequest The request.
* @param list<wild> Raw conduit transactions.
* @param list<PhabricatorEditType> Supported edit types.
* @param PhabricatorApplicationTransaction Template transaction.
* @return list<PhabricatorApplicationTransaction> Generated transactions.
* @task conduit
*/
private function getConduitTransactions(
ConduitAPIRequest $request,
array $xactions,
array $types,
PhabricatorApplicationTransaction $template) {
$viewer = $request->getUser();
$results = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction['type'];
if (empty($types[$type])) {
throw new Exception(
pht(
'Transaction with key "%s" has invalid type "%s". This type is '.
'not recognized. Valid types are: %s.',
$key,
$type,
implode(', ', array_keys($types))));
}
}
if ($this->getIsCreate()) {
$results[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
}
$is_strict = $request->getIsStrictlyTyped();
foreach ($xactions as $xaction) {
$type = $types[$xaction['type']];
// Let the parameter type interpret the value. This allows you to
// use usernames in list<user> fields, for example.
$parameter_type = $type->getConduitParameterType();
$parameter_type->setViewer($viewer);
try {
$value = $xaction['value'];
$value = $parameter_type->getValue($xaction, 'value', $is_strict);
$value = $type->getTransactionValueFromConduit($value);
$xaction['value'] = $value;
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Exception when processing transaction of type "%s": %s',
$xaction['type'],
$ex->getMessage()),
$ex);
}
$type_xactions = $type->generateTransactions(
clone $template,
$xaction);
foreach ($type_xactions as $type_xaction) {
$results[] = $type_xaction;
}
}
return $results;
}
/**
* @return map<string, PhabricatorEditType>
* @task conduit
*/
private function getConduitEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getConduitEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
public function getConduitEditTypes() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return array();
}
$object = $this->newEditableObjectForDocumentation();
$fields = $this->buildEditFields($object);
return $this->getConduitEditTypesFromFields($fields);
}
final public static function getAllEditEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getEngineKey')
->execute();
}
final public static function getByKey(PhabricatorUser $viewer, $key) {
return id(new PhabricatorEditEngineQuery())
->setViewer($viewer)
->withEngineKeys(array($key))
->executeOne();
}
public function getIcon() {
$application = $this->getApplication();
return $application->getIcon();
}
private function loadUsableConfigurationsForCreate() {
$viewer = $this->getViewer();
$configs = id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($viewer)
->withEngineKeys(array($this->getEngineKey()))
->withIsDefault(true)
->withIsDisabled(false)
->execute();
$configs = msort($configs, 'getCreateSortKey');
// Attach this specific engine to configurations we load so they can access
// any runtime configuration. For example, this allows us to generate the
// correct "Create Form" buttons when editing forms, see T12301.
foreach ($configs as $config) {
$config->attachEngine($this);
}
return $configs;
}
protected function getValidationExceptionShortMessage(
PhabricatorApplicationTransactionValidationException $ex,
PhabricatorEditField $field) {
$xaction_type = $field->getTransactionType();
if ($xaction_type === null) {
return null;
}
return $ex->getShortMessage($xaction_type);
}
protected function getCreateNewObjectPolicy() {
return PhabricatorPolicies::POLICY_USER;
}
private function requireCreateCapability() {
PhabricatorPolicyFilter::requireCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
private function hasCreateCapability() {
return PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
public function isCommentAction() {
return ($this->getEditAction() == 'comment');
}
public function getEditAction() {
$controller = $this->getController();
$request = $controller->getRequest();
return $request->getURIData('editAction');
}
protected function newCommentActionGroups() {
return array();
}
protected function newAutomaticCommentTransactions($object) {
return array();
}
protected function newCommentPreviewContent($object, array $xactions) {
return null;
}
/* -( Form Pages )--------------------------------------------------------- */
public function getSelectedPage() {
return $this->page;
}
private function selectPage($object, $page_key) {
$pages = $this->getPages($object);
if (empty($pages[$page_key])) {
return null;
}
$this->page = $pages[$page_key];
return $this->page;
}
protected function newPages($object) {
return array();
}
protected function getPages($object) {
if ($this->pages === null) {
$pages = $this->newPages($object);
assert_instances_of($pages, 'PhabricatorEditPage');
$pages = mpull($pages, null, 'getKey');
$this->pages = $pages;
}
return $this->pages;
}
private function applyPageToFields($object, array $fields) {
$pages = $this->getPages($object);
if (!$pages) {
return $fields;
}
if (!$this->getSelectedPage()) {
return $fields;
}
$page_picks = array();
$default_key = head($pages)->getKey();
foreach ($pages as $page_key => $page) {
foreach ($page->getFieldKeys() as $field_key) {
$page_picks[$field_key] = $page_key;
}
if ($page->getIsDefault()) {
$default_key = $page_key;
}
}
$page_map = array_fill_keys(array_keys($pages), array());
foreach ($fields as $field_key => $field) {
if (isset($page_picks[$field_key])) {
$page_map[$page_picks[$field_key]][$field_key] = $field;
continue;
}
// TODO: Maybe let the field pick a page to associate itself with so
// extensions can force themselves onto a particular page?
$page_map[$default_key][$field_key] = $field;
}
$page = $this->getSelectedPage();
if (!$page) {
$page = head($pages);
}
$selected_key = $page->getKey();
return $page_map[$selected_key];
}
protected function willApplyTransactions($object, array $xactions) {
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
return;
}
/* -( Bulk Edits )--------------------------------------------------------- */
final public function newBulkEditGroupMap() {
$groups = $this->newBulkEditGroups();
$map = array();
foreach ($groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Two bulk edit groups have the same key ("%s"). Each bulk edit '.
'group must have a unique key.',
$key));
}
$map[$key] = $group;
}
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
foreach ($extensions as $extension) {
$extension_groups = $extension->newBulkEditGroups($this);
foreach ($extension_groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Extension "%s" defines a bulk edit group with the same key '.
'("%s") as the main editor or another extension. Each bulk '.
'edit group must have a unique key.',
get_class($extension),
$key));
}
$map[$key] = $group;
}
}
return $map;
}
protected function newBulkEditGroups() {
return array(
id(new PhabricatorBulkEditGroup())
->setKey('default')
->setLabel(pht('Primary Fields')),
id(new PhabricatorBulkEditGroup())
->setKey('extension')
->setLabel(pht('Support Applications')),
);
}
final public function newBulkEditMap() {
$viewer = $this->getViewer();
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$groups = $this->newBulkEditGroupMap();
$edit_types = $this->getBulkEditTypesFromFields($fields);
$map = array();
foreach ($edit_types as $key => $type) {
$bulk_type = $type->getBulkParameterType();
if ($bulk_type === null) {
continue;
}
$bulk_type->setViewer($viewer);
$bulk_label = $type->getBulkEditLabel();
if ($bulk_label === null) {
continue;
}
$group_key = $type->getBulkEditGroupKey();
if (!$group_key) {
$group_key = 'default';
}
if (!isset($groups[$group_key])) {
throw new Exception(
pht(
'Field "%s" has a bulk edit group key ("%s") with no '.
'corresponding bulk edit group.',
$key,
$group_key));
}
$map[] = array(
'label' => $bulk_label,
'xaction' => $key,
'group' => $group_key,
'control' => array(
'type' => $bulk_type->getPHUIXControlType(),
'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
),
);
}
return $map;
}
final public function newRawBulkTransactions(array $xactions) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$edit_types = $this->getBulkEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$raw_xactions = array();
foreach ($xactions as $key => $xaction) {
PhutilTypeSpec::checkMap(
$xaction,
array(
'type' => 'string',
'value' => 'optional wild',
));
$type = $xaction['type'];
if (!isset($edit_types[$type])) {
throw new Exception(
pht(
'Unsupported bulk edit type "%s".',
$type));
}
$edit_type = $edit_types[$type];
// Replace the edit type with the underlying transaction type. Usually
// these are 1:1 and the transaction type just has more internal noise,
// but it's possible that this isn't the case.
$xaction['type'] = $edit_type->getTransactionType();
$value = $xaction['value'];
$value = $edit_type->getTransactionValueFromBulkEdit($value);
$xaction['value'] = $value;
$xaction_objects = $edit_type->generateTransactions(
clone $template,
$xaction);
foreach ($xaction_objects as $xaction_object) {
$raw_xaction = array(
'type' => $xaction_object->getTransactionType(),
'metadata' => $xaction_object->getMetadata(),
'new' => $xaction_object->getNewValue(),
);
if ($xaction_object->hasOldValue()) {
$raw_xaction['old'] = $xaction_object->getOldValue();
}
if ($xaction_object->hasComment()) {
$comment = $xaction_object->getComment();
$raw_xaction['comment'] = $comment->getContent();
}
$raw_xactions[] = $raw_xaction;
}
}
return $raw_xactions;
}
private function getBulkEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getBulkEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return get_class($this);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getCreateNewObjectPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
index 2e469c5e0a..684c348914 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
@@ -1,639 +1,665 @@
<?php
final class PhabricatorApplicationTransactionCommentView
extends AphrontView {
private $submitButtonName;
private $action;
private $previewPanelID;
private $previewTimelineID;
private $previewToggleID;
private $formID;
private $statusID;
private $commentID;
private $draft;
private $requestURI;
private $showPreview = true;
- private $objectPHID;
+ private $object;
private $headerText;
private $noPermission;
private $fullWidth;
private $infoView;
+ private $editEngine;
private $editEngineLock;
private $noBorder;
private $requiresMFA;
private $currentVersion;
private $versionedDraft;
private $commentActions;
private $commentActionGroups = array();
private $transactionTimeline;
- public function setObjectPHID($object_phid) {
- $this->objectPHID = $object_phid;
+ /**
+ * Set object in which this comment textarea field is displayed
+ */
+ public function setObject($object) {
+ $this->object = $object;
return $this;
}
- public function getObjectPHID() {
- return $this->objectPHID;
+ /**
+ * Get object in which this comment textarea is displayed
+ */
+ public function getObject() {
+ return $this->object;
}
public function setShowPreview($show_preview) {
$this->showPreview = $show_preview;
return $this;
}
public function getShowPreview() {
return $this->showPreview;
}
public function setRequestURI(PhutilURI $request_uri) {
$this->requestURI = $request_uri;
return $this;
}
public function getRequestURI() {
return $this->requestURI;
}
public function setCurrentVersion($current_version) {
$this->currentVersion = $current_version;
return $this;
}
public function getCurrentVersion() {
return $this->currentVersion;
}
public function setVersionedDraft(
PhabricatorVersionedDraft $versioned_draft) {
$this->versionedDraft = $versioned_draft;
return $this;
}
public function getVersionedDraft() {
return $this->versionedDraft;
}
public function setDraft(PhabricatorDraft $draft) {
$this->draft = $draft;
return $this;
}
public function getDraft() {
return $this->draft;
}
public function setSubmitButtonName($submit_button_name) {
$this->submitButtonName = $submit_button_name;
return $this;
}
public function getSubmitButtonName() {
return $this->submitButtonName;
}
public function setAction($action) {
$this->action = $action;
return $this;
}
public function getAction() {
return $this->action;
}
public function setHeaderText($text) {
$this->headerText = $text;
return $this;
}
public function setFullWidth($fw) {
$this->fullWidth = $fw;
return $this;
}
public function setInfoView(PHUIInfoView $info_view) {
$this->infoView = $info_view;
return $this;
}
public function getInfoView() {
return $this->infoView;
}
public function setCommentActions(array $comment_actions) {
assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction');
$this->commentActions = $comment_actions;
return $this;
}
public function getCommentActions() {
return $this->commentActions;
}
public function setCommentActionGroups(array $groups) {
assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup');
$this->commentActionGroups = $groups;
return $this;
}
public function getCommentActionGroups() {
return $this->commentActionGroups;
}
public function setNoPermission($no_permission) {
$this->noPermission = $no_permission;
return $this;
}
public function getNoPermission() {
return $this->noPermission;
}
+ public function setEditEngine(PhabricatorEditEngine $edit_engine) {
+ $this->editEngine = $edit_engine;
+ return $this;
+ }
+
+ public function getEditEngine() {
+ return $this->editEngine;
+ }
+
public function setEditEngineLock(PhabricatorEditEngineLock $lock) {
$this->editEngineLock = $lock;
return $this;
}
public function getEditEngineLock() {
return $this->editEngineLock;
}
public function setRequiresMFA($requires_mfa) {
$this->requiresMFA = $requires_mfa;
return $this;
}
public function getRequiresMFA() {
return $this->requiresMFA;
}
public function setTransactionTimeline(
PhabricatorApplicationTransactionView $timeline) {
$timeline->setQuoteTargetID($this->getCommentID());
if ($this->getNoPermission() || $this->getEditEngineLock()) {
$timeline->setShouldTerminate(true);
}
$this->transactionTimeline = $timeline;
return $this;
}
public function render() {
if ($this->getNoPermission()) {
return null;
}
$lock = $this->getEditEngineLock();
if ($lock) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$lock->getLockedObjectDisplayText(),
));
}
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
$uri = id(new PhutilURI('/login/'))
->replaceQueryParam('next', (string)$this->getRequestURI());
return id(new PHUIObjectBoxView())
->setFlush(true)
->appendChild(
javelin_tag(
'a',
array(
'class' => 'login-to-comment button',
'href' => $uri,
),
pht('Log In to Comment')));
}
if ($this->getRequiresMFA()) {
if (!$viewer->getIsEnrolledInMultiFactor()) {
$viewer->updateMultiFactorEnrollment();
if (!$viewer->getIsEnrolledInMultiFactor()) {
$messages = array();
$messages[] = pht(
'You must provide multi-factor credentials to comment or make '.
'changes, but you do not have multi-factor authentication '.
'configured on your account.');
$messages[] = pht(
'To continue, configure multi-factor authentication in Settings.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages);
}
}
}
$data = array();
$comment = $this->renderCommentPanel();
if ($this->getShowPreview()) {
$preview = $this->renderPreviewPanel();
} else {
$preview = null;
}
if (!$this->getCommentActions()) {
Javelin::initBehavior(
'phabricator-transaction-comment-form',
array(
'formID' => $this->getFormID(),
'timelineID' => $this->getPreviewTimelineID(),
'panelID' => $this->getPreviewPanelID(),
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
));
}
require_celerity_resource('phui-comment-form-css');
$image_uri = $viewer->getProfileImageURI();
$image = javelin_tag(
'div',
array(
'style' => 'background-image: url('.$image_uri.')',
'class' => 'phui-comment-image',
'aural' => false,
));
$wedge = phutil_tag(
'div',
array(
'class' => 'phui-timeline-wedge',
),
'');
$badge_view = $this->renderBadgeView();
$anchor = id(new PhabricatorAnchorView())
->setAnchorName('reply');
$comment_box = id(new PHUIObjectBoxView())
->setFlush(true)
->addClass('phui-comment-form-view')
->addSigil('phui-comment-form')
->appendChild($anchor)
->appendChild(
phutil_tag(
'h3',
array(
'class' => 'aural-only',
),
pht('Add Comment')))
->appendChild($image)
->appendChild($badge_view)
->appendChild($wedge)
->appendChild($comment);
return array($comment_box, $preview);
}
private function renderCommentPanel() {
$viewer = $this->getViewer();
+ $engine = $this->getEditEngine();
+ // In a few rare cases PhabricatorApplicationTransactionCommentView gets
+ // initiated in a View or Controller class. Don't crash in that case.
+ if ($engine) {
+ $placeholder_text = $engine
+ ->getCommentFieldPlaceholderText($this->getObject());
+ } else {
+ $placeholder_text = '';
+ }
$remarkup_control = id(new PhabricatorRemarkupControl())
->setViewer($viewer)
->setID($this->getCommentID())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-textarea-control')
->setCanPin(true)
+ ->setPlaceholder($placeholder_text)
->setName('comment');
$draft_comment = '';
$draft_metadata = array();
$draft_key = null;
$legacy_draft = $this->getDraft();
if ($legacy_draft) {
$draft_comment = $legacy_draft->getDraft();
$draft_key = $legacy_draft->getDraftKey();
}
$versioned_draft = $this->getVersionedDraft();
if ($versioned_draft) {
$draft_comment = $versioned_draft->getProperty(
'comment',
$draft_comment);
$draft_metadata = $versioned_draft->getProperty(
'metadata',
$draft_metadata);
}
$remarkup_control->setValue($draft_comment);
if (!is_array($draft_metadata)) {
$draft_metadata = array();
}
$remarkup_control->setRemarkupMetadata($draft_metadata);
- if (!$this->getObjectPHID()) {
+ if (!$this->getObject()->getPHID()) {
throw new PhutilInvalidStateException('setObjectPHID', 'render');
}
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$version_value = $this->getCurrentVersion();
$form = id(new AphrontFormView())
->setUser($viewer)
->addSigil('transaction-append')
->setWorkflow(true)
->setFullWidth($this->fullWidth)
->setMetadata(
array(
- 'objectPHID' => $this->getObjectPHID(),
+ 'objectPHID' => $this->getObject()->getPHID(),
))
->setAction($this->getAction())
->setID($this->getFormID())
->addHiddenInput('__draft__', $draft_key)
->addHiddenInput($version_key, $version_value);
$comment_actions = $this->getCommentActions();
if ($comment_actions) {
$action_map = array();
$type_map = array();
$comment_actions = mpull($comment_actions, null, 'getKey');
$draft_actions = array();
$draft_keys = array();
if ($versioned_draft) {
$draft_actions = $versioned_draft->getProperty('actions', array());
if (!is_array($draft_actions)) {
$draft_actions = array();
}
foreach ($draft_actions as $action) {
$type = idx($action, 'type');
$comment_action = idx($comment_actions, $type);
if (!$comment_action) {
continue;
}
$value = idx($action, 'value');
$comment_action->setValue($value);
$draft_keys[] = $type;
}
}
foreach ($comment_actions as $key => $comment_action) {
$key = $comment_action->getKey();
$label = $comment_action->getLabel();
$action_map[$key] = array(
'key' => $key,
'label' => $label,
'type' => $comment_action->getPHUIXControlType(),
'spec' => $comment_action->getPHUIXControlSpecification(),
'initialValue' => $comment_action->getInitialValue(),
'groupKey' => $comment_action->getGroupKey(),
'conflictKey' => $comment_action->getConflictKey(),
'auralLabel' => pht('Remove Action: %s', $label),
'buttonText' => $comment_action->getSubmitButtonText(),
);
$type_map[$key] = $comment_action;
}
$options = $this->newCommentActionOptions($action_map);
$action_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
$place_id = celerity_generate_unique_node_id();
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'editengine.actions',
'id' => $input_id,
)));
$invisi_bar = phutil_tag(
'div',
array(
'id' => $place_id,
'class' => 'phui-comment-control-stack',
));
$action_select = id(new AphrontFormSelectControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-action-control')
->setID($action_id)
->setOptions($options);
$action_bar = phutil_tag(
'div',
array(
'class' => 'phui-comment-action-bar grouped',
),
array(
$action_select,
));
$form->appendChild($action_bar);
$info_view = $this->getInfoView();
if ($info_view) {
$form->appendChild($info_view);
}
if ($this->getRequiresMFA()) {
$message = pht(
'You will be required to provide multi-factor credentials to '.
'comment or make changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
}
$form->appendChild($invisi_bar);
$form->addClass('phui-comment-has-actions');
$timeline = $this->transactionTimeline;
$view_data = array();
if ($timeline) {
$view_data = $timeline->getViewData();
}
Javelin::initBehavior(
'comment-actions',
array(
'actionID' => $action_id,
'inputID' => $input_id,
'formID' => $this->getFormID(),
'placeID' => $place_id,
'panelID' => $this->getPreviewPanelID(),
'timelineID' => $this->getPreviewTimelineID(),
'actions' => $action_map,
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
'drafts' => $draft_keys,
'defaultButtonText' => $this->getSubmitButtonName(),
'viewData' => $view_data,
));
}
$submit_button = id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->setValue($this->getSubmitButtonName());
$form
->appendChild($remarkup_control)
->appendChild(
id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->addSigil('submit-transactions')
->setValue($this->getSubmitButtonName()));
return $form;
}
private function renderPreviewPanel() {
$preview = id(new PHUITimelineView())
->setID($this->getPreviewTimelineID());
return phutil_tag(
'div',
array(
'id' => $this->getPreviewPanelID(),
'style' => 'display: none',
'class' => 'phui-comment-preview-view',
),
$preview);
}
private function getPreviewPanelID() {
if (!$this->previewPanelID) {
$this->previewPanelID = celerity_generate_unique_node_id();
}
return $this->previewPanelID;
}
private function getPreviewTimelineID() {
if (!$this->previewTimelineID) {
$this->previewTimelineID = celerity_generate_unique_node_id();
}
return $this->previewTimelineID;
}
public function setFormID($id) {
$this->formID = $id;
return $this;
}
private function getFormID() {
if (!$this->formID) {
$this->formID = celerity_generate_unique_node_id();
}
return $this->formID;
}
private function getStatusID() {
if (!$this->statusID) {
$this->statusID = celerity_generate_unique_node_id();
}
return $this->statusID;
}
private function getCommentID() {
if (!$this->commentID) {
$this->commentID = celerity_generate_unique_node_id();
}
return $this->commentID;
}
private function newCommentActionOptions(array $action_map) {
$options = array();
$options['+'] = pht('Add Action...');
// Merge options into groups.
$groups = array();
foreach ($action_map as $key => $item) {
$group_key = $item['groupKey'];
if (!isset($groups[$group_key])) {
$groups[$group_key] = array();
}
$groups[$group_key][$key] = $item;
}
$group_specs = $this->getCommentActionGroups();
$group_labels = mpull($group_specs, 'getLabel', 'getKey');
// Reorder groups to put them in the same order as the recognized
// group definitions.
$groups = array_select_keys($groups, array_keys($group_labels)) + $groups;
// Move options with no group to the end.
$default_group = idx($groups, '');
if ($default_group) {
unset($groups['']);
$groups[''] = $default_group;
}
foreach ($groups as $group_key => $group_items) {
if (strlen($group_key)) {
$group_label = idx($group_labels, $group_key, $group_key);
$options[$group_label] = ipull($group_items, 'label');
} else {
foreach ($group_items as $key => $item) {
$options[$key] = $item['label'];
}
}
}
return $options;
}
private function renderBadgeView() {
$user = $this->getUser();
$can_use_badges = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorBadgesApplication',
$user);
if (!$can_use_badges) {
return null;
}
// Pull Badges from UserCache
$badges = $user->getRecentBadgeAwards();
$badge_view = null;
if ($badges) {
$badge_list = array();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge['id'].'/');
$badge_list[] = $badge_view;
}
$flex = new PHUIBadgeBoxView();
$flex->addItems($badge_list);
$flex->setCollapsed(true);
$badge_view = phutil_tag(
'div',
array(
'class' => 'phui-timeline-badges',
),
$flex);
}
return $badge_view;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 1:06 AM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1133
Default Alt Text
(139 KB)

Event Timeline