diff --git a/src/applications/differential/customfield/DifferentialCoreCustomField.php b/src/applications/differential/customfield/DifferentialCoreCustomField.php
index a63ffca7b..7333857ce 100644
--- a/src/applications/differential/customfield/DifferentialCoreCustomField.php
+++ b/src/applications/differential/customfield/DifferentialCoreCustomField.php
@@ -1,120 +1,120 @@
setFieldError(null);
$errors = parent::validateApplicationTransactions(
$editor,
$type,
$xactions);
$transaction = null;
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if ($this->isCoreFieldRequired()) {
if ($this->isCoreFieldValueEmpty($value)) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
$this->getCoreFieldRequiredErrorString(),
$xaction);
$error->setIsMissingFieldError(true);
$errors[] = $error;
$this->setFieldError(pht('Required'));
}
}
}
return $errors;
}
public function canDisableField() {
return false;
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return true;
}
- protected function didSetObject(PhabricatorCustomFieldInterface $object) {
+ public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
if ($this->isCoreFieldRequired()) {
$this->setFieldError(true);
}
$this->setValue($this->readValueFromRevision($object));
}
public function getOldValueForApplicationTransactions() {
return $this->readValueFromRevision($this->getObject());
}
public function getNewValueForApplicationTransactions() {
return $this->getValue();
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->writeValueToRevision($this->getObject(), $xaction->getNewValue());
}
public function setFieldError($field_error) {
$this->fieldError = $field_error;
return $this;
}
public function getFieldError() {
return $this->fieldError;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index 396473078..a93846a61 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,719 +1,713 @@
id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$e_title = null;
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$workflow = $request->getStr('workflow');
$parent_task = null;
if ($workflow && is_numeric($workflow)) {
$parent_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($workflow))
->executeOne();
}
$transactions = id(new ManiphestTransactionQuery())
->setViewer($user)
->withObjectPHIDs(array($task->getPHID()))
->needComments(true)
->execute();
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_VIEW);
-
- foreach ($field_list->getFields() as $field) {
- $field->setObject($task);
- $field->setViewer($user);
- }
-
- $field_list->readFieldsFromStorage($task);
-
- $aux_fields = $field_list->getFields();
+ $field_list
+ ->setViewer($user)
+ ->readFieldsFromStorage($task);
$e_commit = PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT;
$e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK;
$e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK;
$e_rev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
$e_mock = PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK;
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes(
array(
$e_commit,
$e_dep_on,
$e_dep_by,
$e_rev,
$e_mock,
));
$edges = idx($query->execute(), $phid);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
foreach ($task->getCCPHIDs() as $phid) {
$phids[$phid] = true;
}
foreach ($task->getProjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$attached = $task->getAttached();
foreach ($attached as $type => $list) {
foreach ($list as $phid => $info) {
$phids[$phid] = true;
}
}
if ($parent_task) {
$phids[$parent_task->getPHID()] = true;
}
$phids = array_keys($phids);
$this->loadHandles($phids);
$handles = $this->getLoadedHandles();
$context_bar = null;
if ($parent_task) {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?parent='.$parent_task->getID(),
'class' => 'green button',
),
pht('Create Another Subtask')));
$context_bar->appendChild(hsprintf(
'Created a subtask of %s',
$this->getHandle($parent_task->getPHID())->renderLink()));
} else if ($workflow == 'create') {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag('label', array(), 'Create Another'));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?template='.$task->getID(),
'class' => 'green button',
),
pht('Similar Task')));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/',
'class' => 'green button',
),
pht('Empty Task')));
$context_bar->appendChild(pht('New task created.'));
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION);
foreach ($transactions as $modern_xaction) {
if ($modern_xaction->getComment()) {
$engine->addObject(
$modern_xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$resolution_types = ManiphestTaskStatus::getTaskStatusMap();
$transaction_types = array(
PhabricatorTransactions::TYPE_COMMENT => pht('Comment'),
ManiphestTransaction::TYPE_STATUS => pht('Close Task'),
ManiphestTransaction::TYPE_OWNER => pht('Reassign / Claim'),
ManiphestTransaction::TYPE_CCS => pht('Add CCs'),
ManiphestTransaction::TYPE_PRIORITY => pht('Change Priority'),
ManiphestTransaction::TYPE_ATTACH => pht('Upload File'),
ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'),
);
// Remove actions the user doesn't have permission to take.
$requires = array(
ManiphestTransaction::TYPE_OWNER =>
ManiphestCapabilityEditAssign::CAPABILITY,
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestCapabilityEditPriority::CAPABILITY,
ManiphestTransaction::TYPE_PROJECTS =>
ManiphestCapabilityEditProjects::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestCapabilityEditStatus::CAPABILITY,
);
foreach ($transaction_types as $type => $name) {
if (isset($requires[$type])) {
if (!$this->hasApplicationCapability($requires[$type])) {
unset($transaction_types[$type]);
}
}
}
if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) {
$resolution_types = array_select_keys(
$resolution_types,
array(
ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
ManiphestTaskStatus::STATUS_CLOSED_INVALID,
ManiphestTaskStatus::STATUS_CLOSED_SPITE,
));
} else {
$resolution_types = array(
ManiphestTaskStatus::STATUS_OPEN => 'Reopened',
);
$transaction_types[ManiphestTransaction::TYPE_STATUS] =
'Reopen Task';
unset($transaction_types[ManiphestTransaction::TYPE_PRIORITY]);
unset($transaction_types[ManiphestTransaction::TYPE_OWNER]);
}
$default_claim = array(
$user->getPHID() => $user->getUsername().' ('.$user->getRealName().')',
);
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft_text = $draft->getDraft();
} else {
$draft_text = null;
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
// Prevent tasks from being closed "out of spite" in serious business
// installs.
unset($resolution_types[ManiphestTaskStatus::STATUS_CLOSED_SPITE]);
}
$comment_form = new AphrontFormView();
$comment_form
->setUser($user)
->setAction('/maniphest/transaction/save/')
->setEncType('multipart/form-data')
->addHiddenInput('taskID', $task->getID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Action'))
->setName('action')
->setOptions($transaction_types)
->setID('transaction-action'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Resolution'))
->setName('resolution')
->setControlID('resolution')
->setControlStyle('display: none')
->setOptions($resolution_types))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assign To'))
->setName('assign_to')
->setControlID('assign_to')
->setControlStyle('display: none')
->setID('assign-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CCs'))
->setName('ccs')
->setControlID('ccs')
->setControlStyle('display: none')
->setID('cc-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setControlID('priority')
->setControlStyle('display: none')
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setControlID('projects')
->setControlStyle('display: none')
->setID('projects-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormFileControl())
->setLabel(pht('File'))
->setName('file')
->setControlID('file')
->setControlStyle('display: none'))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Comments'))
->setName('comments')
->setValue($draft_text)
->setID('transaction-comments')
->setUser($user))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? pht('Submit') : pht('Avast!')));
$control_map = array(
ManiphestTransaction::TYPE_STATUS => 'resolution',
ManiphestTransaction::TYPE_OWNER => 'assign_to',
ManiphestTransaction::TYPE_CCS => 'ccs',
ManiphestTransaction::TYPE_PRIORITY => 'priority',
ManiphestTransaction::TYPE_PROJECTS => 'projects',
ManiphestTransaction::TYPE_ATTACH => 'file',
);
$tokenizer_map = array(
ManiphestTransaction::TYPE_PROJECTS => array(
'id' => 'projects-tokenizer',
'src' => '/typeahead/common/projects/',
'placeholder' => pht('Type a project name...'),
),
ManiphestTransaction::TYPE_OWNER => array(
'id' => 'assign-tokenizer',
'src' => '/typeahead/common/users/',
'value' => $default_claim,
'limit' => 1,
'placeholder' => pht('Type a user name...'),
),
ManiphestTransaction::TYPE_CCS => array(
'id' => 'cc-tokenizer',
'src' => '/typeahead/common/mailable/',
'placeholder' => pht('Type a user or mailing list...'),
),
);
// TODO: Initializing these behaviors for logged out users fatals things.
if ($user->isLoggedIn()) {
Javelin::initBehavior('maniphest-transaction-controls', array(
'select' => 'transaction-action',
'controlMap' => $control_map,
'tokenizers' => $tokenizer_map,
));
Javelin::initBehavior('maniphest-transaction-preview', array(
'uri' => '/maniphest/transaction/preview/'.$task->getID().'/',
'preview' => 'transaction-preview',
'comments' => 'transaction-comments',
'action' => 'transaction-action',
'map' => $control_map,
'tokenizers' => $tokenizer_map,
));
}
$comment_header = $is_serious
? pht('Add Comment')
: pht('Weigh In');
$preview_panel = phutil_tag_div(
'aphront-panel-preview',
phutil_tag(
'div',
array('id' => 'transaction-preview'),
phutil_tag_div(
'aphront-panel-preview-loading-text',
pht('Loading preview...'))));
$timeline = id(new PhabricatorApplicationTransactionView())
->setUser($user)
->setObjectPHID($task->getPHID())
->setTransactions($transactions)
->setMarkupEngine($engine);
$object_name = 'T'.$task->getID();
$actions = $this->buildActionView($task);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($object_name, '/'.$object_name)
->setActionList($actions);
$header = $this->buildHeaderView($task);
$properties = $this->buildPropertyView(
$task, $field_list, $edges, $actions);
$description = $this->buildDescriptionView($task, $engine);
if (!$user->isLoggedIn()) {
// TODO: Eventually, everything should run through this. For now, we're
// only using it to get a consistent "Login to Comment" button.
$comment_box = id(new PhabricatorApplicationTransactionCommentView())
->setUser($user)
->setRequestURI($request->getRequestURI());
$preview_panel = null;
} else {
$comment_box = id(new PHUIObjectBoxView())
->setFlush(true)
->setHeaderText($comment_header)
->appendChild($comment_form);
}
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
if ($description) {
$object_box->addPropertyList($description);
}
return $this->buildApplicationPage(
array(
$crumbs,
$context_bar,
$object_box,
$timeline,
$comment_box,
$preview_panel,
),
array(
'title' => 'T'.$task->getID().' '.$task->getTitle(),
'pageObjects' => array($task->getPHID()),
'device' => true,
));
}
private function buildHeaderView(ManiphestTask $task) {
$view = id(new PHUIHeaderView())
->setHeader($task->getTitle())
->setUser($this->getRequest()->getUser())
->setPolicyObject($task);
$status = $task->getStatus();
$status_name = ManiphestTaskStatus::renderFullDescription($status);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name);
return $view;
}
private function buildActionView(ManiphestTask $task) {
$viewer = $this->getRequest()->getUser();
$viewer_phid = $viewer->getPHID();
$viewer_is_cc = in_array($viewer_phid, $task->getCCPHIDs());
$id = $task->getID();
$phid = $task->getPHID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$task,
PhabricatorPolicyCapability::CAN_EDIT);
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($task)
->setObjectURI($this->getRequest()->getRequestURI());
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Task'))
->setIcon('edit')
->setHref($this->getApplicationURI("/task/edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($task->getOwnerPHID() === $viewer_phid) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Automatically Subscribed'))
->setDisabled(true)
->setIcon('enable'));
} else {
$action = $viewer_is_cc ? 'rem' : 'add';
$name = $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe');
$icon = $viewer_is_cc ? 'disable' : 'check';
$view->addAction(
id(new PhabricatorActionView())
->setName($name)
->setHref("/maniphest/subscribe/{$action}/{$id}/")
->setRenderAsForm(true)
->setUser($viewer)
->setIcon($icon));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Merge Duplicates In'))
->setHref("/search/attach/{$phid}/TASK/merge/")
->setWorkflow(true)
->setIcon('merge')
->setDisabled(!$can_edit)
->setWorkflow(true));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($this->getApplicationURI("/task/create/?parent={$id}"))
->setIcon('fork'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Dependencies'))
->setHref("/search/attach/{$phid}/TASK/dependencies/")
->setWorkflow(true)
->setIcon('link')
->setDisabled(!$can_edit)
->setWorkflow(true));
return $view;
}
private function buildPropertyView(
ManiphestTask $task,
PhabricatorCustomFieldList $field_list,
array $edges,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($task)
->setActionList($actions);
$view->addProperty(
pht('Assigned To'),
$task->getOwnerPHID()
? $this->getHandle($task->getOwnerPHID())->renderLink()
: phutil_tag('em', array(), pht('None')));
$view->addProperty(
pht('Priority'),
ManiphestTaskPriority::getTaskPriorityName($task->getPriority()));
$view->addProperty(
pht('Subscribers'),
$task->getCCPHIDs()
? $this->renderHandlesForPHIDs($task->getCCPHIDs(), ',')
: phutil_tag('em', array(), pht('None')));
$view->addProperty(
pht('Author'),
$this->getHandle($task->getAuthorPHID())->renderLink());
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$view->addProperty(
pht('From Email'),
phutil_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject
),
$source));
}
$project_phids = $task->getProjectPHIDs();
if ($project_phids) {
require_celerity_resource('maniphest-task-summary-css');
// If we end up with real-world projects with many hundreds of columns, it
// might be better to just load all the edges, then load those columns and
// work backward that way, or denormalize this data more.
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs($project_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
$column_edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN;
$all_column_phids = array_keys($columns);
$column_edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($task->getPHID()))
->withEdgeTypes(array($column_edge_type))
->withDestinationPHIDs($all_column_phids);
$column_edge_query->execute();
$in_column_phids = array_fuse($column_edge_query->getDestinationPHIDs());
$column_groups = mgroup($columns, 'getProjectPHID');
$project_rows = array();
foreach ($project_phids as $project_phid) {
$row = array();
$handle = $this->getHandle($project_phid);
$row[] = $handle->renderLink();
$columns = idx($column_groups, $project_phid, array());
$column = head(array_intersect_key($columns, $in_column_phids));
if ($column) {
$column_name = pht('(%s)', $column->getDisplayName());
// TODO: This is really hacky but there's no cleaner way to do it
// right now, T4022 should give us better tools for this.
$column_href = str_replace(
'project/view',
'project/board',
$handle->getURI());
$column_link = phutil_tag(
'a',
array(
'href' => $column_href,
'class' => 'maniphest-board-link',
),
$column_name);
$row[] = ' ';
$row[] = $column_link;
}
$project_rows[] = phutil_tag('div', array(), $row);
}
} else {
$project_rows = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(pht('Projects'), $project_rows);
$edge_types = array(
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK
=> pht('Dependent Tasks'),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK
=> pht('Depends On'),
PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV
=> pht('Differential Revisions'),
PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK
=> pht('Pholio Mocks'),
);
$revisions_commits = array();
$handles = $this->getLoadedHandles();
$commit_phids = array_keys(
$edges[PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT]);
if ($commit_phids) {
$commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV;
$drev_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($commit_drev))
->execute();
foreach ($commit_phids as $phid) {
$revisions_commits[$phid] = $handles[$phid]->renderLink();
$revision_phid = key($drev_edges[$phid][$commit_drev]);
$revision_handle = idx($handles, $revision_phid);
if ($revision_handle) {
$task_drev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
unset($edges[$task_drev][$revision_phid]);
$revisions_commits[$phid] = hsprintf(
'%s / %s',
$revision_handle->renderLink($revision_handle->getName()),
$revisions_commits[$phid]);
}
}
}
foreach ($edge_types as $edge_type => $edge_name) {
if ($edges[$edge_type]) {
$view->addProperty(
$edge_name,
$this->renderHandlesForPHIDs(array_keys($edges[$edge_type])));
}
}
if ($revisions_commits) {
$view->addProperty(
pht('Commits'),
phutil_implode_html(phutil_tag('br'), $revisions_commits));
}
$attached = $task->getAttached();
if (!is_array($attached)) {
$attached = array();
}
$file_infos = idx($attached, PhabricatorFilePHIDTypeFile::TYPECONST);
if ($file_infos) {
$file_phids = array_keys($file_infos);
// TODO: These should probably be handles or something; clean this up
// as we sort out file attachments.
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$file_view = new PhabricatorFileLinkListView();
$file_view->setFiles($files);
$view->addProperty(
pht('Files'),
$file_view->render());
}
$field_list->appendFieldsToPropertyList(
$task,
$viewer,
$view);
$view->invokeWillRenderEvent();
return $view;
}
private function buildDescriptionView(
ManiphestTask $task,
PhabricatorMarkupEngine $engine) {
$section = null;
if (strlen($task->getDescription())) {
$section = new PHUIPropertyListView();
$section->addSectionHeader(
pht('Description'),
PHUIPropertyListView::ICON_SUMMARY);
$section->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION)));
}
return $section;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index ecdbae082..294ff0724 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,683 +1,679 @@
id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$can_edit_assign = $this->hasApplicationCapability(
ManiphestCapabilityEditAssign::CAPABILITY);
$can_edit_policies = $this->hasApplicationCapability(
ManiphestCapabilityEditPolicies::CAPABILITY);
$can_edit_priority = $this->hasApplicationCapability(
ManiphestCapabilityEditPriority::CAPABILITY);
$can_edit_projects = $this->hasApplicationCapability(
ManiphestCapabilityEditProjects::CAPABILITY);
$can_edit_status = $this->hasApplicationCapability(
ManiphestCapabilityEditStatus::CAPABILITY);
$files = array();
$parent_task = null;
$template_id = null;
if ($this->id) {
$task = id(new ManiphestTaskQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($this->id))
->executeOne();
if (!$task) {
return new Aphront404Response();
}
} else {
$task = ManiphestTask::initializeNewTask($user);
// These allow task creation with defaults.
if (!$request->isFormPost()) {
$task->setTitle($request->getStr('title'));
if ($can_edit_projects) {
$projects = $request->getStr('projects');
if ($projects) {
$tokens = explode(';', $projects);
$slug_map = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPhrictionSlugs($tokens)
->execute();
$name_map = id(new PhabricatorProjectQuery())
->setViewer($user)
->withNames($tokens)
->execute();
$phid_map = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs($tokens)
->execute();
$all_map = mpull($slug_map, null, 'getPhrictionSlug') +
mpull($name_map, null, 'getName') +
mpull($phid_map, null, 'getPHID');
$default_projects = array();
foreach ($tokens as $token) {
if (isset($all_map[$token])) {
$default_projects[] = $all_map[$token]->getPHID();
}
}
if ($default_projects) {
$task->setProjectPHIDs($default_projects);
}
}
}
if ($can_edit_priority) {
$priority = $request->getInt('priority');
if ($priority !== null) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if (isset($priority_map[$priority])) {
$task->setPriority($priority);
}
}
}
$task->setDescription($request->getStr('description'));
if ($can_edit_assign) {
$assign = $request->getStr('assign');
if (strlen($assign)) {
$assign_user = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withUsernames(array($assign))
->executeOne();
if (!$assign_user) {
$assign_user = id(new PhabricatorPeopleQuery())
->setViewer($user)
->withPHIDs(array($assign))
->executeOne();
}
if ($assign_user) {
$task->setOwnerPHID($assign_user->getPHID());
}
}
}
}
$file_phids = $request->getArr('files', array());
if (!$file_phids) {
// Allow a single 'file' key instead, mostly since Mac OS X urlencodes
// square brackets in URLs when passed to 'open', so you can't 'open'
// a URL like '?files[]=xyz' and have PHP interpret it correctly.
$phid = $request->getStr('file');
if ($phid) {
$file_phids = array($phid);
}
}
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($user)
->withPHIDs($file_phids)
->execute();
}
$template_id = $request->getInt('template');
// You can only have a parent task if you're creating a new task.
$parent_id = $request->getInt('parent');
if ($parent_id) {
$parent_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($parent_id))
->executeOne();
if (!$template_id) {
$template_id = $parent_id;
}
}
}
$errors = array();
$e_title = true;
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->setViewer($user);
-
- foreach ($field_list->getFields() as $field) {
- $field->setObject($task);
- }
-
$field_list->readFieldsFromStorage($task);
$aux_fields = $field_list->getFields();
if ($request->isFormPost()) {
$changes = array();
$new_title = $request->getStr('title');
$new_desc = $request->getStr('description');
$new_status = $request->getStr('status');
if (!$task->getID()) {
$workflow = 'create';
} else {
$workflow = '';
}
$changes[ManiphestTransaction::TYPE_TITLE] = $new_title;
$changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc;
if ($can_edit_status) {
$changes[ManiphestTransaction::TYPE_STATUS] = $new_status;
}
$owner_tokenizer = $request->getArr('assigned_to');
$owner_phid = reset($owner_tokenizer);
if (!strlen($new_title)) {
$e_title = pht('Required');
$errors[] = pht('Title is required.');
}
$old_values = array();
foreach ($aux_fields as $aux_arr_key => $aux_field) {
// TODO: This should be buildFieldTransactionsFromRequest() once we
// switch to ApplicationTransactions properly.
$aux_old_value = $aux_field->getOldValueForApplicationTransactions();
$aux_field->readValueFromRequest($request);
$aux_new_value = $aux_field->getNewValueForApplicationTransactions();
// TODO: We're faking a call to the ApplicaitonTransaction validation
// logic here. We need valid objects to pass, but they aren't used
// in a meaningful way. For now, build User objects. Once the Maniphest
// objects exist, this will switch over automatically. This is a big
// hack but shouldn't be long for this world.
$placeholder_editor = new PhabricatorUserProfileEditor();
$field_errors = $aux_field->validateApplicationTransactions(
$placeholder_editor,
PhabricatorTransactions::TYPE_CUSTOMFIELD,
array(
id(new ManiphestTransaction())
->setOldValue($aux_old_value)
->setNewValue($aux_new_value),
));
foreach ($field_errors as $error) {
$errors[] = $error->getMessage();
}
$old_values[$aux_field->getFieldKey()] = $aux_old_value;
}
if ($errors) {
$task->setTitle($new_title);
$task->setDescription($new_desc);
$task->setPriority($request->getInt('priority'));
$task->setOwnerPHID($owner_phid);
$task->setCCPHIDs($request->getArr('cc'));
$task->setProjectPHIDs($request->getArr('projects'));
} else {
if ($can_edit_priority) {
$changes[ManiphestTransaction::TYPE_PRIORITY] =
$request->getInt('priority');
}
if ($can_edit_assign) {
$changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
}
$changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc');
if ($can_edit_projects) {
$changes[ManiphestTransaction::TYPE_PROJECTS] =
$request->getArr('projects');
}
if ($can_edit_policies) {
$changes[PhabricatorTransactions::TYPE_VIEW_POLICY] =
$request->getStr('viewPolicy');
$changes[PhabricatorTransactions::TYPE_EDIT_POLICY] =
$request->getStr('editPolicy');
}
if ($files) {
$file_map = mpull($files, 'getPHID');
$file_map = array_fill_keys($file_map, array());
$changes[ManiphestTransaction::TYPE_ATTACH] = array(
PhabricatorFilePHIDTypeFile::TYPECONST => $file_map,
);
}
$template = new ManiphestTransaction();
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
$transactions[] = $transaction;
}
if ($aux_fields) {
foreach ($aux_fields as $aux_field) {
$transaction = clone $template;
$transaction->setTransactionType(
PhabricatorTransactions::TYPE_CUSTOMFIELD);
$aux_key = $aux_field->getFieldKey();
$transaction->setMetadataValue('customfield:key', $aux_key);
$old = idx($old_values, $aux_key);
$new = $aux_field->getNewValueForApplicationTransactions();
$transaction->setOldValue($old);
$transaction->setNewValue($new);
$transactions[] = $transaction;
}
}
if ($transactions) {
$is_new = !$task->getID();
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
if ($parent_task) {
id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge(
$parent_task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$task->getPHID())
->save();
$workflow = $parent_task->getID();
}
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent(
array(
'tasks' => $this->renderSingleTask($task),
));
}
$redirect_uri = '/T'.$task->getID();
if ($workflow) {
$redirect_uri .= '?workflow='.$workflow;
}
return id(new AphrontRedirectResponse())
->setURI($redirect_uri);
}
} else {
if (!$task->getID()) {
$task->setCCPHIDs(array(
$user->getPHID(),
));
if ($template_id) {
$template_task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($template_id))
->executeOne();
if ($template_task) {
$task->setCCPHIDs($template_task->getCCPHIDs());
$task->setProjectPHIDs($template_task->getProjectPHIDs());
$task->setOwnerPHID($template_task->getOwnerPHID());
$task->setPriority($template_task->getPriority());
$task->setViewPolicy($template_task->getViewPolicy());
$task->setEditPolicy($template_task->getEditPolicy());
$template_fields = PhabricatorCustomField::getObjectFields(
$template_task,
PhabricatorCustomField::ROLE_EDIT);
$fields = $template_fields->getFields();
foreach ($fields as $key => $field) {
if (!$field->shouldCopyWhenCreatingSimilarTask()) {
unset($fields[$key]);
}
if (empty($aux_fields[$key])) {
unset($fields[$key]);
}
}
if ($fields) {
id(new PhabricatorCustomFieldList($fields))
+ ->setViewer($user)
->readFieldsFromStorage($template_task);
foreach ($fields as $key => $field) {
$aux_fields[$key]->setValueFromStorage(
$field->getValueForStorage());
}
}
}
}
}
}
$phids = array_merge(
array($task->getOwnerPHID()),
$task->getCCPHIDs(),
$task->getProjectPHIDs());
if ($parent_task) {
$phids[] = $parent_task->getPHID();
}
$phids = array_filter($phids);
$phids = array_unique($phids);
$handles = $this->loadViewerHandles($phids);
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
}
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($task->getOwnerPHID()) {
$assigned_value = array($handles[$task->getOwnerPHID()]);
} else {
$assigned_value = array();
}
if ($task->getCCPHIDs()) {
$cc_value = array_select_keys($handles, $task->getCCPHIDs());
} else {
$cc_value = array();
}
if ($task->getProjectPHIDs()) {
$projects_value = array_select_keys($handles, $task->getProjectPHIDs());
} else {
$projects_value = array();
}
$cancel_id = nonempty($task->getID(), $template_id);
if ($cancel_id) {
$cancel_uri = '/T'.$cancel_id;
} else {
$cancel_uri = '/maniphest/';
}
if ($task->getID()) {
$button_name = pht('Save Task');
$header_name = pht('Edit Task');
} else if ($parent_task) {
$cancel_uri = '/T'.$parent_task->getID();
$button_name = pht('Create Task');
$header_name = pht('Create New Subtask');
} else {
$button_name = pht('Create Task');
$header_name = pht('Create New Task');
}
require_celerity_resource('maniphest-task-edit-css');
$project_tokenizer_id = celerity_generate_unique_node_id();
if ($request->isAjax()) {
$form = new PHUIFormLayoutView();
} else {
$form = new AphrontFormView();
$form
->setUser($user)
->addHiddenInput('template', $template_id);
}
if ($parent_task) {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Parent Task'))
->setValue($handles[$parent_task->getPHID()]->getFullName()))
->addHiddenInput('parent', $parent_task->getID());
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Title'))
->setName('title')
->setError($e_title)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($task->getTitle()));
if ($task->getID() && $can_edit_status) {
// Only show this in "edit" mode, not "create" mode, since creating a
// non-open task is kind of silly and it would just clutter up the
// "create" interface.
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setValue($task->getStatus())
->setOptions(ManiphestTaskStatus::getTaskStatusMap()));
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($task)
->execute();
if ($can_edit_assign) {
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assigned To'))
->setName('assigned_to')
->setValue($assigned_value)
->setUser($user)
->setDatasource('/typeahead/common/users/')
->setLimit(1));
}
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CC'))
->setName('cc')
->setValue($cc_value)
->setUser($user)
->setDatasource('/typeahead/common/mailable/'));
if ($can_edit_priority) {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setValue($task->getPriority()));
}
if ($can_edit_policies) {
$form
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($task)
->setPolicies($policies)
->setName('viewPolicy'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($task)
->setPolicies($policies)
->setName('editPolicy'));
}
if ($can_edit_projects) {
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($projects_value)
->setID($project_tokenizer_id)
->setCaption(
javelin_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
pht('Create New Project')))
->setDatasource('/typeahead/common/projects/'));
}
$field_list->appendFieldsToForm($form);
require_celerity_resource('aphront-error-view-css');
Javelin::initBehavior('project-create', array(
'tokenizerID' => $project_tokenizer_id,
));
if ($files) {
$file_display = mpull($files, 'getName');
$file_display = phutil_implode_html(phutil_tag('br'), $file_display);
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Files'))
->setValue($file_display));
foreach ($files as $ii => $file) {
$form->addHiddenInput('files['.$ii.']', $file->getPHID());
}
}
$description_control = new PhabricatorRemarkupControl();
// "Upsell" creating tasks via email in create flows if the instance is
// configured for this awesomeness.
$email_create = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.public-create-email');
if (!$task->getID() && $email_create) {
$email_hint = pht(
'You can also create tasks by sending an email to: %s',
phutil_tag('tt', array(), $email_create));
$description_control->setCaption($email_hint);
}
$description_control
->setLabel(pht('Description'))
->setName('description')
->setID('description-textarea')
->setValue($task->getDescription())
->setUser($user);
$form
->appendChild($description_control);
if ($request->isAjax()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_name)
->appendChild(
array(
$error_view,
$form,
))
->addCancelButton($cancel_uri)
->addSubmitButton($button_name);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button_name));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($header_name)
->setFormErrors($errors)
->setForm($form);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview'))
->setControlID('description-textarea')
->setPreviewURI($this->getApplicationURI('task/descriptionpreview/'));
if ($task->getID()) {
$page_objects = array($task->getPHID());
} else {
$page_objects = array();
}
$crumbs = $this->buildApplicationCrumbs();
if ($task->getID()) {
$crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID());
}
$crumbs->addTextCrumb($header_name);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
$preview,
),
array(
'title' => $header_name,
'pageObjects' => $page_objects,
'device' => true,
));
}
}
diff --git a/src/applications/people/customfield/PhabricatorUserBlurbField.php b/src/applications/people/customfield/PhabricatorUserBlurbField.php
index 5e9c5627f..8cdb10022 100644
--- a/src/applications/people/customfield/PhabricatorUserBlurbField.php
+++ b/src/applications/people/customfield/PhabricatorUserBlurbField.php
@@ -1,79 +1,79 @@
value = $object->loadUserProfile()->getBlurb();
}
public function getOldValueForApplicationTransactions() {
return $this->getObject()->loadUserProfile()->getBlurb();
}
public function getNewValueForApplicationTransactions() {
return $this->value;
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->getObject()->loadUserProfile()->setBlurb($xaction->getNewValue());
}
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}
public function renderEditControl(array $handles) {
return id(new PhabricatorRemarkupControl())
->setName($this->getFieldKey())
->setValue($this->value)
->setLabel($this->getFieldName());
}
public function renderPropertyViewLabel() {
return null;
}
public function renderPropertyViewValue() {
$blurb = $this->getObject()->loadUserProfile()->getBlurb();
if (!strlen($blurb)) {
return null;
}
return PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($blurb),
'default',
$this->getViewer());
}
public function getStyleForPropertyView() {
return 'block';
}
}
diff --git a/src/applications/people/customfield/PhabricatorUserRealNameField.php b/src/applications/people/customfield/PhabricatorUserRealNameField.php
index 87a062d21..90e490fc3 100644
--- a/src/applications/people/customfield/PhabricatorUserRealNameField.php
+++ b/src/applications/people/customfield/PhabricatorUserRealNameField.php
@@ -1,68 +1,68 @@
value = $object->getRealName();
}
public function getOldValueForApplicationTransactions() {
return $this->getObject()->getRealName();
}
public function getNewValueForApplicationTransactions() {
if (!$this->isEditable()) {
return $this->getObject()->getRealName();
}
return $this->value;
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->getObject()->setRealName($xaction->getNewValue());
}
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}
public function renderEditControl(array $handles) {
return id(new AphrontFormTextControl())
->setName($this->getFieldKey())
->setValue($this->value)
->setLabel($this->getFieldName())
->setDisabled(!$this->isEditable());
}
private function isEditable() {
return PhabricatorEnv::getEnvConfig('account.editable');
}
}
diff --git a/src/applications/people/customfield/PhabricatorUserTitleField.php b/src/applications/people/customfield/PhabricatorUserTitleField.php
index 472abe163..6e13b93aa 100644
--- a/src/applications/people/customfield/PhabricatorUserTitleField.php
+++ b/src/applications/people/customfield/PhabricatorUserTitleField.php
@@ -1,61 +1,61 @@
value = $object->loadUserProfile()->getTitle();
}
public function getOldValueForApplicationTransactions() {
return $this->getObject()->loadUserProfile()->getTitle();
}
public function getNewValueForApplicationTransactions() {
return $this->value;
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->getObject()->loadUserProfile()->setTitle($xaction->getNewValue());
}
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}
public function renderEditControl(array $handles) {
return id(new AphrontFormTextControl())
->setName($this->getFieldKey())
->setValue($this->value)
->setLabel($this->getFieldName())
->setCaption(pht('Serious business title.'));
}
}
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php
index 56f48e889..133bf382b 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomField.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php
@@ -1,1078 +1,1094 @@
getCustomFields();
} catch (PhabricatorDataNotAttachedException $ex) {
$attachment = new PhabricatorCustomFieldAttachment();
$object->attachCustomFields($attachment);
}
try {
$field_list = $attachment->getCustomFieldList($role);
} catch (PhabricatorCustomFieldNotAttachedException $ex) {
$base_class = $object->getCustomFieldBaseClass();
$spec = $object->getCustomFieldSpecificationForRole($role);
if (!is_array($spec)) {
$obj_class = get_class($object);
throw new Exception(
"Expected an array from getCustomFieldSpecificationForRole() for ".
"object of class '{$obj_class}'.");
}
$fields = PhabricatorCustomField::buildFieldList($base_class, $spec);
foreach ($fields as $key => $field) {
if (!$field->shouldEnableForRole($role)) {
unset($fields[$key]);
}
}
foreach ($fields as $field) {
$field->setObject($object);
}
$field_list = new PhabricatorCustomFieldList($fields);
$attachment->addCustomFieldList($role, $field_list);
}
return $field_list;
}
/**
* @task apps
*/
public static function getObjectField(
PhabricatorCustomFieldInterface $object,
$role,
$field_key) {
$fields = self::getObjectFields($object, $role)->getFields();
return idx($fields, $field_key);
}
/**
* @task apps
*/
public static function buildFieldList($base_class, array $spec) {
$field_objects = id(new PhutilSymbolLoader())
->setAncestorClass($base_class)
->loadObjects();
$fields = array();
$from_map = array();
foreach ($field_objects as $field_object) {
$current_class = get_class($field_object);
foreach ($field_object->createFields() as $field) {
$key = $field->getFieldKey();
if (isset($fields[$key])) {
$original_class = $from_map[$key];
throw new Exception(
"Both '{$original_class}' and '{$current_class}' define a custom ".
"field with field key '{$key}'. Field keys must be unique.");
}
$from_map[$key] = $current_class;
$fields[$key] = $field;
}
}
foreach ($fields as $key => $field) {
if (!$field->isFieldEnabled()) {
unset($fields[$key]);
}
}
$fields = array_select_keys($fields, array_keys($spec)) + $fields;
foreach ($spec as $key => $config) {
if (empty($fields[$key])) {
continue;
}
if (!empty($config['disabled'])) {
if ($fields[$key]->canDisableField()) {
unset($fields[$key]);
}
}
}
return $fields;
}
/* -( Core Properties and Field Identity )--------------------------------- */
/**
* Return a key which uniquely identifies this field, like
* "mycompany:dinosaur:count". Normally you should provide some level of
* namespacing to prevent collisions.
*
* @return string String which uniquely identifies this field.
* @task core
*/
public function getFieldKey() {
if ($this->proxy) {
return $this->proxy->getFieldKey();
}
throw new PhabricatorCustomFieldImplementationIncompleteException(
$this,
$field_key_is_incomplete = true);
}
/**
* Return a human-readable field name.
*
* @return string Human readable field name.
* @task core
*/
public function getFieldName() {
if ($this->proxy) {
return $this->proxy->getFieldName();
}
return $this->getFieldKey();
}
/**
* Return a short, human-readable description of the field's behavior. This
* provides more context to administrators when they are customizing fields.
*
* @return string|null Optional human-readable description.
* @task core
*/
public function getFieldDescription() {
if ($this->proxy) {
return $this->proxy->getFieldDescription();
}
return null;
}
/**
* Most field implementations are unique, in that one class corresponds to
* one field. However, some field implementations are general and a single
* implementation may drive several fields.
*
* For general implementations, the general field implementation can return
* multiple field instances here.
*
* @return list List of fields.
* @task core
*/
public function createFields() {
return array($this);
}
/**
* You can return `false` here if the field should not be enabled for any
* role. For example, it might depend on something (like an application or
* library) which isn't installed, or might have some global configuration
* which allows it to be disabled.
*
* @return bool False to completely disable this field for all roles.
* @task core
*/
public function isFieldEnabled() {
if ($this->proxy) {
return $this->proxy->isFieldEnabled();
}
return true;
}
/**
* Low level selector for field availability. Fields can appear in different
* roles (like an edit view, a list view, etc.), but not every field needs
* to appear everywhere. Fields that are disabled in a role won't appear in
* that context within applications.
*
* Normally, you do not need to override this method. Instead, override the
* methods specific to roles you want to enable. For example, implement
* @{method:shouldUseStorage()} to activate the `'storage'` role.
*
* @return bool True to enable the field for the given role.
* @task core
*/
public function shouldEnableForRole($role) {
if ($this->proxy) {
return $this->proxy->shouldEnableForRole($role);
}
switch ($role) {
case self::ROLE_APPLICATIONTRANSACTIONS:
return $this->shouldAppearInApplicationTransactions();
case self::ROLE_APPLICATIONSEARCH:
return $this->shouldAppearInApplicationSearch();
case self::ROLE_STORAGE:
return $this->shouldUseStorage();
case self::ROLE_EDIT:
return $this->shouldAppearInEditView();
case self::ROLE_VIEW:
return $this->shouldAppearInPropertyView();
case self::ROLE_LIST:
return $this->shouldAppearInListView();
case self::ROLE_DEFAULT:
return true;
default:
throw new Exception("Unknown field role '{$role}'!");
}
}
/**
* Allow administrators to disable this field. Most fields should allow this,
* but some are fundamental to the behavior of the application and can be
* locked down to avoid chaos, disorder, and the decline of civilization.
*
* @return bool False to prevent this field from being disabled through
* configuration.
* @task core
*/
public function canDisableField() {
return true;
}
/**
* Return an index string which uniquely identifies this field.
*
* @return string Index string which uniquely identifies this field.
* @task core
*/
final public function getFieldIndex() {
return PhabricatorHash::digestForIndex($this->getFieldKey());
}
/* -( Field Proxies )------------------------------------------------------ */
/**
* Proxies allow a field to use some other field's implementation for most
* of their behavior while still subclassing an application field. When a
* proxy is set for a field with @{method:setProxy}, all of its methods will
* call through to the proxy by default.
*
* This is most commonly used to implement configuration-driven custom fields
* using @{class:PhabricatorStandardCustomField}.
*
* This method must be overridden to return `true` before a field can accept
* proxies.
*
* @return bool True if you can @{method:setProxy} this field.
* @task proxy
*/
public function canSetProxy() {
if ($this instanceof PhabricatorStandardCustomFieldInterface) {
return true;
}
return false;
}
/**
* Set the proxy implementation for this field. See @{method:canSetProxy} for
* discussion of field proxies.
*
* @param PhabricatorCustomField Field implementation.
* @return this
*/
final public function setProxy(PhabricatorCustomField $proxy) {
if (!$this->canSetProxy()) {
throw new PhabricatorCustomFieldNotProxyException($this);
}
$this->proxy = $proxy;
return $this;
}
/**
* Get the field's proxy implementation, if any. For discussion, see
* @{method:canSetProxy}.
*
* @return PhabricatorCustomField|null Proxy field, if one is set.
*/
final public function getProxy() {
return $this->proxy;
}
/* -( Contextual Data )---------------------------------------------------- */
/**
* Sets the object this field belongs to.
*
* @param PhabricatorCustomFieldInterface The object this field belongs to.
+ * @return this
* @task context
*/
final public function setObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->setObject($object);
return $this;
}
$this->object = $object;
$this->didSetObject($object);
return $this;
}
+ /**
+ * Read object data into local field storage, if applicable.
+ *
+ * @param PhabricatorCustomFieldInterface The object this field belongs to.
+ * @return this
+ * @task context
+ */
+ public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
+ if ($this->proxy) {
+ $this->proxy->readValueFromObject($object);
+ }
+ return $this;
+ }
+
+
/**
* Get the object this field belongs to.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @task context
*/
final public function getObject() {
if ($this->proxy) {
return $this->proxy->getObject();
}
return $this->object;
}
/**
* This is a hook, primarily for subclasses to load object data.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @return void
*/
protected function didSetObject(PhabricatorCustomFieldInterface $object) {
return;
}
/**
* @task context
*/
final public function setViewer(PhabricatorUser $viewer) {
if ($this->proxy) {
$this->proxy->setViewer($viewer);
return $this;
}
$this->viewer = $viewer;
return $this;
}
/**
* @task context
*/
final public function getViewer() {
if ($this->proxy) {
return $this->proxy->getViewer();
}
return $this->viewer;
}
/**
* @task context
*/
final protected function requireViewer() {
if ($this->proxy) {
return $this->proxy->requireViewer();
}
if (!$this->viewer) {
throw new PhabricatorCustomFieldDataNotAvailableException($this);
}
return $this->viewer;
}
/* -( Storage )------------------------------------------------------------ */
/**
* Return true to use field storage.
*
* Fields which can be edited by the user will most commonly use storage,
* while some other types of fields (for instance, those which just display
* information in some stylized way) may not. Many builtin fields do not use
* storage because their data is available on the object itself.
*
* If you implement this, you must also implement @{method:getValueForStorage}
* and @{method:setValueFromStorage}.
*
* @return bool True to use storage.
* @task storage
*/
public function shouldUseStorage() {
if ($this->proxy) {
return $this->proxy->shouldUseStorage();
}
return false;
}
/**
* Return a new, empty storage object. This should be a subclass of
* @{class:PhabricatorCustomFieldStorage} which is bound to the application's
* database.
*
* @return PhabricatorCustomFieldStorage New empty storage object.
* @task storage
*/
public function newStorageObject() {
if ($this->proxy) {
return $this->proxy->newStorageObject();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Return a serialized representation of the field value, appropriate for
* storing in auxiliary field storage. You must implement this method if
* you implement @{method:shouldUseStorage}.
*
* If the field value is a scalar, it can be returned unmodiifed. If not,
* it should be serialized (for example, using JSON).
*
* @return string Serialized field value.
* @task storage
*/
public function getValueForStorage() {
if ($this->proxy) {
return $this->proxy->getValueForStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Set the field's value given a serialized storage value. This is called
* when the field is loaded; if no data is available, the value will be
* null. You must implement this method if you implement
* @{method:shouldUseStorage}.
*
* Usually, the value can be loaded directly. If it isn't a scalar, you'll
* need to undo whatever serialization you applied in
* @{method:getValueForStorage}.
*
* @param string|null Serialized field representation (from
* @{method:getValueForStorage}) or null if no value has
* ever been stored.
* @return this
* @task storage
*/
public function setValueFromStorage($value) {
if ($this->proxy) {
return $this->proxy->setValueFromStorage($value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( ApplicationSearch )-------------------------------------------------- */
/**
* Appearing in ApplicationSearch allows a field to be indexed and searched
* for.
*
* @return bool True to appear in ApplicationSearch.
* @task appsearch
*/
public function shouldAppearInApplicationSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationSearch();
}
return false;
}
/**
* Return one or more indexes which this field can meaningfully query against
* to implement ApplicationSearch.
*
* Normally, you should build these using @{method:newStringIndex} and
* @{method:newNumericIndex}. For example, if a field holds a numeric value
* it might return a single numeric index:
*
* return array($this->newNumericIndex($this->getValue()));
*
* If a field holds a more complex value (like a list of users), it might
* return several string indexes:
*
* $indexes = array();
* foreach ($this->getValue() as $phid) {
* $indexes[] = $this->newStringIndex($phid);
* }
* return $indexes;
*
* @return list List of indexes.
* @task appsearch
*/
public function buildFieldIndexes() {
if ($this->proxy) {
return $this->proxy->buildFieldIndexes();
}
return array();
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newStringIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newNumericIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build and populate storage for a string index.
*
* @param string String to index.
* @return PhabricatorCustomFieldStringIndexStorage Populated storage.
* @task appsearch
*/
protected function newStringIndex($value) {
if ($this->proxy) {
return $this->proxy->newStringIndex();
}
$key = $this->getFieldIndex();
return $this->newStringIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Build and populate storage for a numeric index.
*
* @param string Numeric value to index.
* @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
* @task appsearch
*/
protected function newNumericIndex($value) {
if ($this->proxy) {
return $this->proxy->newNumericIndex();
}
$key = $this->getFieldIndex();
return $this->newNumericIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Read a query value from a request, for storage in a saved query. Normally,
* this method should, e.g., read a string out of the request.
*
* @param PhabricatorApplicationSearchEngine Engine building the query.
* @param AphrontRequest Request to read from.
* @return wild
* @task appsearch
*/
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readApplicationSearchValueFromRequest(
$engine,
$request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Constrain a query, given a field value. Generally, this method should
* use `with...()` methods to apply filters or other constraints to the
* query.
*
* @param PhabricatorApplicationSearchEngine Engine executing the query.
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param wild Constraint provided by the user.
* @return void
* @task appsearch
*/
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if ($this->proxy) {
return $this->proxy->applyApplicationSearchConstraintToQuery(
$engine,
$query,
$value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Append search controls to the interface. If you need handles, use
* @{method:getRequiredHandlePHIDsForApplicationSearch} to get them.
*
* @param PhabricatorApplicationSearchEngine Engine constructing the form.
* @param AphrontFormView The form to update.
* @param wild Value from the saved query.
* @param list List of handles.
* @return void
* @task appsearch
*/
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value,
array $handles) {
if ($this->proxy) {
return $this->proxy->appendToApplicationSearchForm(
$engine,
$form,
$value,
$handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Return a list of PHIDs which @{method:appendToApplicationSearchForm} needs
* handles for. This is primarily useful if the field stores PHIDs and you
* need to (for example) render a tokenizer control.
*
* @param wild Value from the saved query.
* @return list List of PHIDs.
* @task appsearch
*/
public function getRequiredHandlePHIDsForApplicationSearch($value) {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value);
}
return array();
}
/* -( ApplicationTransactions )-------------------------------------------- */
/**
* Appearing in ApplicationTrasactions allows a field to be edited using
* standard workflows.
*
* @return bool True to appear in ApplicationTransactions.
* @task appxaction
*/
public function shouldAppearInApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationTransactions();
}
return false;
}
/**
* @task appxaction
*/
public function getApplicationTransactionType() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionType();
}
return PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
/**
* @task appxaction
*/
public function getApplicationTransactionMetadata() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionMetadata();
}
return array();
}
/**
* @task appxaction
*/
public function getOldValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getOldValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function getNewValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getNewValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function setValueFromApplicationTransactions($value) {
if ($this->proxy) {
return $this->proxy->setValueFromApplicationTransactions($value);
}
return $this->setValueFromStorage($value);
}
/**
* @task appxaction
*/
public function getNewValueFromApplicationTransactions(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getNewValueFromApplicationTransactions($xaction);
}
return $xaction->getNewValue();
}
/**
* @task appxaction
*/
public function getApplicationTransactionHasEffect(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasEffect($xaction);
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
/**
* @task appxaction
*/
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
}
return;
}
/**
* @task appxaction
*/
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
}
if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
return;
}
$this->setValueFromApplicationTransactions($xaction->getNewValue());
$value = $this->getValueForStorage();
$table = $this->newStorageObject();
$conn_w = $table->establishConnection('w');
if ($value === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex());
} else {
queryfx(
$conn_w,
'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex(),
$value);
}
return;
}
/**
* Validate transactions for an object. This allows you to raise an error
* when a transaction would set a field to an invalid value, or when a field
* is required but no transactions provide value.
*
* @param PhabricatorLiskDAO Editor applying the transactions.
* @param string Transaction type. This type is always
* `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
* convenience when constructing exceptions.
* @param list Transactions being applied,
* which may be empty if this field is not being edited.
* @return list Validation
* errors.
*
* @task appxaction
*/
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
if ($this->proxy) {
return $this->proxy->validateApplicationTransactions(
$editor,
$type,
$xactions);
}
return array();
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitle(
$xaction);
}
$author_phid = $xaction->getAuthorPHID();
return pht(
'%s updated this object.',
$xaction->renderHandleLink($author_phid));
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction,
PhabricatorFeedStory $story) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitleForFeed(
$xaction,
$story);
}
$author_phid = $xaction->getAuthorPHID();
$object_phid = $xaction->getObjectPHID();
return pht(
'%s updated %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid));
}
public function getApplicationTransactionHasChangeDetails(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasChangeDetails(
$xaction);
}
return false;
}
public function getApplicationTransactionChangeDetails(
PhabricatorApplicationTransaction $xaction,
PhabricatorUser $viewer) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionChangeDetails(
$xaction,
$viewer);
}
return null;
}
public function getApplicationTransactionRequiredHandlePHIDs(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
$xaction);
}
return array();
}
/* -( Edit View )---------------------------------------------------------- */
/**
* @task edit
*/
public function shouldAppearInEditView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditView();
}
return false;
}
/**
* @task edit
*/
public function readValueFromRequest(AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readValueFromRequest($request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task edit
*/
public function getRequiredHandlePHIDsForEdit() {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForEdit();
}
return array();
}
/**
* @task edit
*/
public function renderEditControl(array $handles) {
if ($this->proxy) {
return $this->proxy->renderEditControl($handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( Property View )------------------------------------------------------ */
/**
* @task view
*/
public function shouldAppearInPropertyView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInPropertyView();
}
return false;
}
/**
* @task view
*/
public function renderPropertyViewLabel() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewLabel();
}
return $this->getFieldName();
}
/**
* @task view
*/
public function renderPropertyViewValue() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task view
*/
public function getStyleForPropertyView() {
if ($this->proxy) {
return $this->proxy->getStyleForPropertyView();
}
return 'property';
}
/* -( List View )---------------------------------------------------------- */
/**
* @task list
*/
public function shouldAppearInListView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInListView();
}
return false;
}
/**
* @task list
*/
public function renderOnListItem(PHUIObjectItemView $view) {
if ($this->proxy) {
return $this->proxy->renderOnListItem($view);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
}
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
index d5623771d..ffe96bc28 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
@@ -1,302 +1,307 @@
fields = $fields;
}
public function getFields() {
return $this->fields;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
foreach ($this->getFields() as $field) {
$field->setViewer($viewer);
}
return $this;
}
/**
* Read stored values for all fields which support storage.
*
* @param PhabricatorCustomFieldInterface Object to read field values for.
* @return void
*/
public function readFieldsFromStorage(
PhabricatorCustomFieldInterface $object) {
+ foreach ($this->fields as $field) {
+ $field->setObject($object);
+ $field->readValueFromObject($object);
+ }
+
$keys = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_STORAGE)) {
$keys[$field->getFieldIndex()] = $field;
}
}
if (!$keys) {
return;
}
// NOTE: We assume all fields share the same storage. This isn't guaranteed
// to be true, but always is for now.
$table = head($keys)->newStorageObject();
$objects = array();
if ($object->getPHID()) {
$objects = $table->loadAllWhere(
'objectPHID = %s AND fieldIndex IN (%Ls)',
$object->getPHID(),
array_keys($keys));
$objects = mpull($objects, null, 'getFieldIndex');
}
foreach ($keys as $key => $field) {
$storage = idx($objects, $key);
if ($storage) {
$field->setValueFromStorage($storage->getFieldValue());
} else if ($object->getPHID()) {
// NOTE: We set this only if the object exists. Otherwise, we allow the
// field to retain any default value it may have.
$field->setValueFromStorage(null);
}
}
}
public function appendFieldsToForm(AphrontFormView $form) {
$enabled = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_EDIT)) {
$enabled[] = $field;
}
}
$phids = array();
foreach ($enabled as $field_key => $field) {
$phids[$field_key] = $field->getRequiredHandlePHIDsForEdit();
}
$all_phids = array_mergev($phids);
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs($all_phids)
->execute();
} else {
$handles = array();
}
foreach ($enabled as $field_key => $field) {
$field_handles = array_select_keys($handles, $phids[$field_key]);
$form->appendChild($field->renderEditControl($field_handles));
}
}
public function appendFieldsToPropertyList(
PhabricatorCustomFieldInterface $object,
PhabricatorUser $viewer,
PHUIPropertyListView $view) {
$this->readFieldsFromStorage($object);
$fields = $this->fields;
foreach ($fields as $field) {
$field->setViewer($viewer);
}
// Move all the blocks to the end, regardless of their configuration order,
// because it always looks silly to render a block in the middle of a list
// of properties.
$head = array();
$tail = array();
foreach ($fields as $key => $field) {
$style = $field->getStyleForPropertyView();
switch ($style) {
case 'property':
case 'header':
$head[$key] = $field;
break;
case 'block':
$tail[$key] = $field;
break;
default:
throw new Exception(
"Unknown field property view style '{$style}'; valid styles are ".
"'block' and 'property'.");
}
}
$fields = $head + $tail;
$add_header = null;
foreach ($fields as $field) {
$label = $field->renderPropertyViewLabel();
$value = $field->renderPropertyViewValue();
if ($value !== null) {
switch ($field->getStyleForPropertyView()) {
case 'header':
// We want to hide headers if the fields the're assciated with
// don't actually produce any visible properties. For example, in a
// list like this:
//
// Header A
// Prop A: Value A
// Header B
// Prop B: Value B
//
// ...if the "Prop A" field returns `null` when rendering its
// property value and we rendered naively, we'd get this:
//
// Header A
// Header B
// Prop B: Value B
//
// This is silly. Instead, we hide "Header A".
$add_header = $value;
break;
case 'property':
if ($add_header !== null) {
// Add the most recently seen header.
$view->addSectionHeader($add_header);
$add_header = null;
}
$view->addProperty($label, $value);
break;
case 'block':
$view->invokeWillRenderEvent();
if ($label !== null) {
$view->addSectionHeader($label);
}
$view->addTextContent($value);
break;
}
}
}
}
public function buildFieldTransactionsFromRequest(
PhabricatorApplicationTransaction $template,
AphrontRequest $request) {
$xactions = array();
$role = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$transaction_type = $field->getApplicationTransactionType();
$xaction = id(clone $template)
->setTransactionType($transaction_type);
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions only, we provide the old value
// as an input.
$old_value = $field->getOldValueForApplicationTransactions();
$xaction->setOldValue($old_value);
}
$field->readValueFromRequest($request);
$xaction
->setNewValue($field->getNewValueForApplicationTransactions());
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions, add the field key in metadata.
$xaction->setMetadataValue('customfield:key', $field->getFieldKey());
}
$metadata = $field->getApplicationTransactionMetadata();
foreach ($metadata as $key => $value) {
$xaction->setMetadataValue($key, $value);
}
$xactions[] = $xaction;
}
return $xactions;
}
/**
* Publish field indexes into index tables, so ApplicationSearch can search
* them.
*
* @return void
*/
public function rebuildIndexes(PhabricatorCustomFieldInterface $object) {
$indexes = array();
$index_keys = array();
$phid = $object->getPHID();
$role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$index_keys[$field->getFieldIndex()] = true;
foreach ($field->buildFieldIndexes() as $index) {
$index->setObjectPHID($phid);
$indexes[$index->getTableName()][] = $index;
}
}
if (!$indexes) {
return;
}
$any_index = head(head($indexes));
$conn_w = $any_index->establishConnection('w');
foreach ($indexes as $table => $index_list) {
$sql = array();
foreach ($index_list as $index) {
$sql[] = $index->formatForInsert($conn_w);
}
$indexes[$table] = $sql;
}
$any_index->openTransaction();
foreach ($indexes as $table => $sql_list) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)',
$table,
$phid,
array_keys($index_keys));
if (!$sql_list) {
continue;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q',
$table,
$chunk);
}
}
$any_index->saveTransaction();
}
}
diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php
index b926f8b4f..31b0798f8 100644
--- a/src/view/form/control/AphrontFormPolicyControl.php
+++ b/src/view/form/control/AphrontFormPolicyControl.php
@@ -1,229 +1,231 @@
object = $object;
return $this;
}
public function setPolicies(array $policies) {
assert_instances_of($policies, 'PhabricatorPolicy');
$this->policies = $policies;
return $this;
}
public function setCapability($capability) {
$this->capability = $capability;
$labels = array(
PhabricatorPolicyCapability::CAN_VIEW => pht('Visible To'),
PhabricatorPolicyCapability::CAN_EDIT => pht('Editable By'),
PhabricatorPolicyCapability::CAN_JOIN => pht('Joinable By'),
);
if (isset($labels[$capability])) {
$label = $labels[$capability];
} else {
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if ($capobj) {
$label = $capobj->getCapabilityName();
} else {
$label = pht('Capability "%s"', $capability);
}
}
$this->setLabel($label);
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-policy';
}
protected function getOptions() {
$capability = $this->capability;
$options = array();
foreach ($this->policies as $policy) {
if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) {
// Never expose "Public" for capabilities which don't support it.
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) {
continue;
}
}
$options[$policy->getType()][$policy->getPHID()] = array(
'name' => phutil_utf8_shorten($policy->getName(), 28),
'full' => $policy->getName(),
'icon' => $policy->getIcon(),
);
}
// If we were passed several custom policy options, throw away the ones
// which aren't the value for this capability. For example, an object might
// have a custom view pollicy and a custom edit policy. When we render
// the selector for "Can View", we don't want to show the "Can Edit"
// custom policy -- if we did, the menu would look like this:
//
// Custom
// Custom Policy
// Custom Policy
//
// ...where one is the "view" custom policy, and one is the "edit" custom
// policy.
$type_custom = PhabricatorPolicyType::TYPE_CUSTOM;
if (!empty($options[$type_custom])) {
$options[$type_custom] = array_select_keys(
$options[$type_custom],
array($this->getValue()));
}
// If there aren't any custom policies, add a placeholder policy so we
// render a menu item. This allows the user to switch to a custom policy.
if (empty($options[$type_custom])) {
$placeholder = new PhabricatorPolicy();
$placeholder->setName(pht('Custom Policy...'));
$options[$type_custom][$this->getCustomPolicyPlaceholder()] = array(
'name' => $placeholder->getName(),
'full' => $placeholder->getName(),
'icon' => $placeholder->getIcon(),
);
}
$options = array_select_keys(
$options,
array(
PhabricatorPolicyType::TYPE_GLOBAL,
PhabricatorPolicyType::TYPE_USER,
PhabricatorPolicyType::TYPE_CUSTOM,
PhabricatorPolicyType::TYPE_PROJECT,
));
return $options;
}
protected function renderInput() {
if (!$this->object) {
throw new Exception(pht("Call setPolicyObject() before rendering!"));
}
if (!$this->capability) {
throw new Exception(pht("Call setCapability() before rendering!"));
}
$policy = $this->object->getPolicy($this->capability);
if (!$policy) {
// TODO: Make this configurable.
$policy = PhabricatorPolicies::POLICY_USER;
}
if (!$this->getValue()) {
$this->setValue($policy);
}
$control_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
$caret = phutil_tag(
'span',
array(
'class' => 'caret',
));
$input = phutil_tag(
'input',
array(
'type' => 'hidden',
'id' => $input_id,
'name' => $this->getName(),
'value' => $this->getValue(),
));
$options = $this->getOptions();
$order = array();
$labels = array();
foreach ($options as $key => $values) {
$order[$key] = array_keys($values);
$labels[$key] = PhabricatorPolicyType::getPolicyTypeName($key);
}
$flat_options = array_mergev($options);
$icons = array();
foreach (igroup($flat_options, 'icon') as $icon => $ignored) {
$icons[$icon] = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_STATUS)
->setSpriteIcon($icon);
}
Javelin::initBehavior(
'policy-control',
array(
'controlID' => $control_id,
'inputID' => $input_id,
'options' => $flat_options,
'groups' => array_keys($options),
'order' => $order,
'icons' => $icons,
'labels' => $labels,
'value' => $this->getValue(),
'customPlaceholder' => $this->getCustomPolicyPlaceholder(),
));
- $selected = $flat_options[$this->getValue()];
+ $selected = idx($flat_options, $this->getValue(), array());
+ $selected_icon = idx($selected, 'icon');
+ $selected_name = idx($selected, 'name');
return phutil_tag(
'div',
array(
),
array(
javelin_tag(
'a',
array(
'class' => 'grey button dropdown has-icon policy-control',
'href' => '#',
'mustcapture' => true,
'sigil' => 'policy-control',
'id' => $control_id,
),
array(
$caret,
javelin_tag(
'span',
array(
'sigil' => 'policy-label',
'class' => 'phui-button-text',
),
array(
- $icons[$selected['icon']],
- $selected['name'],
+ idx($icons, $selected_icon),
+ $selected_name,
)),
)),
$input,
));
return AphrontFormSelectControl::renderSelectTag(
$this->getValue(),
$this->getOptions(),
array(
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
));
}
private function getCustomPolicyPlaceholder() {
return 'custom:placeholder';
}
}